Buckets:
ktongue/docker_container / simsite /venv /lib /python3.14 /site-packages /django /utils /autoreload.py
| import itertools | |
| import logging | |
| import os | |
| import signal | |
| import subprocess | |
| import sys | |
| import threading | |
| import time | |
| import traceback | |
| import weakref | |
| from collections import defaultdict | |
| from functools import lru_cache, wraps | |
| from pathlib import Path | |
| from types import ModuleType | |
| from zipimport import zipimporter | |
| import django | |
| from django.apps import apps | |
| from django.core.signals import request_finished | |
| from django.dispatch import Signal | |
| from django.utils.functional import cached_property | |
| from django.utils.version import get_version_tuple | |
| autoreload_started = Signal() | |
| file_changed = Signal() | |
| DJANGO_AUTORELOAD_ENV = "RUN_MAIN" | |
| logger = logging.getLogger("django.utils.autoreload") | |
| # If an error is raised while importing a file, it's not placed in sys.modules. | |
| # This means that any future modifications aren't caught. Keep a list of these | |
| # file paths to allow watching them in the future. | |
| _error_files = [] | |
| _exception = None | |
| try: | |
| import termios | |
| except ImportError: | |
| termios = None | |
| try: | |
| import pywatchman | |
| except ImportError: | |
| pywatchman = None | |
| def is_django_module(module): | |
| """Return True if the given module is nested under Django.""" | |
| return module.__name__.startswith("django.") | |
| def is_django_path(path): | |
| """Return True if the given file path is nested under Django.""" | |
| return Path(django.__file__).parent in Path(path).parents | |
| def check_errors(fn): | |
| def wrapper(*args, **kwargs): | |
| global _exception | |
| try: | |
| fn(*args, **kwargs) | |
| except Exception: | |
| _exception = sys.exc_info() | |
| et, ev, tb = _exception | |
| if getattr(ev, "filename", None) is None: | |
| # get the filename from the last item in the stack | |
| filename = traceback.extract_tb(tb)[-1][0] | |
| else: | |
| filename = ev.filename | |
| if filename not in _error_files: | |
| _error_files.append(filename) | |
| raise | |
| return wrapper | |
| def raise_last_exception(): | |
| if _exception is not None: | |
| raise _exception[1] | |
| def ensure_echo_on(): | |
| """ | |
| Ensure that echo mode is enabled. Some tools such as PDB disable | |
| it which causes usability issues after reload. | |
| """ | |
| if not termios or not sys.stdin.isatty(): | |
| return | |
| attr_list = termios.tcgetattr(sys.stdin) | |
| if not attr_list[3] & termios.ECHO: | |
| attr_list[3] |= termios.ECHO | |
| if hasattr(signal, "SIGTTOU"): | |
| old_handler = signal.signal(signal.SIGTTOU, signal.SIG_IGN) | |
| else: | |
| old_handler = None | |
| termios.tcsetattr(sys.stdin, termios.TCSANOW, attr_list) | |
| if old_handler is not None: | |
| signal.signal(signal.SIGTTOU, old_handler) | |
| def iter_all_python_module_files(): | |
| # This is a hot path during reloading. Create a stable sorted list of | |
| # modules based on the module name and pass it to iter_modules_and_files(). | |
| # This ensures cached results are returned in the usual case that modules | |
| # aren't loaded on the fly. | |
| keys = sorted(sys.modules) | |
| modules = tuple( | |
| m | |
| for m in map(sys.modules.__getitem__, keys) | |
| if not isinstance(m, weakref.ProxyTypes) | |
| ) | |
| return iter_modules_and_files(modules, frozenset(_error_files)) | |
| def iter_modules_and_files(modules, extra_files): | |
| """Iterate through all modules needed to be watched.""" | |
| sys_file_paths = [] | |
| for module in modules: | |
| # During debugging (with PyDev) the 'typing.io' and 'typing.re' objects | |
| # are added to sys.modules, however they are types not modules and so | |
| # cause issues here. | |
| if not isinstance(module, ModuleType): | |
| continue | |
| if module.__name__ in ("__main__", "__mp_main__"): | |
| # __main__ (usually manage.py) doesn't always have a __spec__ set. | |
| # Handle this by falling back to using __file__, resolved below. | |
| # See https://docs.python.org/reference/import.html#main-spec | |
| # __file__ may not exists, e.g. when running ipdb debugger. | |
| if hasattr(module, "__file__"): | |
| sys_file_paths.append(module.__file__) | |
| continue | |
| if getattr(module, "__spec__", None) is None: | |
| continue | |
| spec = module.__spec__ | |
| # Modules could be loaded from places without a concrete location. If | |
| # this is the case, skip them. | |
| if spec.has_location: | |
| origin = ( | |
| spec.loader.archive | |
| if isinstance(spec.loader, zipimporter) | |
| else spec.origin | |
| ) | |
| sys_file_paths.append(origin) | |
| results = set() | |
| for filename in itertools.chain(sys_file_paths, extra_files): | |
| if not filename: | |
| continue | |
| path = Path(filename) | |
| try: | |
| if not path.exists(): | |
| # The module could have been removed, don't fail loudly if this | |
| # is the case. | |
| continue | |
| except ValueError as e: | |
| # Network filesystems may return null bytes in file paths. | |
| logger.debug('"%s" raised when resolving path: "%s"', e, path) | |
| continue | |
| resolved_path = path.resolve().absolute() | |
| results.add(resolved_path) | |
| return frozenset(results) | |
| def common_roots(paths): | |
| """ | |
| Return a tuple of common roots that are shared between the given paths. | |
| File system watchers operate on directories and aren't cheap to create. | |
| Try to find the minimum set of directories to watch that encompass all of | |
| the files that need to be watched. | |
| """ | |
| # Inspired from Werkzeug: | |
| # https://github.com/pallets/werkzeug/blob/7477be2853df70a022d9613e765581b9411c3c39/werkzeug/_reloader.py | |
| # Create a sorted list of the path components, longest first. | |
| path_parts = sorted([x.parts for x in paths], key=len, reverse=True) | |
| tree = {} | |
| for chunks in path_parts: | |
| node = tree | |
| # Add each part of the path to the tree. | |
| for chunk in chunks: | |
| node = node.setdefault(chunk, {}) | |
| # Clear the last leaf in the tree. | |
| node.clear() | |
| # Turn the tree into a list of Path instances. | |
| def _walk(node, path): | |
| for prefix, child in node.items(): | |
| yield from _walk(child, [*path, prefix]) | |
| if not node: | |
| yield Path(*path) | |
| return tuple(_walk(tree, ())) | |
| def sys_path_directories(): | |
| """ | |
| Yield absolute directories from sys.path, ignoring entries that don't | |
| exist. | |
| """ | |
| for path in sys.path: | |
| path = Path(path) | |
| if not path.exists(): | |
| continue | |
| resolved_path = path.resolve().absolute() | |
| # If the path is a file (like a zip file), watch the parent directory. | |
| if resolved_path.is_file(): | |
| yield resolved_path.parent | |
| else: | |
| yield resolved_path | |
| def get_child_arguments(): | |
| """ | |
| Return the executable. This contains a workaround for Windows if the | |
| executable is reported to not have the .exe extension which can cause bugs | |
| on reloading. | |
| """ | |
| import __main__ | |
| py_script = Path(sys.argv[0]) | |
| exe_entrypoint = py_script.with_suffix(".exe") | |
| args = [sys.executable] + ["-W%s" % o for o in sys.warnoptions] | |
| if sys.implementation.name in ("cpython", "pypy"): | |
| args.extend( | |
| f"-X{key}" if value is True else f"-X{key}={value}" | |
| for key, value in sys._xoptions.items() | |
| ) | |
| # __spec__ is set when the server was started with the `-m` option, | |
| # see https://docs.python.org/3/reference/import.html#main-spec | |
| # __spec__ may not exist, e.g. when running in a Conda env. | |
| if getattr(__main__, "__spec__", None) is not None and not exe_entrypoint.exists(): | |
| spec = __main__.__spec__ | |
| if (spec.name == "__main__" or spec.name.endswith(".__main__")) and spec.parent: | |
| name = spec.parent | |
| else: | |
| name = spec.name | |
| args += ["-m", name] | |
| args += sys.argv[1:] | |
| elif not py_script.exists(): | |
| # sys.argv[0] may not exist for several reasons on Windows. | |
| # It may exist with a .exe extension or have a -script.py suffix. | |
| if exe_entrypoint.exists(): | |
| # Should be executed directly, ignoring sys.executable. | |
| return [exe_entrypoint, *sys.argv[1:]] | |
| script_entrypoint = py_script.with_name("%s-script.py" % py_script.name) | |
| if script_entrypoint.exists(): | |
| # Should be executed as usual. | |
| return [*args, script_entrypoint, *sys.argv[1:]] | |
| raise RuntimeError("Script %s does not exist." % py_script) | |
| else: | |
| args += sys.argv | |
| return args | |
| def trigger_reload(filename): | |
| logger.info("%s changed, reloading.", filename) | |
| sys.exit(3) | |
| def restart_with_reloader(): | |
| new_environ = {**os.environ, DJANGO_AUTORELOAD_ENV: "true"} | |
| orig = getattr(sys, "orig_argv", ()) | |
| if any( | |
| (arg == "-u") | |
| or ( | |
| arg.startswith("-") | |
| and not arg.startswith(("--", "-X", "-W")) | |
| and len(arg) > 2 | |
| and arg[1:].isalpha() | |
| and "u" in arg | |
| ) | |
| for arg in orig[1:] | |
| ): | |
| new_environ.setdefault("PYTHONUNBUFFERED", "1") | |
| args = get_child_arguments() | |
| while True: | |
| p = subprocess.run(args, env=new_environ, close_fds=False) | |
| if p.returncode != 3: | |
| return p.returncode | |
| class BaseReloader: | |
| def __init__(self): | |
| self.extra_files = set() | |
| self.directory_globs = defaultdict(set) | |
| self._stop_condition = threading.Event() | |
| def watch_dir(self, path, glob): | |
| path = Path(path) | |
| try: | |
| path = path.absolute() | |
| except FileNotFoundError: | |
| logger.debug( | |
| "Unable to watch directory %s as it cannot be resolved.", | |
| path, | |
| exc_info=True, | |
| ) | |
| return | |
| logger.debug("Watching dir %s with glob %s.", path, glob) | |
| self.directory_globs[path].add(glob) | |
| def watched_files(self, include_globs=True): | |
| """ | |
| Yield all files that need to be watched, including module files and | |
| files within globs. | |
| """ | |
| yield from iter_all_python_module_files() | |
| yield from self.extra_files | |
| if include_globs: | |
| for directory, patterns in self.directory_globs.items(): | |
| for pattern in patterns: | |
| yield from directory.glob(pattern) | |
| def wait_for_apps_ready(self, app_reg, django_main_thread): | |
| """ | |
| Wait until Django reports that the apps have been loaded. If the given | |
| thread has terminated before the apps are ready, then a SyntaxError or | |
| other non-recoverable error has been raised. In that case, stop waiting | |
| for the apps_ready event and continue processing. | |
| Return True if the thread is alive and the ready event has been | |
| triggered, or False if the thread is terminated while waiting for the | |
| event. | |
| """ | |
| while django_main_thread.is_alive(): | |
| if app_reg.ready_event.wait(timeout=0.1): | |
| return True | |
| else: | |
| logger.debug("Main Django thread has terminated before apps are ready.") | |
| return False | |
| def run(self, django_main_thread): | |
| logger.debug("Waiting for apps ready_event.") | |
| self.wait_for_apps_ready(apps, django_main_thread) | |
| from django.urls import get_resolver | |
| # Prevent a race condition where URL modules aren't loaded when the | |
| # reloader starts by accessing the urlconf_module property. | |
| try: | |
| get_resolver().urlconf_module | |
| except Exception: | |
| # Loading the urlconf can result in errors during development. | |
| # If this occurs then swallow the error and continue. | |
| pass | |
| logger.debug("Apps ready_event triggered. Sending autoreload_started signal.") | |
| autoreload_started.send(sender=self) | |
| self.run_loop() | |
| def run_loop(self): | |
| ticker = self.tick() | |
| while not self.should_stop: | |
| try: | |
| next(ticker) | |
| except StopIteration: | |
| break | |
| self.stop() | |
| def tick(self): | |
| """ | |
| This generator is called in a loop from run_loop. It's important that | |
| the method takes care of pausing or otherwise waiting for a period of | |
| time. This split between run_loop() and tick() is to improve the | |
| testability of the reloader implementations by decoupling the work they | |
| do from the loop. | |
| """ | |
| raise NotImplementedError("subclasses must implement tick().") | |
| def check_availability(cls): | |
| raise NotImplementedError("subclasses must implement check_availability().") | |
| def notify_file_changed(self, path): | |
| results = file_changed.send(sender=self, file_path=path) | |
| logger.debug("%s notified as changed. Signal results: %s.", path, results) | |
| if not any(res[1] for res in results): | |
| trigger_reload(path) | |
| # These are primarily used for testing. | |
| def should_stop(self): | |
| return self._stop_condition.is_set() | |
| def stop(self): | |
| self._stop_condition.set() | |
| class StatReloader(BaseReloader): | |
| SLEEP_TIME = 1 # Check for changes once per second. | |
| def tick(self): | |
| mtimes = {} | |
| while True: | |
| for filepath, mtime in self.snapshot_files(): | |
| old_time = mtimes.get(filepath) | |
| mtimes[filepath] = mtime | |
| if old_time is None: | |
| logger.debug("File %s first seen with mtime %s", filepath, mtime) | |
| continue | |
| elif mtime > old_time: | |
| logger.debug( | |
| "File %s previous mtime: %s, current mtime: %s", | |
| filepath, | |
| old_time, | |
| mtime, | |
| ) | |
| self.notify_file_changed(filepath) | |
| time.sleep(self.SLEEP_TIME) | |
| yield | |
| def snapshot_files(self): | |
| # watched_files may produce duplicate paths if globs overlap. | |
| seen_files = set() | |
| for file in self.watched_files(): | |
| if file in seen_files: | |
| continue | |
| try: | |
| mtime = file.stat().st_mtime | |
| except OSError: | |
| # This is thrown when the file does not exist. | |
| continue | |
| seen_files.add(file) | |
| yield file, mtime | |
| def check_availability(cls): | |
| return True | |
| class WatchmanUnavailable(RuntimeError): | |
| pass | |
| class WatchmanReloader(BaseReloader): | |
| def __init__(self): | |
| self.roots = defaultdict(set) | |
| self.processed_request = threading.Event() | |
| self.client_timeout = int(os.environ.get("DJANGO_WATCHMAN_TIMEOUT", 5)) | |
| super().__init__() | |
| def client(self): | |
| return pywatchman.client(timeout=self.client_timeout) | |
| def _watch_root(self, root): | |
| # In practice this shouldn't occur, however, it's possible that a | |
| # directory that doesn't exist yet is being watched. If it's outside of | |
| # sys.path then this will end up a new root. How to handle this isn't | |
| # clear: Not adding the root will likely break when subscribing to the | |
| # changes, however, as this is currently an internal API, no files | |
| # will be being watched outside of sys.path. Fixing this by checking | |
| # inside watch_glob() and watch_dir() is expensive, instead this could | |
| # could fall back to the StatReloader if this case is detected? For | |
| # now, watching its parent, if possible, is sufficient. | |
| if not root.exists(): | |
| if not root.parent.exists(): | |
| logger.warning( | |
| "Unable to watch root dir %s as neither it or its parent exist.", | |
| root, | |
| ) | |
| return | |
| root = root.parent | |
| result = self.client.query("watch-project", str(root.absolute())) | |
| if "warning" in result: | |
| logger.warning("Watchman warning: %s", result["warning"]) | |
| logger.debug("Watchman watch-project result: %s", result) | |
| return result["watch"], result.get("relative_path") | |
| def _get_clock(self, root): | |
| return self.client.query("clock", root)["clock"] | |
| def _subscribe(self, directory, name, expression): | |
| root, rel_path = self._watch_root(directory) | |
| # Only receive notifications of files changing, filtering out other | |
| # types like special files: | |
| # https://facebook.github.io/watchman/docs/type | |
| only_files_expression = [ | |
| "allof", | |
| ["anyof", ["type", "f"], ["type", "l"]], | |
| expression, | |
| ] | |
| query = { | |
| "expression": only_files_expression, | |
| "fields": ["name"], | |
| "since": self._get_clock(root), | |
| "dedup_results": True, | |
| } | |
| if rel_path: | |
| query["relative_root"] = rel_path | |
| logger.debug( | |
| "Issuing watchman subscription %s, for root %s. Query: %s", | |
| name, | |
| root, | |
| query, | |
| ) | |
| self.client.query("subscribe", root, name, query) | |
| def _subscribe_dir(self, directory, filenames): | |
| if not directory.exists(): | |
| if not directory.parent.exists(): | |
| logger.warning( | |
| "Unable to watch directory %s as neither it or its parent exist.", | |
| directory, | |
| ) | |
| return | |
| prefix = "files-parent-%s" % directory.name | |
| filenames = ["%s/%s" % (directory.name, filename) for filename in filenames] | |
| directory = directory.parent | |
| expression = ["name", filenames, "wholename"] | |
| else: | |
| prefix = "files" | |
| expression = ["name", filenames] | |
| self._subscribe(directory, "%s:%s" % (prefix, directory), expression) | |
| def _watch_glob(self, directory, patterns): | |
| """ | |
| Watch a directory with a specific glob. If the directory doesn't yet | |
| exist, attempt to watch the parent directory and amend the patterns to | |
| include this. It's important this method isn't called more than one per | |
| directory when updating all subscriptions. Subsequent calls will | |
| overwrite the named subscription, so it must include all possible glob | |
| expressions. | |
| """ | |
| prefix = "glob" | |
| if not directory.exists(): | |
| if not directory.parent.exists(): | |
| logger.warning( | |
| "Unable to watch directory %s as neither it or its parent exist.", | |
| directory, | |
| ) | |
| return | |
| prefix = "glob-parent-%s" % directory.name | |
| patterns = ["%s/%s" % (directory.name, pattern) for pattern in patterns] | |
| directory = directory.parent | |
| expression = ["anyof"] | |
| for pattern in patterns: | |
| expression.append(["match", pattern, "wholename"]) | |
| self._subscribe(directory, "%s:%s" % (prefix, directory), expression) | |
| def watched_roots(self, watched_files): | |
| extra_directories = self.directory_globs.keys() | |
| watched_file_dirs = [f.parent for f in watched_files] | |
| sys_paths = list(sys_path_directories()) | |
| return frozenset((*extra_directories, *watched_file_dirs, *sys_paths)) | |
| def _update_watches(self): | |
| watched_files = list(self.watched_files(include_globs=False)) | |
| found_roots = common_roots(self.watched_roots(watched_files)) | |
| logger.debug("Watching %s files", len(watched_files)) | |
| logger.debug("Found common roots: %s", found_roots) | |
| # Setup initial roots for performance, shortest roots first. | |
| for root in sorted(found_roots): | |
| self._watch_root(root) | |
| for directory, patterns in self.directory_globs.items(): | |
| self._watch_glob(directory, patterns) | |
| # Group sorted watched_files by their parent directory. | |
| sorted_files = sorted(watched_files, key=lambda p: p.parent) | |
| for directory, group in itertools.groupby(sorted_files, key=lambda p: p.parent): | |
| # These paths need to be relative to the parent directory. | |
| self._subscribe_dir( | |
| directory, [str(p.relative_to(directory)) for p in group] | |
| ) | |
| def update_watches(self): | |
| try: | |
| self._update_watches() | |
| except Exception as ex: | |
| # If the service is still available, raise the original exception. | |
| if self.check_server_status(ex): | |
| raise | |
| def _check_subscription(self, sub): | |
| subscription = self.client.getSubscription(sub) | |
| if not subscription: | |
| return | |
| logger.debug("Watchman subscription %s has results.", sub) | |
| for result in subscription: | |
| # When using watch-project, it's not simple to get the relative | |
| # directory without storing some specific state. Store the full | |
| # path to the directory in the subscription name, prefixed by its | |
| # type (glob, files). | |
| root_directory = Path(result["subscription"].split(":", 1)[1]) | |
| logger.debug("Found root directory %s", root_directory) | |
| for file in result.get("files", []): | |
| self.notify_file_changed(root_directory / file) | |
| def request_processed(self, **kwargs): | |
| logger.debug("Request processed. Setting update_watches event.") | |
| self.processed_request.set() | |
| def tick(self): | |
| request_finished.connect(self.request_processed) | |
| self.update_watches() | |
| while True: | |
| if self.processed_request.is_set(): | |
| self.update_watches() | |
| self.processed_request.clear() | |
| try: | |
| self.client.receive() | |
| except pywatchman.SocketTimeout: | |
| pass | |
| except pywatchman.WatchmanError as ex: | |
| logger.debug("Watchman error: %s, checking server status.", ex) | |
| self.check_server_status(ex) | |
| else: | |
| for sub in list(self.client.subs.keys()): | |
| self._check_subscription(sub) | |
| yield | |
| # Protect against busy loops. | |
| time.sleep(0.1) | |
| def stop(self): | |
| self.client.close() | |
| super().stop() | |
| def check_server_status(self, inner_ex=None): | |
| """Return True if the server is available.""" | |
| try: | |
| self.client.query("version") | |
| except Exception: | |
| raise WatchmanUnavailable(str(inner_ex)) from inner_ex | |
| return True | |
| def check_availability(cls): | |
| if not pywatchman: | |
| raise WatchmanUnavailable("pywatchman not installed.") | |
| client = pywatchman.client(timeout=0.1) | |
| try: | |
| result = client.capabilityCheck() | |
| except Exception: | |
| # The service is down? | |
| raise WatchmanUnavailable("Cannot connect to the watchman service.") | |
| version = get_version_tuple(result["version"]) | |
| # Watchman 4.9 includes multiple improvements to watching project | |
| # directories as well as case insensitive filesystems. | |
| logger.debug("Watchman version %s", version) | |
| if version < (4, 9): | |
| raise WatchmanUnavailable("Watchman 4.9 or later is required.") | |
| def get_reloader(): | |
| """Return the most suitable reloader for this environment.""" | |
| try: | |
| WatchmanReloader.check_availability() | |
| except WatchmanUnavailable: | |
| return StatReloader() | |
| return WatchmanReloader() | |
| def start_django(reloader, main_func, *args, **kwargs): | |
| ensure_echo_on() | |
| main_func = check_errors(main_func) | |
| django_main_thread = threading.Thread( | |
| target=main_func, args=args, kwargs=kwargs, name="django-main-thread" | |
| ) | |
| django_main_thread.daemon = True | |
| django_main_thread.start() | |
| while not reloader.should_stop: | |
| reloader.run(django_main_thread) | |
| def run_with_reloader(main_func, *args, **kwargs): | |
| signal.signal(signal.SIGTERM, lambda *args: sys.exit(0)) | |
| try: | |
| if os.environ.get(DJANGO_AUTORELOAD_ENV) == "true": | |
| reloader = get_reloader() | |
| logger.info( | |
| "Watching for file changes with %s", reloader.__class__.__name__ | |
| ) | |
| start_django(reloader, main_func, *args, **kwargs) | |
| else: | |
| exit_code = restart_with_reloader() | |
| sys.exit(exit_code) | |
| except KeyboardInterrupt: | |
| pass | |
Xet Storage Details
- Size:
- 24.8 kB
- Xet hash:
- 7753b469ab672d8b2c51238167fe0f472b2d5d64e2a79cb645e9d763b2d00004
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.