Skip to content

Commit

Permalink
feat: Add reload-include and reload-exclude from uvicorn to CLI (#…
Browse files Browse the repository at this point in the history
…2973)

* Add reload-include and reload-exclude feature

* Update documentation

* Fix tests, update documentation
  • Loading branch information
FergusMok authored and provinzkraut committed Feb 4, 2024
1 parent 1eec87d commit e34ffa7
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 12 deletions.
38 changes: 38 additions & 0 deletions docs/usage/cli.rst
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ Options
+-------------------------------------------+----------------------------------------------+----------------------------------------------------------------------------------+
| ``-R``\ , ``--reload-dir`` | ``LITESTAR_RELOAD_DIRS`` | Specify directories to watch for reload. |
+-------------------------------------------+----------------------------------------------+----------------------------------------------------------------------------------+
| ``-I``\ , ``--reload-include`` | ``LITESTAR_RELOAD_INCLUDES`` | Specify glob patterns for files to include when watching for reload. |
+-------------------------------------------+----------------------------------------------+----------------------------------------------------------------------------------+
| ``-E``\ , ``--reload-exclude`` | ``LITESTAR_RELOAD_EXCLUDES`` | Specify glob patterns for files to exclude when watching for reload. |
+-------------------------------------------+----------------------------------------------+----------------------------------------------------------------------------------+
| ``-p``\ , ``--port`` | ``LITESTAR_PORT`` | Bind the server to this port [default: 8000] |
+-------------------------------------------+----------------------------------------------+----------------------------------------------------------------------------------+
| ``--wc``\ , ``--web-concurrency`` | ``WEB_CONCURRENCY`` | The number of concurrent web workers to start [default: 1] |
Expand Down Expand Up @@ -143,6 +147,40 @@ To set multiple directories via an environment variable, use a comma-separated l
LITESTAR_RELOAD_DIRS=.,../other-library/src
--reload-include
++++++++++++++++

The ``--reload-include`` flag allows you to specify glob patterns to include when watching for file changes. If you specify this flag, the ``--reload`` flag is implied. Furthermore, ``.py`` files are included implicitly by default.

You can specify multiple glob patterns by passing the flag multiple times:

.. code-block:: shell
litestar run --reload-include="*.rst" --reload-include="*.yml"
To set multiple directories via an environment variable, use a comma-separated list:

.. code-block:: shell
LITESTAR_RELOAD_INCLUDES=*.rst,*.yml
--reload-exclude
++++++++++++++++

The ``--reload-exclude`` flag allows you to specify glob patterns to exclude when watching for file changes. If you specify this flag, the ``--reload`` flag is implied.

You can specify multiple glob patterns by passing the flag multiple times:

.. code-block:: shell
litestar run --reload-exclude="*.py" --reload-exclude="*.yml"
To set multiple directories via an environment variable, use a comma-separated list:

.. code-block:: shell
LITESTAR_RELOAD_EXCLUDES=*.py,*.yml
SSL
+++

Expand Down
6 changes: 6 additions & 0 deletions litestar/cli/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ class LitestarEnv:
uds: str | None = None
reload: bool | None = None
reload_dirs: tuple[str, ...] | None = None
reload_include: tuple[str, ...] | None = None
reload_exclude: tuple[str, ...] | None = None
web_concurrency: int | None = None
is_app_factory: bool = False
certfile_path: str | None = None
Expand Down Expand Up @@ -120,6 +122,8 @@ def from_env(cls, app_path: str | None, app_dir: Path | None = None) -> Litestar
uds = getenv("LITESTAR_UNIX_DOMAIN_SOCKET")
fd = getenv("LITESTAR_FILE_DESCRIPTOR")
reload_dirs = tuple(s.strip() for s in getenv("LITESTAR_RELOAD_DIRS", "").split(",") if s) or None
reload_include = tuple(s.strip() for s in getenv("LITESTAR_RELOAD_INCLUDES", "").split(",") if s) or None
reload_exclude = tuple(s.strip() for s in getenv("LITESTAR_RELOAD_EXCLUDES", "").split(",") if s) or None

