diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml index 683c85bef..adddea75d 100644 --- a/.github/workflows/pre-commit.yaml +++ b/.github/workflows/pre-commit.yaml @@ -2,7 +2,7 @@ name: pre-commit on: pull_request: push: - branches: [main, '*.x'] + branches: [main, stable] jobs: main: runs-on: ubuntu-latest diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index ae7cc5a72..61c622140 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -9,8 +9,8 @@ jobs: outputs: hash: ${{ steps.hash.outputs.hash }} steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 with: python-version: '3.x' cache: pip @@ -23,7 +23,7 @@ jobs: - name: generate hash id: hash run: cd dist && echo "hash=$(sha256sum * | base64 -w0)" >> $GITHUB_OUTPUT - - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: path: ./dist provenance: @@ -64,10 +64,10 @@ jobs: id-token: write steps: - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 - - uses: pypa/gh-action-pypi-publish@ec4db0b4ddc65acdf4bff5fa45ac92d78b56bdf0 # v1.9.0 + - uses: pypa/gh-action-pypi-publish@f7600683efdcb7656dec5b29656edb7bc586e597 # v1.10.3 with: repository-url: https://test.pypi.org/legacy/ packages-dir: artifact/ - - uses: pypa/gh-action-pypi-publish@ec4db0b4ddc65acdf4bff5fa45ac92d78b56bdf0 # v1.9.0 + - uses: pypa/gh-action-pypi-publish@f7600683efdcb7656dec5b29656edb7bc586e597 # v1.10.3 with: packages-dir: artifact/ diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 4a755422f..8a9741235 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -1,18 +1,10 @@ name: Tests on: push: - branches: - - main - - '*.x' - paths-ignore: - - 'docs/**' - - '*.md' - - '*.rst' + branches: [main, stable] + paths-ignore: ['docs/**', '*.md', '*.rst'] pull_request: - paths-ignore: - - 'docs/**' - - '*.md' - - '*.rst' + paths-ignore: ['docs/**', '*.md', '*.rst'] jobs: tests: name: ${{ matrix.name || matrix.python }} @@ -31,8 +23,8 @@ jobs: - {python: '3.8'} - {name: PyPy, python: 'pypy-3.10', tox: pypy310} steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 with: python-version: ${{ matrix.python }} allow-prereleases: true @@ -43,14 +35,14 @@ jobs: typing: runs-on: ubuntu-latest steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 with: python-version: '3.x' cache: pip cache-dependency-path: requirements*/*.txt - name: cache mypy - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 with: path: ./.mypy_cache key: mypy|${{ hashFiles('pyproject.toml') }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index acd7bba88..6ad19aacd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,11 +1,11 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.6.2 + rev: v0.7.1 hooks: - id: ruff - id: ruff-format - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: check-merge-conflict - id: debug-statements diff --git a/CHANGES.rst b/CHANGES.rst index 33b441c79..13620bc75 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -11,6 +11,19 @@ Unreleased some typing issues on cache control. :issue:`2881` +Version 3.0.5 +------------- + +Released 2024-10-24 + +- The Watchdog reloader ignores file closed no write events. :issue:`2945` +- Logging works with client addresses containing an IPv6 scope :issue:`2952` +- Ignore invalid authorization parameters. :issue:`2955` +- Improve type annotation fore ``SharedDataMiddleware``. :issue:`2958` +- Compatibility with Python 3.13 when generating debugger pin and the current + UID does not have an associated name. :issue:`2957` + + Version 3.0.4 ------------- diff --git a/requirements/build.txt b/requirements/build.txt index 4b289ca7f..1b13b0552 100644 --- a/requirements/build.txt +++ b/requirements/build.txt @@ -1,12 +1,12 @@ # -# This file is autogenerated by pip-compile with Python 3.12 +# This file is autogenerated by pip-compile with Python 3.13 # by the following command: # # pip-compile build.in # -build==1.2.1 +build==1.2.2.post1 # via -r build.in packaging==24.1 # via build -pyproject-hooks==1.1.0 +pyproject-hooks==1.2.0 # via build diff --git a/requirements/dev.txt b/requirements/dev.txt index d75352c7b..4f6a735d4 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.12 +# This file is autogenerated by pip-compile with Python 3.13 # by the following command: # # pip-compile dev.in @@ -14,11 +14,11 @@ babel==2.16.0 # sphinx cachetools==5.5.0 # via tox -certifi==2024.7.4 +certifi==2024.8.30 # via # -r docs.txt # requests -cffi==1.17.0rc1 +cffi==1.17.1 # via # -r tests.txt # cryptography @@ -26,15 +26,15 @@ cfgv==3.4.0 # via pre-commit chardet==5.2.0 # via tox -charset-normalizer==3.3.2 +charset-normalizer==3.4.0 # via # -r docs.txt # requests colorama==0.4.6 # via tox -cryptography==43.0.0 +cryptography==43.0.3 # via -r tests.txt -distlib==0.3.8 +distlib==0.3.9 # via virtualenv docutils==0.21.2 # via @@ -42,13 +42,13 @@ docutils==0.21.2 # sphinx ephemeral-port-reserve==1.1.4 # via -r tests.txt -filelock==3.15.4 +filelock==3.16.1 # via # tox # virtualenv -identify==2.6.0 +identify==2.6.1 # via pre-commit -idna==3.8 +idna==3.10 # via # -r docs.txt # requests @@ -65,11 +65,11 @@ jinja2==3.1.4 # via # -r docs.txt # sphinx -markupsafe==2.1.5 +markupsafe==3.0.2 # via # -r docs.txt # jinja2 -mypy==1.11.1 +mypy==1.13.0 # via -r typing.txt mypy-extensions==1.0.0 # via @@ -90,9 +90,9 @@ packaging==24.1 # pytest # sphinx # tox -pallets-sphinx-themes==2.1.3 +pallets-sphinx-themes==2.3.0 # via -r docs.txt -platformdirs==4.2.2 +platformdirs==4.3.6 # via # tox # virtualenv @@ -102,9 +102,9 @@ pluggy==1.5.0 # -r typing.txt # pytest # tox -pre-commit==3.8.0 +pre-commit==4.0.1 # via -r dev.in -psutil==6.0.0 +psutil==6.1.0 # via # -r tests.txt # pytest-xprocess @@ -116,11 +116,11 @@ pygments==2.18.0 # via # -r docs.txt # sphinx -pyproject-api==1.7.1 +pyproject-api==1.8.0 # via tox -pyright==1.1.377 +pyright==1.1.386 # via -r typing.txt -pytest==8.3.2 +pytest==8.3.3 # via # -r tests.txt # -r typing.txt @@ -140,11 +140,16 @@ snowballstemmer==2.2.0 # via # -r docs.txt # sphinx -sphinx==8.0.2 +sphinx==8.1.3 # via # -r docs.txt # pallets-sphinx-themes + # sphinx-notfound-page # sphinxcontrib-log-cabinet +sphinx-notfound-page==1.0.4 + # via + # -r docs.txt + # pallets-sphinx-themes sphinxcontrib-applehelp==2.0.0 # via # -r docs.txt @@ -171,27 +176,28 @@ sphinxcontrib-serializinghtml==2.0.0 # via # -r docs.txt # sphinx -tox==4.18.0 +tox==4.23.2 # via -r dev.in types-contextvars==2.4.7.3 # via -r typing.txt types-dataclasses==0.6.6 # via -r typing.txt -types-setuptools==73.0.0.20240822 +types-setuptools==75.2.0.20241019 # via -r typing.txt typing-extensions==4.12.2 # via # -r typing.txt # mypy -urllib3==2.2.2 + # pyright +urllib3==2.2.3 # via # -r docs.txt # requests -virtualenv==20.26.3 +virtualenv==20.27.0 # via # pre-commit # tox -watchdog==4.0.2 +watchdog==5.0.3 # via # -r tests.txt # -r typing.txt diff --git a/requirements/docs.txt b/requirements/docs.txt index 454762892..1e3a54ebb 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.12 +# This file is autogenerated by pip-compile with Python 3.13 # by the following command: # # pip-compile docs.in @@ -8,25 +8,25 @@ alabaster==1.0.0 # via sphinx babel==2.16.0 # via sphinx -certifi==2024.7.4 +certifi==2024.8.30 # via requests -charset-normalizer==3.3.2 +charset-normalizer==3.4.0 # via requests docutils==0.21.2 # via sphinx -idna==3.8 +idna==3.10 # via requests imagesize==1.4.1 # via sphinx jinja2==3.1.4 # via sphinx -markupsafe==2.1.5 +markupsafe==3.0.2 # via jinja2 packaging==24.1 # via # pallets-sphinx-themes # sphinx -pallets-sphinx-themes==2.1.3 +pallets-sphinx-themes==2.3.0 # via -r docs.in pygments==2.18.0 # via sphinx @@ -34,11 +34,14 @@ requests==2.32.3 # via sphinx snowballstemmer==2.2.0 # via sphinx -sphinx==8.0.2 +sphinx==8.1.3 # via # -r docs.in # pallets-sphinx-themes + # sphinx-notfound-page # sphinxcontrib-log-cabinet +sphinx-notfound-page==1.0.4 + # via pallets-sphinx-themes sphinxcontrib-applehelp==2.0.0 # via sphinx sphinxcontrib-devhelp==2.0.0 @@ -53,5 +56,5 @@ sphinxcontrib-qthelp==2.0.0 # via sphinx sphinxcontrib-serializinghtml==2.0.0 # via sphinx -urllib3==2.2.2 +urllib3==2.2.3 # via requests diff --git a/requirements/tests.in b/requirements/tests.in index d6ea15c66..494b61022 100644 --- a/requirements/tests.in +++ b/requirements/tests.in @@ -1,9 +1,7 @@ pytest pytest-timeout -# pinned for python 3.8 support pytest-xprocess<1 cryptography watchdog ephemeral-port-reserve -# pin cffi 1.17.0 pre-release for python 3.13 support -cffi==1.17.0 +cffi diff --git a/requirements/tests.txt b/requirements/tests.txt index 170da4400..c4557c90f 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -1,14 +1,14 @@ # -# This file is autogenerated by pip-compile with Python 3.12 +# This file is autogenerated by pip-compile with Python 3.13 # by the following command: # # pip-compile tests.in # -cffi==1.17.0 +cffi==1.17.1 # via # -r tests.in # cryptography -cryptography==43.0.0 +cryptography==43.0.3 # via -r tests.in ephemeral-port-reserve==1.1.4 # via -r tests.in @@ -18,11 +18,11 @@ packaging==24.1 # via pytest pluggy==1.5.0 # via pytest -psutil==6.0.0 +psutil==6.1.0 # via pytest-xprocess pycparser==2.22 # via cffi -pytest==8.3.2 +pytest==8.3.3 # via # -r tests.in # pytest-timeout @@ -31,5 +31,5 @@ pytest-timeout==2.3.1 # via -r tests.in pytest-xprocess==0.23.0 # via -r tests.in -watchdog==4.0.2 +watchdog==5.0.3 # via -r tests.in diff --git a/requirements/tests38.txt b/requirements/tests38.txt new file mode 100644 index 000000000..10829d84c --- /dev/null +++ b/requirements/tests38.txt @@ -0,0 +1,39 @@ +# +# This file is autogenerated by pip-compile with Python 3.8 +# by the following command: +# +# pip-compile --output-file=tests38.txt tests.in +# +cffi==1.17.1 + # via + # -r tests.in + # cryptography +cryptography==43.0.3 + # via -r tests.in +ephemeral-port-reserve==1.1.4 + # via -r tests.in +exceptiongroup==1.2.2 + # via pytest +iniconfig==2.0.0 + # via pytest +packaging==24.1 + # via pytest +pluggy==1.5.0 + # via pytest +psutil==6.1.0 + # via pytest-xprocess +pycparser==2.22 + # via cffi +pytest==8.3.3 + # via + # -r tests.in + # pytest-timeout + # pytest-xprocess +pytest-timeout==2.3.1 + # via -r tests.in +pytest-xprocess==0.23.0 + # via -r tests.in +tomli==2.0.2 + # via pytest +watchdog==4.0.2 + # via -r tests.in diff --git a/requirements/typing.txt b/requirements/typing.txt index 37467d926..b90f838dd 100644 --- a/requirements/typing.txt +++ b/requirements/typing.txt @@ -1,12 +1,12 @@ # -# This file is autogenerated by pip-compile with Python 3.12 +# This file is autogenerated by pip-compile with Python 3.13 # by the following command: # # pip-compile typing.in # iniconfig==2.0.0 # via pytest -mypy==1.11.1 +mypy==1.13.0 # via -r typing.in mypy-extensions==1.0.0 # via mypy @@ -16,17 +16,19 @@ packaging==24.1 # via pytest pluggy==1.5.0 # via pytest -pyright==1.1.377 +pyright==1.1.386 # via -r typing.in -pytest==8.3.2 +pytest==8.3.3 # via -r typing.in types-contextvars==2.4.7.3 # via -r typing.in types-dataclasses==0.6.6 # via -r typing.in -types-setuptools==73.0.0.20240822 +types-setuptools==75.2.0.20241019 # via -r typing.in typing-extensions==4.12.2 - # via mypy -watchdog==4.0.2 + # via + # mypy + # pyright +watchdog==5.0.3 # via -r typing.in diff --git a/src/werkzeug/_reloader.py b/src/werkzeug/_reloader.py index d7e91a61c..8fd50b963 100644 --- a/src/werkzeug/_reloader.py +++ b/src/werkzeug/_reloader.py @@ -281,7 +281,7 @@ def trigger_reload(self, filename: str) -> None: self.log_reload(filename) sys.exit(3) - def log_reload(self, filename: str) -> None: + def log_reload(self, filename: str | bytes) -> None: filename = os.path.abspath(filename) _log("info", f" * Detected change in {filename!r}, reloading") @@ -312,7 +312,11 @@ def run_step(self) -> None: class WatchdogReloaderLoop(ReloaderLoop): def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: - from watchdog.events import EVENT_TYPE_OPENED + from watchdog.events import EVENT_TYPE_CLOSED + from watchdog.events import EVENT_TYPE_CREATED + from watchdog.events import EVENT_TYPE_DELETED + from watchdog.events import EVENT_TYPE_MODIFIED + from watchdog.events import EVENT_TYPE_MOVED from watchdog.events import FileModifiedEvent from watchdog.events import PatternMatchingEventHandler from watchdog.observers import Observer @@ -322,7 +326,14 @@ def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: class EventHandler(PatternMatchingEventHandler): def on_any_event(self, event: FileModifiedEvent): # type: ignore - if event.event_type == EVENT_TYPE_OPENED: + if event.event_type not in { + EVENT_TYPE_CLOSED, + EVENT_TYPE_CREATED, + EVENT_TYPE_DELETED, + EVENT_TYPE_MODIFIED, + EVENT_TYPE_MOVED, + }: + # skip events that don't involve changes to the file return trigger_reload(event.src_path) @@ -340,7 +351,7 @@ def on_any_event(self, event: FileModifiedEvent): # type: ignore # the source file (or initial pyc file) as well. Ignore Git and # Mercurial internal changes. extra_patterns = [p for p in self.extra_files if not os.path.isdir(p)] - self.event_handler = EventHandler( # type: ignore[no-untyped-call] + self.event_handler = EventHandler( patterns=["*.py", "*.pyc", "*.zip", *extra_patterns], ignore_patterns=[ *[f"*/{d}/*" for d in _ignore_common_dirs], @@ -349,7 +360,7 @@ def on_any_event(self, event: FileModifiedEvent): # type: ignore ) self.should_reload = False - def trigger_reload(self, filename: str) -> None: + def trigger_reload(self, filename: str | bytes) -> None: # This is called inside an event handler, which means throwing # SystemExit has no effect. # https://github.com/gorakhargosh/watchdog/issues/294 @@ -358,11 +369,11 @@ def trigger_reload(self, filename: str) -> None: def __enter__(self) -> ReloaderLoop: self.watches: dict[str, t.Any] = {} - self.observer.start() # type: ignore[no-untyped-call] + self.observer.start() return super().__enter__() def __exit__(self, exc_type, exc_val, exc_tb): # type: ignore - self.observer.stop() # type: ignore[no-untyped-call] + self.observer.stop() self.observer.join() def run(self) -> None: @@ -378,7 +389,7 @@ def run_step(self) -> None: 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( # type: ignore[no-untyped-call] + self.watches[path] = self.observer.schedule( self.event_handler, path, recursive=True ) except OSError: @@ -393,7 +404,7 @@ def run_step(self) -> None: watch = self.watches.pop(path, None) if watch is not None: - self.observer.unschedule(watch) # type: ignore[no-untyped-call] + self.observer.unschedule(watch) reloader_loops: dict[str, type[ReloaderLoop]] = { diff --git a/src/werkzeug/debug/__init__.py b/src/werkzeug/debug/__init__.py index 69ad3f4f4..0c4cabd89 100644 --- a/src/werkzeug/debug/__init__.py +++ b/src/werkzeug/debug/__init__.py @@ -173,7 +173,8 @@ def get_pin_and_cookie_name( # App Engine. It may also raise a KeyError if the UID does not # have a username, such as in Docker. username = getpass.getuser() - except (ImportError, KeyError): + # Python >= 3.13 only raises OSError + except (ImportError, KeyError, OSError): username = None mod = sys.modules.get(modname) diff --git a/src/werkzeug/http.py b/src/werkzeug/http.py index c86e4750b..1b80c5012 100644 --- a/src/werkzeug/http.py +++ b/src/werkzeug/http.py @@ -361,6 +361,10 @@ def parse_dict_header(value: str) -> dict[str, str | None]: key, has_value, value = item.partition("=") key = key.strip() + if not key: + # =value is not valid + continue + if not has_value: result[key] = None continue diff --git a/src/werkzeug/middleware/shared_data.py b/src/werkzeug/middleware/shared_data.py index 0a0c95675..0f467f2e2 100644 --- a/src/werkzeug/middleware/shared_data.py +++ b/src/werkzeug/middleware/shared_data.py @@ -11,6 +11,7 @@ from __future__ import annotations +import collections.abc as cabc import importlib.util import mimetypes import os @@ -103,7 +104,7 @@ def __init__( self, app: WSGIApplication, exports: ( - dict[str, str | tuple[str, str]] + cabc.Mapping[str, str | tuple[str, str]] | t.Iterable[tuple[str, str | tuple[str, str]]] ), disallow: None = None, @@ -116,7 +117,7 @@ def __init__( self.cache = cache self.cache_timeout = cache_timeout - if isinstance(exports, dict): + if isinstance(exports, cabc.Mapping): exports = exports.items() for key, value in exports: diff --git a/src/werkzeug/serving.py b/src/werkzeug/serving.py index 4faf9262c..ef32b8811 100644 --- a/src/werkzeug/serving.py +++ b/src/werkzeug/serving.py @@ -473,9 +473,11 @@ def log_message(self, format: str, *args: t.Any) -> None: self.log("info", format, *args) def log(self, type: str, message: str, *args: t.Any) -> None: + # an IPv6 scoped address contains "%" which breaks logging + address_string = self.address_string().replace("%", "%%") _log( type, - f"{self.address_string()} - - [{self.log_date_time_string()}] {message}\n", + f"{address_string} - - [{self.log_date_time_string()}] {message}\n", *args, ) diff --git a/tests/live_apps/run.py b/tests/live_apps/run.py index aacdcb664..1371e6723 100644 --- a/tests/live_apps/run.py +++ b/tests/live_apps/run.py @@ -4,6 +4,7 @@ from werkzeug.serving import generate_adhoc_ssl_context from werkzeug.serving import run_simple +from werkzeug.serving import WSGIRequestHandler from werkzeug.wrappers import Request from werkzeug.wrappers import Response @@ -23,10 +24,14 @@ def app(request): kwargs.update(hostname="127.0.0.1", port=5000, application=app) kwargs.update(json.loads(sys.argv[2])) ssl_context = kwargs.get("ssl_context") +override_client_addr = kwargs.pop("override_client_addr", None) if ssl_context == "custom": kwargs["ssl_context"] = generate_adhoc_ssl_context() elif isinstance(ssl_context, list): kwargs["ssl_context"] = tuple(ssl_context) +if override_client_addr: + WSGIRequestHandler.address_string = lambda _: override_client_addr + run_simple(**kwargs) diff --git a/tests/test_http.py b/tests/test_http.py index 11147f63c..9febd0f0c 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -107,9 +107,16 @@ def test_set_header(self): def test_list_header(self, value, expect): assert http.parse_list_header(value) == expect - def test_dict_header(self): - d = http.parse_dict_header('foo="bar baz", blah=42') - assert d == {"foo": "bar baz", "blah": "42"} + @pytest.mark.parametrize( + ("value", "expect"), + [ + ('foo="bar baz", blah=42', {"foo": "bar baz", "blah": "42"}), + ("foo, bar=", {"foo": None, "bar": ""}), + ("=foo, =", {}), + ], + ) + def test_dict_header(self, value, expect): + assert http.parse_dict_header(value) == expect def test_cache_control_header(self): cc = http.parse_cache_control_header("max-age=0, no-cache") @@ -204,6 +211,10 @@ def test_authorization_header(self): assert Authorization.from_header(None) is None assert Authorization.from_header("foo").type == "foo" + def test_authorization_ignore_invalid_parameters(self): + a = Authorization.from_header("Digest foo, bar=, =qux, =") + assert a.to_header() == 'Digest foo, bar=""' + def test_authorization_token_padding(self): # padded with = token = base64.b64encode(b"This has base64 padding").decode() diff --git a/tests/test_serving.py b/tests/test_serving.py index 4abc755d9..2de67dab0 100644 --- a/tests/test_serving.py +++ b/tests/test_serving.py @@ -10,6 +10,7 @@ from unittest.mock import patch import pytest +from watchdog import version as watchdog_version from watchdog.events import EVENT_TYPE_MODIFIED from watchdog.events import EVENT_TYPE_OPENED from watchdog.events import FileModifiedEvent @@ -136,6 +137,28 @@ def test_watchdog_reloader_ignores_opened(mock_trigger_reload): reloader.trigger_reload.assert_not_called() +@pytest.mark.skipif( + watchdog_version.VERSION_MAJOR < 5, + reason="'closed no write' event introduced in watchdog 5.0", +) +@patch.object(WatchdogReloaderLoop, "trigger_reload") +def test_watchdog_reloader_ignores_closed_no_write(mock_trigger_reload): + from watchdog.events import EVENT_TYPE_CLOSED_NO_WRITE + + reloader = WatchdogReloaderLoop() + modified_event = FileModifiedEvent("") + modified_event.event_type = EVENT_TYPE_MODIFIED + reloader.event_handler.on_any_event(modified_event) + mock_trigger_reload.assert_called_once() + + reloader.trigger_reload.reset_mock() + + opened_event = FileModifiedEvent("") + opened_event.event_type = EVENT_TYPE_CLOSED_NO_WRITE + reloader.event_handler.on_any_event(opened_event) + reloader.trigger_reload.assert_not_called() + + @pytest.mark.skipif(sys.version_info >= (3, 10), reason="not needed on >= 3.10") def test_windows_get_args_for_reloading(monkeypatch, tmp_path): argv = [str(tmp_path / "test.exe"), "run"] @@ -314,3 +337,14 @@ def test_streaming_chunked_truncation(dev_server): """ with pytest.raises(http.client.IncompleteRead): dev_server("streaming", threaded=True).request("/crash") + + +@pytest.mark.filterwarnings("ignore::pytest.PytestUnraisableExceptionWarning") +@pytest.mark.dev_server +def test_host_with_ipv6_scope(dev_server): + client = dev_server(override_client_addr="fe80::1ff:fe23:4567:890a%eth2") + r = client.request("/crash") + + assert r.status == 500 + assert b"Internal Server Error" in r.data + assert "Logging error" not in client.log.read() diff --git a/tox.ini b/tox.ini index da81aa65c..77f381c59 100644 --- a/tox.ini +++ b/tox.ini @@ -15,6 +15,9 @@ use_frozen_constraints = true deps = -r requirements/tests.txt commands = pytest -v --tb=short --basetemp={envtmpdir} {posargs} +[testenv:py38,py3.8] +deps = -r requirements/tests38.txt + [testenv:style] deps = pre-commit skip_install = true @@ -50,3 +53,11 @@ commands = pip-compile tests.in -q {posargs:-U} pip-compile typing.in -q {posargs:-U} pip-compile dev.in -q {posargs:-U} + +[testenv:update-requirements38] +base_python = 3.8 +labels = update +deps = pip-tools +skip_install = true +change_dir = requirements +commands = pip-compile tests.in -q -o tests38.txt {posargs:-U}