diff options
Diffstat (limited to '.venv/lib/python3.12/site-packages/uvicorn/supervisors')
6 files changed, 538 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/uvicorn/supervisors/__init__.py b/.venv/lib/python3.12/site-packages/uvicorn/supervisors/__init__.py new file mode 100644 index 00000000..deaf12ed --- /dev/null +++ b/.venv/lib/python3.12/site-packages/uvicorn/supervisors/__init__.py @@ -0,0 +1,21 @@ +from typing import TYPE_CHECKING, Type + +from uvicorn.supervisors.basereload import BaseReload +from uvicorn.supervisors.multiprocess import Multiprocess + +if TYPE_CHECKING: + ChangeReload: Type[BaseReload] +else: + try: + from uvicorn.supervisors.watchfilesreload import ( + WatchFilesReload as ChangeReload, + ) + except ImportError: # pragma: no cover + try: + from uvicorn.supervisors.watchgodreload import ( + WatchGodReload as ChangeReload, + ) + except ImportError: + from uvicorn.supervisors.statreload import StatReload as ChangeReload + +__all__ = ["Multiprocess", "ChangeReload"] diff --git a/.venv/lib/python3.12/site-packages/uvicorn/supervisors/basereload.py b/.venv/lib/python3.12/site-packages/uvicorn/supervisors/basereload.py new file mode 100644 index 00000000..6e2e0c35 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/uvicorn/supervisors/basereload.py @@ -0,0 +1,127 @@ +from __future__ import annotations + +import logging +import os +import signal +import sys +import threading +from pathlib import Path +from socket import socket +from types import FrameType +from typing import Callable, Iterator + +import click + +from uvicorn._subprocess import get_subprocess +from uvicorn.config import Config + +HANDLED_SIGNALS = ( + signal.SIGINT, # Unix signal 2. Sent by Ctrl+C. + signal.SIGTERM, # Unix signal 15. Sent by `kill <pid>`. +) + +logger = logging.getLogger("uvicorn.error") + + +class BaseReload: + def __init__( + self, + config: Config, + target: Callable[[list[socket] | None], None], + sockets: list[socket], + ) -> None: + self.config = config + self.target = target + self.sockets = sockets + self.should_exit = threading.Event() + self.pid = os.getpid() + self.is_restarting = False + self.reloader_name: str | None = None + + def signal_handler(self, sig: int, frame: FrameType | None) -> None: + """ + A signal handler that is registered with the parent process. + """ + if sys.platform == "win32" and self.is_restarting: + self.is_restarting = False # pragma: py-not-win32 + else: + self.should_exit.set() # pragma: py-win32 + + def run(self) -> None: + self.startup() + for changes in self: + if changes: + logger.warning( + "%s detected changes in %s. Reloading...", + self.reloader_name, + ", ".join(map(_display_path, changes)), + ) + self.restart() + + self.shutdown() + + def pause(self) -> None: + if self.should_exit.wait(self.config.reload_delay): + raise StopIteration() + + def __iter__(self) -> Iterator[list[Path] | None]: + return self + + def __next__(self) -> list[Path] | None: + return self.should_restart() + + def startup(self) -> None: + message = f"Started reloader process [{self.pid}] using {self.reloader_name}" + color_message = "Started reloader process [{}] using {}".format( + click.style(str(self.pid), fg="cyan", bold=True), + click.style(str(self.reloader_name), fg="cyan", bold=True), + ) + logger.info(message, extra={"color_message": color_message}) + + for sig in HANDLED_SIGNALS: + signal.signal(sig, self.signal_handler) + + self.process = get_subprocess( + config=self.config, target=self.target, sockets=self.sockets + ) + self.process.start() + + def restart(self) -> None: + if sys.platform == "win32": # pragma: py-not-win32 + self.is_restarting = True + assert self.process.pid is not None + os.kill(self.process.pid, signal.CTRL_C_EVENT) + else: # pragma: py-win32 + self.process.terminate() + self.process.join() + + self.process = get_subprocess( + config=self.config, target=self.target, sockets=self.sockets + ) + self.process.start() + + def shutdown(self) -> None: + if sys.platform == "win32": + self.should_exit.set() # pragma: py-not-win32 + else: + self.process.terminate() # pragma: py-win32 + self.process.join() + + for sock in self.sockets: + sock.close() + + message = "Stopping reloader process [{}]".format(str(self.pid)) + color_message = "Stopping reloader process [{}]".format( + click.style(str(self.pid), fg="cyan", bold=True) + ) + logger.info(message, extra={"color_message": color_message}) + + def should_restart(self) -> list[Path] | None: + raise NotImplementedError("Reload strategies should override should_restart()") + + +def _display_path(path: Path) -> str: + try: + return f"'{path.relative_to(Path.cwd())}'" + except ValueError: + return f"'{path}'" diff --git a/.venv/lib/python3.12/site-packages/uvicorn/supervisors/multiprocess.py b/.venv/lib/python3.12/site-packages/uvicorn/supervisors/multiprocess.py new file mode 100644 index 00000000..153b3d65 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/uvicorn/supervisors/multiprocess.py @@ -0,0 +1,76 @@ +from __future__ import annotations + +import logging +import os +import signal +import threading +from multiprocessing.context import SpawnProcess +from socket import socket +from types import FrameType +from typing import Callable + +import click + +from uvicorn._subprocess import get_subprocess +from uvicorn.config import Config + +HANDLED_SIGNALS = ( + signal.SIGINT, # Unix signal 2. Sent by Ctrl+C. + signal.SIGTERM, # Unix signal 15. Sent by `kill <pid>`. +) + +logger = logging.getLogger("uvicorn.error") + + +class Multiprocess: + def __init__( + self, + config: Config, + target: Callable[[list[socket] | None], None], + sockets: list[socket], + ) -> None: + self.config = config + self.target = target + self.sockets = sockets + self.processes: list[SpawnProcess] = [] + self.should_exit = threading.Event() + self.pid = os.getpid() + + def signal_handler(self, sig: int, frame: FrameType | None) -> None: + """ + A signal handler that is registered with the parent process. + """ + self.should_exit.set() + + def run(self) -> None: + self.startup() + self.should_exit.wait() + self.shutdown() + + def startup(self) -> None: + message = "Started parent process [{}]".format(str(self.pid)) + color_message = "Started parent process [{}]".format( + click.style(str(self.pid), fg="cyan", bold=True) + ) + logger.info(message, extra={"color_message": color_message}) + + for sig in HANDLED_SIGNALS: + signal.signal(sig, self.signal_handler) + + for _idx in range(self.config.workers): + process = get_subprocess( + config=self.config, target=self.target, sockets=self.sockets + ) + process.start() + self.processes.append(process) + + def shutdown(self) -> None: + for process in self.processes: + process.terminate() + process.join() + + message = "Stopping parent process [{}]".format(str(self.pid)) + color_message = "Stopping parent process [{}]".format( + click.style(str(self.pid), fg="cyan", bold=True) + ) + logger.info(message, extra={"color_message": color_message}) diff --git a/.venv/lib/python3.12/site-packages/uvicorn/supervisors/statreload.py b/.venv/lib/python3.12/site-packages/uvicorn/supervisors/statreload.py new file mode 100644 index 00000000..2e25dd4a --- /dev/null +++ b/.venv/lib/python3.12/site-packages/uvicorn/supervisors/statreload.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +import logging +from pathlib import Path +from socket import socket +from typing import Callable, Iterator + +from uvicorn.config import Config +from uvicorn.supervisors.basereload import BaseReload + +logger = logging.getLogger("uvicorn.error") + + +class StatReload(BaseReload): + def __init__( + self, + config: Config, + target: Callable[[list[socket] | None], None], + sockets: list[socket], + ) -> None: + super().__init__(config, target, sockets) + self.reloader_name = "StatReload" + self.mtimes: dict[Path, float] = {} + + if config.reload_excludes or config.reload_includes: + logger.warning( + "--reload-include and --reload-exclude have no effect unless " + "watchfiles is installed." + ) + + def should_restart(self) -> list[Path] | None: + self.pause() + + for file in self.iter_py_files(): + try: + mtime = file.stat().st_mtime + except OSError: # pragma: nocover + continue + + old_time = self.mtimes.get(file) + if old_time is None: + self.mtimes[file] = mtime + continue + elif mtime > old_time: + return [file] + return None + + def restart(self) -> None: + self.mtimes = {} + return super().restart() + + def iter_py_files(self) -> Iterator[Path]: + for reload_dir in self.config.reload_dirs: + for path in list(reload_dir.rglob("*.py")): + yield path.resolve() diff --git a/.venv/lib/python3.12/site-packages/uvicorn/supervisors/watchfilesreload.py b/.venv/lib/python3.12/site-packages/uvicorn/supervisors/watchfilesreload.py new file mode 100644 index 00000000..e1cb311f --- /dev/null +++ b/.venv/lib/python3.12/site-packages/uvicorn/supervisors/watchfilesreload.py @@ -0,0 +1,96 @@ +from __future__ import annotations + +from pathlib import Path +from socket import socket +from typing import Callable + +from watchfiles import watch + +from uvicorn.config import Config +from uvicorn.supervisors.basereload import BaseReload + + +class FileFilter: + def __init__(self, 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.exclude_dirs = [] + for e in config.reload_excludes: + p = Path(e) + try: + is_dir = p.is_dir() + except OSError: # pragma: no cover + # gets raised on Windows for values like "*.py" + is_dir = False + + if is_dir: + self.exclude_dirs.append(p) + else: + self.excludes.append(e) + self.excludes = list(set(self.excludes)) + + def __call__(self, path: Path) -> bool: + for include_pattern in self.includes: + if path.match(include_pattern): + if str(path).endswith(include_pattern): + return True + + for exclude_dir in self.exclude_dirs: + if exclude_dir in path.parents: + return False + + for exclude_pattern in self.excludes: + if path.match(exclude_pattern): + return False + + return True + return False + + +class WatchFilesReload(BaseReload): + def __init__( + self, + config: Config, + target: Callable[[list[socket] | None], None], + sockets: list[socket], + ) -> None: + super().__init__(config, target, sockets) + self.reloader_name = "WatchFiles" + self.reload_dirs = [] + for directory in config.reload_dirs: + if Path.cwd() not in directory.parents: + self.reload_dirs.append(directory) + if Path.cwd() not in self.reload_dirs: + self.reload_dirs.append(Path.cwd()) + + self.watch_filter = FileFilter(config) + self.watcher = watch( + *self.reload_dirs, + watch_filter=None, + stop_event=self.should_exit, + # using yield_on_timeout here mostly to make sure tests don't + # hang forever, won't affect the class's behavior + yield_on_timeout=True, + ) + + def should_restart(self) -> list[Path] | None: + self.pause() + + changes = next(self.watcher) + if changes: + unique_paths = {Path(c[1]) for c in changes} + return [p for p in unique_paths if self.watch_filter(p)] + return None diff --git a/.venv/lib/python3.12/site-packages/uvicorn/supervisors/watchgodreload.py b/.venv/lib/python3.12/site-packages/uvicorn/supervisors/watchgodreload.py new file mode 100644 index 00000000..987909fd --- /dev/null +++ b/.venv/lib/python3.12/site-packages/uvicorn/supervisors/watchgodreload.py @@ -0,0 +1,163 @@ +from __future__ import annotations + +import logging +import warnings +from pathlib import Path +from socket import socket +from typing import TYPE_CHECKING, Callable + +from watchgod import DefaultWatcher + +from uvicorn.config import Config +from uvicorn.supervisors.basereload import BaseReload + +if TYPE_CHECKING: + import os + + DirEntry = os.DirEntry[str] + +logger = logging.getLogger("uvicorn.error") + + +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 Path.cwd() not in self.dirs_includes: + self.watched_files[entry.path] = False + return False + for include_pattern in self.includes: + if str(entry_path).endswith(include_pattern): + self.watched_files[entry.path] = True + return True + 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[[list[socket] | None], None], + sockets: list[socket], + ) -> None: + warnings.warn( + '"watchgod" is deprecated, you should switch ' + "to watchfiles (`pip install watchfiles`).", + DeprecationWarning, + ) + 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) -> list[Path] | None: + self.pause() + + for watcher in self.watchers: + change = watcher.check() + if change != set(): + return list({Path(c[1]) for c in change}) + + return None |