aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/uvicorn/supervisors
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/uvicorn/supervisors')
-rw-r--r--.venv/lib/python3.12/site-packages/uvicorn/supervisors/__init__.py21
-rw-r--r--.venv/lib/python3.12/site-packages/uvicorn/supervisors/basereload.py127
-rw-r--r--.venv/lib/python3.12/site-packages/uvicorn/supervisors/multiprocess.py76
-rw-r--r--.venv/lib/python3.12/site-packages/uvicorn/supervisors/statreload.py55
-rw-r--r--.venv/lib/python3.12/site-packages/uvicorn/supervisors/watchfilesreload.py96
-rw-r--r--.venv/lib/python3.12/site-packages/uvicorn/supervisors/watchgodreload.py163
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