diff options
author | Teymour Aldridge <teymour.aldridge@icloud.com> | 2020-06-23 09:34:50 +0100 |
---|---|---|
committer | David Lord <davidism@gmail.com> | 2020-10-28 11:41:09 -0700 |
commit | 14719cbacd86b05a12242d2ba585245a0fcbe7fe (patch) | |
tree | 145fe78b9d7265b7155f27b86207ee26d2ee40da | |
parent | 44583678eb166aa5dfdcbb07087879ac78705e3c (diff) | |
download | werkzeug-14719cbacd86b05a12242d2ba585245a0fcbe7fe.tar.gz |
exclude paths from reloader
Co-authored-by: David Lord <davidism@gmail.com>
-rw-r--r-- | CHANGES.rst | 2 | ||||
-rw-r--r-- | src/werkzeug/_reloader.py | 54 | ||||
-rw-r--r-- | src/werkzeug/serving.py | 15 | ||||
-rw-r--r-- | tests/test_serving.py | 12 |
4 files changed, 67 insertions, 16 deletions
diff --git a/CHANGES.rst b/CHANGES.rst index 386e9b10..81606083 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -82,6 +82,8 @@ Unreleased most user code. It will also watch all Python files under directories given in ``extra_files``. :pr:`1945` - The reloader ignores ``__pycache__`` directories again. :pr:`1945` +- ``run_simple`` takes ``exclude_patterns`` a list of ``fnmatch`` + patterns that will not be scanned by the reloader. :issue:`1333` Version 1.0.2 diff --git a/src/werkzeug/_reloader.py b/src/werkzeug/_reloader.py index 567c5a10..c3a7b098 100644 --- a/src/werkzeug/_reloader.py +++ b/src/werkzeug/_reloader.py @@ -1,11 +1,11 @@ +import fnmatch import os import subprocess import sys import threading import time +import typing as t from itertools import chain -from typing import Any -from typing import Optional from ._internal import _log @@ -42,7 +42,12 @@ def _iter_module_paths(): yield name -def _find_stat_paths(extra_files): +def _remove_by_pattern(paths: t.Set[str], exclude_patterns: t.Set[str]) -> None: + for pattern in exclude_patterns: + paths.difference_update(fnmatch.filter(paths, pattern)) + + +def _find_stat_paths(extra_files, exclude_patterns): """Find paths for the stat reloader to watch. Returns imported module files, Python files under non-system paths. Extra files and Python files under extra directories can also be scanned. @@ -51,12 +56,14 @@ def _find_stat_paths(extra_files): such as a project root or ``sys.path.insert``, should be the paths of interest to the user anyway. """ + paths = set() + for path in chain(list(sys.path), extra_files): path = os.path.abspath(path) if os.path.isfile(path): # zip file on sys.path, or extra file - yield path + paths.add(path) for root, dirs, files in os.walk(path): # Ignore system prefixes for efficience. Don't scan @@ -73,12 +80,14 @@ def _find_stat_paths(extra_files): for name in files: if name.endswith((".py", ".pyc")): - yield os.path.join(root, name) + paths.add(os.path.join(root, name)) - yield from _iter_module_paths() + paths.update(_iter_module_paths()) + _remove_by_pattern(paths, exclude_patterns) + return paths -def _find_watchdog_paths(extra_files): +def _find_watchdog_paths(extra_files, exclude_patterns): """Find paths for the stat reloader to watch. Looks at the same sources as the stat reloader, but watches everything under directories instead of individual files. @@ -96,6 +105,7 @@ def _find_watchdog_paths(extra_files): for name in _iter_module_paths(): dirs.add(os.path.dirname(name)) + _remove_by_pattern(dirs, exclude_patterns) return _find_common_roots(dirs) @@ -186,8 +196,14 @@ def _get_args_for_reloading(): class ReloaderLoop: name = "" - def __init__(self, extra_files=None, interval=1): + def __init__( + self, + extra_files: t.Optional[t.Iterable[str]] = None, + exclude_patterns: t.Optional[t.Iterable[str]] = None, + interval: t.Union[int, float] = 1, + ): self.extra_files = {os.path.abspath(x) for x in extra_files or ()} + self.exclude_patterns = set(exclude_patterns or ()) self.interval = interval def __enter__(self): @@ -246,7 +262,7 @@ class StatReloaderLoop(ReloaderLoop): return super().__enter__() def run_step(self): - for name in chain(_find_stat_paths(self.extra_files)): + for name in chain(_find_stat_paths(self.extra_files, self.exclude_patterns)): try: mtime = os.stat(name).st_mtime except OSError: @@ -289,7 +305,12 @@ class WatchdogReloaderLoop(ReloaderLoop): extra_patterns = [p for p in self.extra_files if not os.path.isdir(p)] self.event_handler = EventHandler( patterns=["*.py", "*.pyc", "*.zip", *extra_patterns], - ignore_patterns=["*/__pycache__/*", "*/.git/*", "*/.hg/*"], + ignore_patterns=[ + "*/__pycache__/*", + "*/.git/*", + "*/.hg/*", + *self.exclude_patterns, + ], ) self.should_reload = False @@ -319,7 +340,7 @@ class WatchdogReloaderLoop(ReloaderLoop): def run_step(self): to_delete = set(self.watches) - for path in _find_watchdog_paths(self.extra_files): + for path in _find_watchdog_paths(self.extra_files, self.exclude_patterns): if path not in self.watches: try: self.watches[path] = self.observer.schedule( @@ -340,7 +361,7 @@ class WatchdogReloaderLoop(ReloaderLoop): self.observer.unschedule(watch) -reloader_loops: Any = { +reloader_loops: t.Dict[str, t.Type[ReloaderLoop]] = { "stat": StatReloaderLoop, "watchdog": WatchdogReloaderLoop, } @@ -374,15 +395,18 @@ def ensure_echo_on(): def run_with_reloader( main_func, - extra_files: Optional[Any] = None, - interval: float = 1, + extra_files: t.Optional[t.Iterable[str]] = None, + exclude_patterns: t.Optional[t.Iterable[str]] = None, + interval: t.Union[int, float] = 1, reloader_type: str = "auto", ): """Run the given function in an independent Python interpreter.""" import signal - reloader = reloader_loops[reloader_type](extra_files, interval) signal.signal(signal.SIGTERM, lambda *args: sys.exit(0)) + reloader = reloader_loops[reloader_type]( + extra_files=extra_files, exclude_patterns=exclude_patterns, interval=interval + ) try: if os.environ.get("WERKZEUG_RUN_MAIN") == "true": diff --git a/src/werkzeug/serving.py b/src/werkzeug/serving.py index 6d89ac43..451e663b 100644 --- a/src/werkzeug/serving.py +++ b/src/werkzeug/serving.py @@ -763,6 +763,7 @@ def run_simple( use_debugger=False, use_evalex=True, extra_files=None, + exclude_patterns=None, reloader_interval=1, reloader_type="auto", threaded=False, @@ -779,6 +780,9 @@ def run_simple( python -m werkzeug.serving --help + .. versionchanged:: 2.0 + Added ``exclude_patterns`` parameter. + .. versionadded:: 0.5 `static_files` was added to simplify serving of static files as well as `passthrough_errors`. @@ -814,6 +818,9 @@ def run_simple( :param extra_files: a list of files the reloader should watch additionally to the modules. For example configuration files. + :param exclude_patterns: List of :mod:`fnmatch` patterns to ignore + when running the reloader. For example, ignore cache files that + shouldn't reload when updated. :param reloader_interval: the interval for the reloader in seconds. :param reloader_type: the type of reloader to use. The default is auto detection. Valid values are ``'stat'`` and @@ -926,7 +933,13 @@ def run_simple( from ._reloader import run_with_reloader - run_with_reloader(inner, extra_files, reloader_interval, reloader_type) + run_with_reloader( + inner, + extra_files=extra_files, + exclude_patterns=exclude_patterns, + interval=reloader_interval, + reloader_type=reloader_type, + ) else: inner() diff --git a/tests/test_serving.py b/tests/test_serving.py index 71fe3e52..8be75a45 100644 --- a/tests/test_serving.py +++ b/tests/test_serving.py @@ -10,6 +10,8 @@ from pathlib import Path import pytest from werkzeug import run_simple +from werkzeug._reloader import _find_stat_paths +from werkzeug._reloader import _find_watchdog_paths from werkzeug._reloader import _get_args_for_reloading from werkzeug.datastructures import FileStorage from werkzeug.serving import make_ssl_devcert @@ -105,6 +107,16 @@ def test_windows_get_args_for_reloading(monkeypatch, tmp_path): assert rv == argv +@pytest.mark.parametrize("find", [_find_stat_paths, _find_watchdog_paths]) +def test_exclude_patterns(find): + # Imported paths under sys.prefix will be included by default. + paths = find(set(), set()) + assert any(p.startswith(sys.prefix) for p in paths) + # Those paths should be excluded due to the pattern. + paths = find(set(), {f"{sys.prefix}/*"}) + assert not any(p.startswith(sys.prefix) for p in paths) + + def test_wrong_protocol(standard_app): """An HTTPS request to an HTTP server doesn't show a traceback. https://github.com/pallets/werkzeug/pull/838 |