return cls(
app_path=loaded_app.app_path,
Expand All @@ -131,6 +135,8 @@ def from_env(cls, app_path: str | None, app_dir: Path | None = None) -> Litestar
fd=int(fd) if fd else None,
reload=_bool_from_env("LITESTAR_RELOAD"),
reload_dirs=reload_dirs,
reload_include=reload_include,
reload_exclude=reload_exclude,
web_concurrency=int(web_concurrency) if web_concurrency else None,
is_app_factory=loaded_app.is_factory,
cwd=cwd,
Expand Down
20 changes: 19 additions & 1 deletion litestar/cli/commands/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ def _run_uvicorn_in_subprocess(
workers: int | None,
reload: bool,
reload_dirs: tuple[str, ...] | None,
reload_include: tuple[str, ...] | None,
reload_exclude: tuple[str, ...] | None,
fd: int | None,
uds: str | None,
certfile_path: str | None,
Expand All @@ -87,6 +89,10 @@ def _run_uvicorn_in_subprocess(
process_args["uds"] = uds
if reload_dirs:
process_args["reload-dir"] = reload_dirs
if reload_include:
process_args["reload-include"] = reload_include
if reload_exclude:
process_args["reload-exclude"] = reload_exclude
if certfile_path is not None:
process_args["ssl-certfile"] = certfile_path
if keyfile_path is not None:
Expand Down Expand Up @@ -116,6 +122,12 @@ def info_command(app: Litestar) -> None:
@command(name="run")
@option("-r", "--reload", help="Reload server on changes", default=False, is_flag=True)
@option("-R", "--reload-dir", help="Directories to watch for file changes", multiple=True)
@option(
"-I", "--reload-include", help="Glob patterns for files to include when watching for file changes", multiple=True
)
@option(
"-E", "--reload-exclude", help="Glob patterns for files to exclude when watching for file changes", multiple=True
)
@option("-p", "--port", help="Serve under this port", type=int, default=8000, show_default=True)
@option(
"-W",
Expand Down Expand Up @@ -155,6 +167,8 @@ def run_command(
uds: str | None,
debug: bool,
reload_dir: tuple[str, ...],
reload_include: tuple[str, ...],
reload_exclude: tuple[str, ...],
pdb: bool,
ssl_certfile: str | None,
ssl_keyfile: str | None,
Expand Down Expand Up @@ -194,12 +208,14 @@ def run_command(
app = env.app

reload_dirs = env.reload_dirs or reload_dir
reload_include = env.reload_include or reload_include
reload_exclude = env.reload_exclude or reload_exclude

host = env.host or host
port = env.port if env.port is not None else port
fd = env.fd if env.fd is not None else fd
uds = env.uds or uds
reload = env.reload or reload or bool(reload_dirs)
reload = env.reload or reload or bool(reload_dirs) or bool(reload_include) or bool(reload_exclude)
workers = env.web_concurrency or wc

ssl_certfile = ssl_certfile or env.certfile_path
Expand Down Expand Up @@ -248,6 +264,8 @@ def run_command(
workers=workers,
reload=reload,
reload_dirs=reload_dirs,
reload_include=reload_include,
reload_exclude=reload_exclude,
fd=fd,
uds=uds,
certfile_path=certfile_path,
Expand Down
43 changes: 32 additions & 11 deletions tests/unit/test_cli/test_core_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,19 @@ def mock_show_app_info(mocker: MockerFixture) -> MagicMock:
@pytest.mark.parametrize("custom_app_file,", [Path("my_app.py"), None])
@pytest.mark.parametrize("app_dir", ["custom_subfolder", None])
@pytest.mark.parametrize(
"reload, reload_dir, web_concurrency",
"reload, reload_dir, reload_include, reload_exclude, web_concurrency",
[
(None, None, None),
(True, None, None),
(False, None, None),
(True, [".", "../somewhere_else"], None),
(False, [".", "../somewhere_else"], None),
(None, None, 2),
(True, None, 2),
(False, None, 2),
(None, None, None, None, None),
(True, None, None, None, None),
(False, None, None, None, None),
(True, [".", "../somewhere_else"], None, None, None),
(False, [".", "../somewhere_else"], None, None, None),
(True, None, ["*.rst", "*.yml"], None, None),
(False, None, None, ["*.py"], None),
(False, None, ["*.yml", "*.rst"], None, None),
(None, None, None, None, 2),
(True, None, None, None, 2),
(False, None, None, None, 2),
],
)
def test_run_command(
Expand All @@ -64,6 +67,8 @@ def test_run_command(
web_concurrency: Optional[int],
app_dir: Optional[str],
reload_dir: Optional[List[str]],
reload_include: Optional[List[str]],
reload_exclude: Optional[List[str]],
custom_app_file: Optional[Path],
create_app_file: CreateAppFileFixture,
set_in_env: bool,
Expand Down Expand Up @@ -131,14 +136,26 @@ def test_run_command(
else:
args.extend([f"--reload-dir={s}" for s in reload_dir])

if reload_include is not None:
if set_in_env:
monkeypatch.setenv("LITESTAR_RELOAD_INCLUDES", ",".join(reload_include))
else:
args.extend([f"--reload-include={s}" for s in reload_include])

if reload_exclude is not None:
if set_in_env:
monkeypatch.setenv("LITESTAR_RELOAD_EXCLUDES", ",".join(reload_exclude))
else:
args.extend([f"--reload-exclude={s}" for s in reload_exclude])

path = create_app_file(custom_app_file or "app.py", directory=app_dir)

result = runner.invoke(cli_command, args)

assert result.exception is None
assert result.exit_code == 0

if reload or reload_dir or web_concurrency > 1:
if reload or reload_dir or reload_include or reload_exclude or web_concurrency > 1:
expected_args = [
sys.executable,
"-m",
Expand All @@ -151,12 +168,16 @@ def test_run_command(
expected_args.append(f"--fd={fd}")
if uds is not None:
expected_args.append(f"--uds={uds}")
if reload or reload_dir:
if reload or reload_dir or reload_include or reload_exclude:
expected_args.append("--reload")
if web_concurrency:
expected_args.append(f"--workers={web_concurrency}")
if reload_dir:
expected_args.extend([f"--reload-dir={s}" for s in reload_dir])
if reload_include:
expected_args.extend([f"--reload-include={s}" for s in reload_include])
if reload_exclude:
expected_args.extend([f"--reload-exclude={s}" for s in reload_exclude])
mock_subprocess_run.assert_called_once()
assert sorted(mock_subprocess_run.call_args_list[0].args[0]) == sorted(expected_args)
else:
Expand Down

0 comments on commit e34ffa7

Please sign in to comment.