Skip to content

Commit

Permalink
Remove WatchGod (#2536)
Browse files Browse the repository at this point in the history
* Remove WatchGod

* Readd missing pragma

* Add py-darwin coverage
  • Loading branch information
Kludex authored Dec 14, 2024
1 parent a3cc360 commit 3aa1d01
Show file tree
Hide file tree
Showing 7 changed files with 64 additions and 284 deletions.
11 changes: 3 additions & 8 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ license = "BSD-3-Clause"
requires-python = ">=3.8"
authors = [
{ name = "Tom Christie", email = "[email protected]" },
{ name = "Marcelo Trylesinski", email = "[email protected]" }
{ name = "Marcelo Trylesinski", email = "[email protected]" },
]
classifiers = [
"Development Status :: 4 - Beta",
Expand Down Expand Up @@ -60,11 +60,7 @@ Source = "https://github.com/encode/uvicorn"
path = "uvicorn/__init__.py"

[tool.hatch.build.targets.sdist]
include = [
"/uvicorn",
"/tests",
"/requirements.txt",
]
include = ["/uvicorn", "/tests", "/requirements.txt"]

[tool.ruff]
line-length = 120
Expand Down Expand Up @@ -94,10 +90,9 @@ addopts = "-rxXs --strict-config --strict-markers"
xfail_strict = true
filterwarnings = [
"error",
'ignore: \"watchgod\" is deprecated\, you should switch to watchfiles \(`pip install watchfiles`\)\.:DeprecationWarning',
"ignore:Uvicorn's native WSGI implementation is deprecated.*:DeprecationWarning",
"ignore: 'cgi' is deprecated and slated for removal in Python 3.13:DeprecationWarning",
"ignore: remove second argument of ws_handler:DeprecationWarning:websockets"
"ignore: remove second argument of ws_handler:DeprecationWarning:websockets",
]

[tool.coverage.run]
Expand Down
1 change: 0 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ coverage==7.6.1; python_version < '3.9'
coverage==7.6.4; python_version >= '3.9'
coverage-conditional-plugin==0.9.0
httpx==0.27.2
watchgod==0.8.2

# Documentation
mkdocs==1.6.1
Expand Down
23 changes: 0 additions & 23 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
from hashlib import md5
from pathlib import Path
from tempfile import TemporaryDirectory
from threading import Thread
from time import sleep
from typing import Any
from uuid import uuid4

Expand Down Expand Up @@ -214,27 +212,6 @@ def make_tmp_dir(base_dir):
return


def sleep_touch(*paths: Path):
sleep(0.1)
for p in paths:
p.touch()


@pytest.fixture
def touch_soon():
threads = []

def start(*paths: Path):
thread = Thread(target=sleep_touch, args=paths)
thread.start()
threads.append(thread)

yield start

for t in threads:
t.join()


def _unused_port(socket_type: int) -> int:
"""Find an unused localhost port from 1024-65535 and return it."""
with contextlib.closing(socket.socket(type=socket_type)) as sock:
Expand Down
148 changes: 58 additions & 90 deletions tests/supervisors/test_reload.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
from __future__ import annotations

import logging
import platform
import signal
import socket
import sys
from pathlib import Path
from threading import Thread
from time import sleep
from typing import Callable, Generator

import pytest
from pytest_mock import MockerFixture

from tests.utils import as_cwd
from uvicorn.config import Config
Expand All @@ -20,11 +22,6 @@
except ImportError: # pragma: no cover
WatchFilesReload = None # type: ignore[misc,assignment]

try:
from uvicorn.supervisors.watchgodreload import WatchGodReload
except ImportError: # pragma: no cover
WatchGodReload = None # type: ignore[misc,assignment]


# TODO: Investigate why this is flaky on MacOS M1.
skip_if_m1 = pytest.mark.skipif(
Expand All @@ -33,17 +30,34 @@
)


def run(sockets):
def run(sockets: list[socket.socket] | None) -> None:
pass # pragma: no cover


def sleep_touch(*paths: Path):
sleep(0.1)
for p in paths:
p.touch()


@pytest.fixture
def touch_soon() -> Generator[Callable[[Path], None]]:
threads: list[Thread] = []

def start(*paths: Path) -> None:
thread = Thread(target=sleep_touch, args=paths)
thread.start()
threads.append(thread)

yield start

for t in threads:
t.join()


class TestBaseReload:
@pytest.fixture(autouse=True)
def setup(
self,
reload_directory_structure: Path,
reloader_class: type[BaseReload] | None,
):
def setup(self, reload_directory_structure: Path, reloader_class: type[BaseReload] | None):
if reloader_class is None: # pragma: no cover
pytest.skip("Needed dependency not installed")
self.reload_path = reload_directory_structure
Expand All @@ -52,17 +66,15 @@ def setup(
def _setup_reloader(self, config: Config) -> BaseReload:
config.reload_delay = 0 # save time

if self.reloader_class is WatchGodReload:
with pytest.deprecated_call():
reloader = self.reloader_class(config, target=run, sockets=[])
else:
reloader = self.reloader_class(config, target=run, sockets=[])
reloader = self.reloader_class(config, target=run, sockets=[])

assert config.should_reload
reloader.startup()
return reloader

def _reload_tester(self, touch_soon, reloader: BaseReload, *files: Path) -> list[Path] | None:
def _reload_tester(
self, touch_soon: Callable[[Path], None], reloader: BaseReload, *files: Path
) -> list[Path] | None:
reloader.restart()
if WatchFilesReload is not None and isinstance(reloader, WatchFilesReload):
touch_soon(*files)
Expand All @@ -73,7 +85,7 @@ def _reload_tester(self, touch_soon, reloader: BaseReload, *files: Path) -> list
file.touch()
return next(reloader)

@pytest.mark.parametrize("reloader_class", [StatReload, WatchGodReload, WatchFilesReload])
@pytest.mark.parametrize("reloader_class", [StatReload, WatchFilesReload])
def test_reloader_should_initialize(self) -> None:
"""
A basic sanity check.
Expand All @@ -86,8 +98,8 @@ def test_reloader_should_initialize(self) -> None:
reloader = self._setup_reloader(config)
reloader.shutdown()

@pytest.mark.parametrize("reloader_class", [StatReload, WatchGodReload, WatchFilesReload])
def test_reload_when_python_file_is_changed(self, touch_soon) -> None:
@pytest.mark.parametrize("reloader_class", [StatReload, WatchFilesReload])
def test_reload_when_python_file_is_changed(self, touch_soon: Callable[[Path], None]):
file = self.reload_path / "main.py"

with as_cwd(self.reload_path):
Expand All @@ -99,8 +111,8 @@ def test_reload_when_python_file_is_changed(self, touch_soon) -> None:

reloader.shutdown()

@pytest.mark.parametrize("reloader_class", [StatReload, WatchGodReload, WatchFilesReload])
def test_should_reload_when_python_file_in_subdir_is_changed(self, touch_soon) -> None:
@pytest.mark.parametrize("reloader_class", [StatReload, WatchFilesReload])
def test_should_reload_when_python_file_in_subdir_is_changed(self, touch_soon: Callable[[Path], None]):
file = self.reload_path / "app" / "sub" / "sub.py"

with as_cwd(self.reload_path):
Expand All @@ -111,8 +123,8 @@ def test_should_reload_when_python_file_in_subdir_is_changed(self, touch_soon) -

reloader.shutdown()

@pytest.mark.parametrize("reloader_class", [WatchFilesReload, WatchGodReload])
def test_should_not_reload_when_python_file_in_excluded_subdir_is_changed(self, touch_soon) -> None:
@pytest.mark.parametrize("reloader_class", [WatchFilesReload])
def test_should_not_reload_when_python_file_in_excluded_subdir_is_changed(self, touch_soon: Callable[[Path], None]):
sub_dir = self.reload_path / "app" / "sub"
sub_file = sub_dir / "sub.py"

Expand All @@ -129,7 +141,7 @@ def test_should_not_reload_when_python_file_in_excluded_subdir_is_changed(self,
reloader.shutdown()

@pytest.mark.parametrize("reloader_class, result", [(StatReload, False), (WatchFilesReload, True)])
def test_reload_when_pattern_matched_file_is_changed(self, result: bool, touch_soon) -> None:
def test_reload_when_pattern_matched_file_is_changed(self, result: bool, touch_soon: Callable[[Path], None]):
file = self.reload_path / "app" / "js" / "main.js"

with as_cwd(self.reload_path):
Expand All @@ -140,14 +152,10 @@ def test_reload_when_pattern_matched_file_is_changed(self, result: bool, touch_s

reloader.shutdown()

@pytest.mark.parametrize(
"reloader_class",
[
pytest.param(WatchFilesReload, marks=skip_if_m1),
WatchGodReload,
],
)
def test_should_not_reload_when_exclude_pattern_match_file_is_changed(self, touch_soon) -> None:
@pytest.mark.parametrize("reloader_class", [pytest.param(WatchFilesReload, marks=skip_if_m1)])
def test_should_not_reload_when_exclude_pattern_match_file_is_changed(
self, touch_soon: Callable[[Path], None]
): # pragma: py-darwin
python_file = self.reload_path / "app" / "src" / "main.py"
css_file = self.reload_path / "app" / "css" / "main.css"
js_file = self.reload_path / "app" / "js" / "main.js"
Expand All @@ -167,8 +175,8 @@ def test_should_not_reload_when_exclude_pattern_match_file_is_changed(self, touc

reloader.shutdown()

@pytest.mark.parametrize("reloader_class", [StatReload, WatchGodReload, WatchFilesReload])
def test_should_not_reload_when_dot_file_is_changed(self, touch_soon) -> None:
@pytest.mark.parametrize("reloader_class", [StatReload, WatchFilesReload])
def test_should_not_reload_when_dot_file_is_changed(self, touch_soon: Callable[[Path], None]):
file = self.reload_path / ".dotted"

with as_cwd(self.reload_path):
Expand All @@ -179,8 +187,8 @@ def test_should_not_reload_when_dot_file_is_changed(self, touch_soon) -> None:

reloader.shutdown()

@pytest.mark.parametrize("reloader_class", [StatReload, WatchGodReload, WatchFilesReload])
def test_should_reload_when_directories_have_same_prefix(self, touch_soon) -> None:
@pytest.mark.parametrize("reloader_class", [StatReload, WatchFilesReload])
def test_should_reload_when_directories_have_same_prefix(self, touch_soon: Callable[[Path], None]):
app_dir = self.reload_path / "app"
app_file = app_dir / "src" / "main.py"
app_first_dir = self.reload_path / "app_first"
Expand All @@ -201,13 +209,9 @@ def test_should_reload_when_directories_have_same_prefix(self, touch_soon) -> No

@pytest.mark.parametrize(
"reloader_class",
[
StatReload,
WatchGodReload,
pytest.param(WatchFilesReload, marks=skip_if_m1),
],
[StatReload, pytest.param(WatchFilesReload, marks=skip_if_m1)],
)
def test_should_not_reload_when_only_subdirectory_is_watched(self, touch_soon) -> None:
def test_should_not_reload_when_only_subdirectory_is_watched(self, touch_soon: Callable[[Path], None]):
app_dir = self.reload_path / "app"
app_dir_file = self.reload_path / "app" / "src" / "main.py"
root_file = self.reload_path / "main.py"
Expand All @@ -224,14 +228,8 @@ def test_should_not_reload_when_only_subdirectory_is_watched(self, touch_soon) -

reloader.shutdown()

@pytest.mark.parametrize(
"reloader_class",
[
pytest.param(WatchFilesReload, marks=skip_if_m1),
WatchGodReload,
],
)
def test_override_defaults(self, touch_soon) -> None:
@pytest.mark.parametrize("reloader_class", [pytest.param(WatchFilesReload, marks=skip_if_m1)])
def test_override_defaults(self, touch_soon: Callable[[Path], None]) -> None: # pragma: py-darwin
dotted_file = self.reload_path / ".dotted"
dotted_dir_file = self.reload_path / ".dotted_dir" / "file.txt"
python_file = self.reload_path / "main.py"
Expand All @@ -252,14 +250,8 @@ def test_override_defaults(self, touch_soon) -> None:

reloader.shutdown()

@pytest.mark.parametrize(
"reloader_class",
[
pytest.param(WatchFilesReload, marks=skip_if_m1),
WatchGodReload,
],
)
def test_explicit_paths(self, touch_soon) -> None:
@pytest.mark.parametrize("reloader_class", [pytest.param(WatchFilesReload, marks=skip_if_m1)])
def test_explicit_paths(self, touch_soon: Callable[[Path], None]) -> None: # pragma: py-darwin
dotted_file = self.reload_path / ".dotted"
non_dotted_file = self.reload_path / "ext" / "ext.jpg"
python_file = self.reload_path / "main.py"
Expand Down Expand Up @@ -307,33 +299,9 @@ def test_watchfiles_no_changes(self) -> None:

reloader.shutdown()

@pytest.mark.parametrize("reloader_class", [WatchGodReload])
def test_should_detect_new_reload_dirs(self, touch_soon, caplog: pytest.LogCaptureFixture, tmp_path: Path) -> None:
app_dir = tmp_path / "app"
app_file = app_dir / "file.py"
app_dir.mkdir()
app_file.touch()
app_first_dir = tmp_path / "app_first"
app_first_file = app_first_dir / "file.py"

with as_cwd(tmp_path):
config = Config(app="tests.test_config:asgi_app", reload=True, reload_includes=["app*"])
reloader = self._setup_reloader(config)
assert self._reload_tester(touch_soon, reloader, app_file)

app_first_dir.mkdir()
assert self._reload_tester(touch_soon, reloader, app_first_file)
assert caplog.records[-2].levelno == logging.INFO
assert (
caplog.records[-1].message == "WatchGodReload detected a new reload "
f"dir '{app_first_dir.name}' in '{tmp_path}'; Adding to watch list."
)

reloader.shutdown()


@pytest.mark.skipif(WatchFilesReload is None, reason="watchfiles not available")
def test_should_watch_one_dir_cwd(mocker, reload_directory_structure):
def test_should_watch_one_dir_cwd(mocker: MockerFixture, reload_directory_structure: Path):
mock_watch = mocker.patch("uvicorn.supervisors.watchfilesreload.watch")
app_dir = reload_directory_structure / "app"
app_first_dir = reload_directory_structure / "app_first"
Expand All @@ -350,7 +318,7 @@ def test_should_watch_one_dir_cwd(mocker, reload_directory_structure):


@pytest.mark.skipif(WatchFilesReload is None, reason="watchfiles not available")
def test_should_watch_separate_dirs_outside_cwd(mocker, reload_directory_structure):
def test_should_watch_separate_dirs_outside_cwd(mocker: MockerFixture, reload_directory_structure: Path):
mock_watch = mocker.patch("uvicorn.supervisors.watchfilesreload.watch")
app_dir = reload_directory_structure / "app"
app_first_dir = reload_directory_structure / "app_first"
Expand All @@ -368,7 +336,7 @@ def test_should_watch_separate_dirs_outside_cwd(mocker, reload_directory_structu
}


def test_display_path_relative(tmp_path):
def test_display_path_relative(tmp_path: Path):
with as_cwd(tmp_path):
p = tmp_path / "app" / "foobar.py"
# accept windows paths as wells as posix
Expand All @@ -380,8 +348,8 @@ def test_display_path_non_relative():
assert _display_path(p) in ("'/foo/bar.py'", "'\\foo\\bar.py'")


def test_base_reloader_run(tmp_path):
calls = []
def test_base_reloader_run(tmp_path: Path):
calls: list[str] = []
step = 0

class CustomReload(BaseReload):
Expand Down Expand Up @@ -411,7 +379,7 @@ def should_restart(self):
assert calls == ["startup", "restart", "shutdown"]


def test_base_reloader_should_exit(tmp_path):
def test_base_reloader_should_exit(tmp_path: Path):
config = Config(app="tests.test_config:asgi_app", reload=True)
reloader = BaseReload(config, target=run, sockets=[])
assert not reloader.should_exit.is_set()
Expand Down
2 changes: 1 addition & 1 deletion uvicorn/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ def resolve_reload_patterns(patterns_list: list[str], directories_list: list[str
# Special case for the .* pattern, otherwise this would only match
# hidden directories which is probably undesired
if pattern == ".*":
continue
continue # pragma: py-darwin
patterns.append(pattern)
if is_dir(Path(pattern)):
directories.append(Path(pattern))
Expand Down
Loading

0 comments on commit 3aa1d01

Please sign in to comment.