summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTeymour Aldridge <teymour.aldridge@icloud.com>2020-06-23 09:34:50 +0100
committerDavid Lord <davidism@gmail.com>2020-10-28 11:41:09 -0700
commit14719cbacd86b05a12242d2ba585245a0fcbe7fe (patch)
tree145fe78b9d7265b7155f27b86207ee26d2ee40da
parent44583678eb166aa5dfdcbb07087879ac78705e3c (diff)
downloadwerkzeug-14719cbacd86b05a12242d2ba585245a0fcbe7fe.tar.gz
exclude paths from reloader
Co-authored-by: David Lord <davidism@gmail.com>
-rw-r--r--CHANGES.rst2
-rw-r--r--src/werkzeug/_reloader.py54
-rw-r--r--src/werkzeug/serving.py15
-rw-r--r--tests/test_serving.py12
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