Skip to content

Commit

Permalink
add back "WatchGodReload" for backwards compatibility
Browse files Browse the repository at this point in the history
  • Loading branch information
samuelcolvin committed Apr 8, 2022
1 parent f500d50 commit 2a13f22
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 14 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -6,3 +6,4 @@ uvicorn.egg-info/
venv/
htmlcov/
site/
/.idea/
1 change: 1 addition & 0 deletions requirements.txt
Expand Up @@ -23,6 +23,7 @@ coverage==6.3.2
coverage-conditional-plugin==0.5.0
httpx==1.0.0b0
pytest-asyncio==0.15.1
watchgod==0.8.2


# Documentation
Expand Down
25 changes: 13 additions & 12 deletions tests/supervisors/test_reload.py
Expand Up @@ -9,6 +9,7 @@
from uvicorn.supervisors.basereload import BaseReload, _display_path
from uvicorn.supervisors.statreload import StatReload
from uvicorn.supervisors.watchfilesreload import WatchFilesReload
from uvicorn.supervisors.watchgodreload import WatchGodReload


def run(sockets):
Expand Down Expand Up @@ -36,16 +37,16 @@ def _reload_tester(self, touch_soon, reloader: BaseReload, *files: Path) -> bool
reloader.restart()

reloader.restart()
if isinstance(reloader, StatReload):
if isinstance(reloader, WatchFilesReload):
touch_soon(*files)
else:
assert not next(reloader)
sleep(0.1)
for file in files:
file.touch()
else:
touch_soon(*files)
return next(reloader)

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

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

Expand All @@ -70,7 +71,7 @@ def test_reload_when_python_file_is_changed(self, touch_soon) -> None:

reloader.shutdown()

@pytest.mark.parametrize("reloader_class", [StatReload, WatchFilesReload])
@pytest.mark.parametrize("reloader_class", [StatReload, WatchGodReload, WatchFilesReload])
def test_should_reload_when_python_file_in_subdir_is_changed(
self, touch_soon
) -> None:
Expand All @@ -84,7 +85,7 @@ def test_should_reload_when_python_file_in_subdir_is_changed(

reloader.shutdown()

@pytest.mark.parametrize("reloader_class", [WatchFilesReload])
@pytest.mark.parametrize("reloader_class", [WatchFilesReload, WatchGodReload])
def test_should_not_reload_when_python_file_in_excluded_subdir_is_changed(
self, touch_soon
) -> None:
Expand Down Expand Up @@ -121,7 +122,7 @@ def test_reload_when_pattern_matched_file_is_changed(

reloader.shutdown()

@pytest.mark.parametrize("reloader_class", [WatchFilesReload])
@pytest.mark.parametrize("reloader_class", [WatchFilesReload, WatchGodReload])
def test_should_not_reload_when_exclude_pattern_match_file_is_changed(
self, touch_soon
) -> None:
Expand All @@ -144,7 +145,7 @@ def test_should_not_reload_when_exclude_pattern_match_file_is_changed(

reloader.shutdown()

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

Expand All @@ -156,7 +157,7 @@ def test_should_not_reload_when_dot_file_is_changed(self, touch_soon) -> None:

reloader.shutdown()

@pytest.mark.parametrize("reloader_class", [StatReload, WatchFilesReload])
@pytest.mark.parametrize("reloader_class", [StatReload, WatchGodReload, WatchFilesReload])
def test_should_reload_when_directories_have_same_prefix(self, touch_soon) -> None:
app_dir = self.reload_path / "app"
app_file = app_dir / "src" / "main.py"
Expand All @@ -176,7 +177,7 @@ def test_should_reload_when_directories_have_same_prefix(self, touch_soon) -> No

reloader.shutdown()

@pytest.mark.parametrize("reloader_class", [StatReload, WatchFilesReload])
@pytest.mark.parametrize("reloader_class", [StatReload, WatchGodReload, WatchFilesReload])
def test_should_not_reload_when_only_subdirectory_is_watched(
self, touch_soon
) -> None:
Expand All @@ -198,7 +199,7 @@ def test_should_not_reload_when_only_subdirectory_is_watched(

reloader.shutdown()

@pytest.mark.parametrize("reloader_class", [WatchFilesReload])
@pytest.mark.parametrize("reloader_class", [WatchFilesReload, WatchGodReload])
def test_override_defaults(self, touch_soon) -> None:
dotted_file = self.reload_path / ".dotted"
dotted_dir_file = self.reload_path / ".dotted_dir" / "file.txt"
Expand Down
7 changes: 6 additions & 1 deletion uvicorn/supervisors/__init__.py
Expand Up @@ -11,6 +11,11 @@
WatchFilesReload as ChangeReload,
)
except ImportError: # pragma: no cover
from uvicorn.supervisors.statreload import StatReload as ChangeReload
try:
from uvicorn.supervisors.watchgodreload import (
WatchGodReload as ChangeReload,
)
except ImportError:
from uvicorn.supervisors.statreload import StatReload as ChangeReload

__all__ = ["Multiprocess", "ChangeReload"]
2 changes: 1 addition & 1 deletion uvicorn/supervisors/watchfilesreload.py
Expand Up @@ -66,7 +66,7 @@ def __init__(
sockets: List[socket],
) -> None:
super().__init__(config, target, sockets)
self.reloader_name = "WatchFilesReload"
self.reloader_name = "WatchFiles"
self.reload_dirs = []
for directory in config.reload_dirs:
if Path.cwd() not in directory.parents:
Expand Down
150 changes: 150 additions & 0 deletions uvicorn/supervisors/watchgodreload.py
@@ -0,0 +1,150 @@
import logging
from pathlib import Path
from socket import socket
from typing import TYPE_CHECKING, Callable, Dict, List, Optional

from watchgod import DefaultWatcher

from uvicorn.config import Config
from uvicorn.supervisors.basereload import BaseReload

logger = logging.getLogger("uvicorn.error")

if TYPE_CHECKING: # pragma: no cover
import os

DirEntry = os.DirEntry[str]


class CustomWatcher(DefaultWatcher):
def __init__(self, root_path: Path, config: Config):
default_includes = ["*.py"]
self.includes = [
default
for default in default_includes
if default not in config.reload_excludes
]
self.includes.extend(config.reload_includes)
self.includes = list(set(self.includes))

default_excludes = [".*", ".py[cod]", ".sw.*", "~*"]
self.excludes = [
default
for default in default_excludes
if default not in config.reload_includes
]
self.excludes.extend(config.reload_excludes)
self.excludes = list(set(self.excludes))

self.watched_dirs: Dict[str, bool] = {}
self.watched_files: Dict[str, bool] = {}
self.dirs_includes = set(config.reload_dirs)
self.dirs_excludes = set(config.reload_dirs_excludes)
self.resolved_root = root_path
super().__init__(str(root_path))

def should_watch_file(self, entry: "DirEntry") -> bool:
cached_result = self.watched_files.get(entry.path)
if cached_result is not None:
return cached_result

entry_path = Path(entry)

# cwd is not verified through should_watch_dir, so we need to verify here
if entry_path.parent == Path.cwd() and not Path.cwd() in self.dirs_includes:
self.watched_files[entry.path] = False
return False
for include_pattern in self.includes:
if entry_path.match(include_pattern):
for exclude_pattern in self.excludes:
if entry_path.match(exclude_pattern):
self.watched_files[entry.path] = False
return False
self.watched_files[entry.path] = True
return True
self.watched_files[entry.path] = False
return False

def should_watch_dir(self, entry: "DirEntry") -> bool:
cached_result = self.watched_dirs.get(entry.path)
if cached_result is not None:
return cached_result

entry_path = Path(entry)

if entry_path in self.dirs_excludes:
self.watched_dirs[entry.path] = False
return False

for exclude_pattern in self.excludes:
if entry_path.match(exclude_pattern):
is_watched = False
if entry_path in self.dirs_includes:
is_watched = True

for directory in self.dirs_includes:
if directory in entry_path.parents:
is_watched = True

if is_watched:
logger.debug(
"WatchGodReload detected a new excluded dir '%s' in '%s'; "
"Adding to exclude list.",
entry_path.relative_to(self.resolved_root),
str(self.resolved_root),
)
self.watched_dirs[entry.path] = False
self.dirs_excludes.add(entry_path)
return False

if entry_path in self.dirs_includes:
self.watched_dirs[entry.path] = True
return True

for directory in self.dirs_includes:
if directory in entry_path.parents:
self.watched_dirs[entry.path] = True
return True

for include_pattern in self.includes:
if entry_path.match(include_pattern):
logger.info(
"WatchGodReload detected a new reload dir '%s' in '%s'; "
"Adding to watch list.",
str(entry_path.relative_to(self.resolved_root)),
str(self.resolved_root),
)
self.dirs_includes.add(entry_path)
self.watched_dirs[entry.path] = True
return True

self.watched_dirs[entry.path] = False
return False


class WatchGodReload(BaseReload):
def __init__(
self,
config: Config,
target: Callable[[Optional[List[socket]]], None],
sockets: List[socket],
) -> None:
super().__init__(config, target, sockets)
self.reloader_name = "WatchGod"
self.watchers = []
reload_dirs = []
for directory in config.reload_dirs:
if Path.cwd() not in directory.parents:
reload_dirs.append(directory)
if Path.cwd() not in reload_dirs:
reload_dirs.append(Path.cwd())
for w in reload_dirs:
self.watchers.append(CustomWatcher(w.resolve(), self.config))

def should_restart(self) -> Optional[List[Path]]:
for watcher in self.watchers:
change = watcher.check()
if change != set():
return list({Path(c[1]) for c in change})

return None

0 comments on commit 2a13f22

Please sign in to comment.