Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-respx for openSUSE:Factory checked in at 2026-04-14 17:48:09 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-respx (Old) and /work/SRC/openSUSE:Factory/.python-respx.new.21863 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-respx" Tue Apr 14 17:48:09 2026 rev:12 rq:1346239 version:0.23.1 Changes: -------- --- /work/SRC/openSUSE:Factory/python-respx/python-respx.changes 2025-04-11 16:44:58.934690915 +0200 +++ /work/SRC/openSUSE:Factory/.python-respx.new.21863/python-respx.changes 2026-04-14 17:48:17.659477178 +0200 @@ -1,0 +2,20 @@ +Sun Apr 12 19:21:56 UTC 2026 - Dirk Müller <[email protected]> + +- update to 0.23.1: + * Fix regression causing `params` pattern to stop working under + some conditions, by doing a strict detection of `ANY` in multi + items patterns + * Fix `data` pattern with list value + * Fix and enhance incorrect documentations about iterable side + effects + * Fix documentation typo, thanks @markhobson + * Fix support for multiple slashes `//` in URL path by not + using `urljoin` when + * prepending path, thanks @lewiscollard and @Skeen + * Type Route.respond json as `Any` to align with HTTPX + * Properly handle `ANY` in `MuitiItems` patterns + * Fix test warnings + * Add Python 3.14 to test matrix, thanks @carlosdorneles-mb + * Update nix flake, mypy target and workflows + +------------------------------------------------------------------- Old: ---- respx-0.22.0.tar.gz New: ---- respx-0.23.1.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-respx.spec ++++++ --- /var/tmp/diff_new_pack.0bqDqe/_old 2026-04-14 17:48:18.367506444 +0200 +++ /var/tmp/diff_new_pack.0bqDqe/_new 2026-04-14 17:48:18.371506609 +0200 @@ -1,7 +1,7 @@ # # spec file for package python-respx # -# Copyright (c) 2025 SUSE LLC +# Copyright (c) 2026 SUSE LLC and contributors # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -18,7 +18,7 @@ %{?sle15_python_module_pythons} Name: python-respx -Version: 0.22.0 +Version: 0.23.1 Release: 0 Summary: Mock HTTPX with request patterns and response side effects License: BSD-3-Clause @@ -37,7 +37,7 @@ BuildRequires: %{python_module wheel} BuildRequires: fdupes BuildRequires: python-rpm-macros -Requires: python-httpx >= 0.21 +Requires: python-httpx >= 0.25.0 BuildArch: noarch %python_subpackages @@ -60,7 +60,7 @@ %files %{python_files} %license LICENSE.md -%doc README.md +%doc CHANGELOG.md README.md %{python_sitelib}/respx-%{version}.dist-info %{python_sitelib}/respx ++++++ respx-0.22.0.tar.gz -> respx-0.23.1.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/respx-0.22.0/.github/workflows/check-docs.yml new/respx-0.23.1/.github/workflows/check-docs.yml --- old/respx-0.22.0/.github/workflows/check-docs.yml 2024-12-19 23:29:26.000000000 +0100 +++ new/respx-0.23.1/.github/workflows/check-docs.yml 2026-04-08 16:34:22.000000000 +0200 @@ -11,10 +11,10 @@ name: Check Docs runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 with: python-version: "3.10" - run: pip install nox - - name: Run mypy + - name: Build run: nox -N -s docs diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/respx-0.22.0/.github/workflows/lint.yml new/respx-0.23.1/.github/workflows/lint.yml --- old/respx-0.22.0/.github/workflows/lint.yml 2024-12-19 23:29:26.000000000 +0100 +++ new/respx-0.23.1/.github/workflows/lint.yml 2026-04-08 16:34:22.000000000 +0200 @@ -6,4 +6,4 @@ jobs: lint: name: Check Linting - uses: less-action/reusables/.github/workflows/pre-commit.yaml@v8 + uses: less-action/reusables/.github/workflows/pre-commit.yaml@v10 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/respx-0.22.0/.github/workflows/publish-docs.yml new/respx-0.23.1/.github/workflows/publish-docs.yml --- old/respx-0.22.0/.github/workflows/publish-docs.yml 2024-12-19 23:29:26.000000000 +0100 +++ new/respx-0.23.1/.github/workflows/publish-docs.yml 2026-04-08 16:34:22.000000000 +0200 @@ -13,8 +13,8 @@ name: Build & Publish runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 with: python-version: "3.10" - run: pip install nox diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/respx-0.22.0/.github/workflows/test.yml new/respx-0.23.1/.github/workflows/test.yml --- old/respx-0.22.0/.github/workflows/test.yml 2024-12-19 23:29:26.000000000 +0100 +++ new/respx-0.23.1/.github/workflows/test.yml 2026-04-08 16:34:22.000000000 +0200 @@ -26,11 +26,11 @@ fail-fast: false max-parallel: 4 matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - run: pip install nox @@ -51,10 +51,10 @@ name: Check Typing runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 with: - python-version: "3.8" + python-version: "3.10" - run: pip install nox - name: Run mypy run: nox -N -s mypy diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/respx-0.22.0/CHANGELOG.md new/respx-0.23.1/CHANGELOG.md --- old/respx-0.22.0/CHANGELOG.md 2024-12-19 23:29:26.000000000 +0100 +++ new/respx-0.23.1/CHANGELOG.md 2026-04-08 16:34:22.000000000 +0200 @@ -5,6 +5,35 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.23.1] - 2026-04-08 + +### Fixed + +- Fix regression causing `params` pattern to stop working under some conditions, + by doing a strict detection of `ANY` in multi items patterns (#313) + +### CI + +- Update workflows actions (#310) + +## [0.23.0] - 2026-04-07 + +### Fixed + +- Fix `data` pattern with list value (#264) +- Fix and enhance incorrect documentations about iterable side effects (#287) +- Fix documentation typo, thanks @markhobson (#298) +- Fix support for multiple slashes `//` in URL path by not using `urljoin` when + prepending path, thanks @lewiscollard and @Skeen (#302) +- Type Route.respond json as `Any` to align with HTTPX, thanks @JacobHayes (#284) +- Properly handle `ANY` in `MuitiItems` patterns (#289) + +### CI + +- Fix test warnings (#267) +- Add Python 3.14 to test matrix, thanks @carlosdorneles-mb (#300) +- Update nix flake, mypy target and workflows (#306, #282) + ## [0.22.0] - 2024-12-19 ### Fixed diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/respx-0.22.0/README.md new/respx-0.23.1/README.md --- old/respx-0.22.0/README.md 2024-12-19 23:29:26.000000000 +0100 +++ new/respx-0.23.1/README.md 2026-04-08 16:34:22.000000000 +0200 @@ -10,6 +10,7 @@ [](https://github.com/lundberg/respx/actions/workflows/test.yml) [](https://codecov.io/gh/lundberg/respx) [](https://pypi.org/project/respx/) +[](https://pypi.org/project/respx/) [](https://pypi.org/project/respx/) ## Documentation diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/respx-0.22.0/Taskfile.yaml new/respx-0.23.1/Taskfile.yaml --- old/respx-0.22.0/Taskfile.yaml 2024-12-19 23:29:26.000000000 +0100 +++ new/respx-0.23.1/Taskfile.yaml 2026-04-08 16:34:22.000000000 +0200 @@ -24,7 +24,8 @@ desc: Statically type check python files silent: true deps: [tools] - cmds: [.venv/bin/nox -R -s mypy] + cmds: + - .venv/bin/nox {{.CLI_ARGS | default "-R"}} -s mypy lint: desc: Lint project files diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/respx-0.22.0/docs/api.md new/respx-0.23.1/docs/api.md --- old/respx-0.22.0/docs/api.md 2024-12-19 23:29:26.000000000 +0100 +++ new/respx-0.23.1/docs/api.md 2026-04-08 16:34:22.000000000 +0200 @@ -149,7 +149,7 @@ > Response *text* content to mock, with automatic content-type header added. > * **html** - *(optional) str* > Response *HTML* content to mock, with automatic content-type header added. -> * **json** - *(optional) str | list | dict* +> * **json** - *(optional) Any* > Response *JSON* content to mock, with automatic content-type header added. > * **stream** - *(optional) Iterable[bytes]* > Response *stream* to mock. @@ -190,7 +190,7 @@ > Text content, with automatic content-type header added. > * **html** - *(optional) str* > HTML content, with automatic content-type header added. -> * **json** - *(optional) str | list | dict* +> * **json** - *(optional) Any* > JSON content, with automatic content-type header added. > * **stream** - *(optional) Iterable[bytes]* > Content *stream*. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/respx-0.22.0/docs/guide.md new/respx-0.23.1/docs/guide.md --- old/respx-0.22.0/docs/guide.md 2024-12-19 23:29:26.000000000 +0100 +++ new/respx-0.23.1/docs/guide.md 2026-04-08 16:34:22.000000000 +0200 @@ -518,9 +518,10 @@ assert route.call_count == 2 ``` -Once the iterable is *exhausted*, the route will fallback and respond with the `return_value`, if set. +Like python `Mock` side effects, `StopIteration` will be raised once the iterable is *exhausted*. A more practical use case is to have the last entry infinitely repeated, which can be done by utilizing `itertools`. ``` python +from itertools import chain, repeat import httpx import respx @@ -528,8 +529,10 @@ @respx.mock def test_stacked_responses(): respx.post("https://example.org/").mock( - side_effect=[httpx.Response(201)], - return_value=httpx.Response(200) + side_effect=chain( + [httpx.Response(201)], + repeat(httpx.Response(200)), + ) ) response1 = httpx.post("https://example.org/") @@ -637,7 +640,7 @@ ## Mock without patching HTTPX -If you don't *need* to patch `HTTPX`, use `httpx.MockTransport` with a REPX router as handler, when instantiating your client. +If you don't *need* to patch `HTTPX`, use `httpx.MockTransport` with a RESPX router as handler, when instantiating your client. ``` python import httpx diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/respx-0.22.0/docs/index.md new/respx-0.23.1/docs/index.md --- old/respx-0.22.0/docs/index.md 2024-12-19 23:29:26.000000000 +0100 +++ new/respx-0.23.1/docs/index.md 2026-04-08 16:34:22.000000000 +0200 @@ -10,7 +10,7 @@ Mock [HTTPX](https://www.python-httpx.org/) with awesome request patterns and response side effects. -[](https://github.com/lundberg/respx/actions/workflows/test.yml) [](https://codecov.io/gh/lundberg/respx) [](https://pypi.org/project/respx/) [](https://pypi.org/project/respx/) +[](https://github.com/lundberg/respx/actions/workflows/test.yml) [](https://codecov.io/gh/lundberg/respx) [](https://pypi.org/project/respx/) [](https://pypi.org/project/respx/) [](https://pypi.org/project/respx/) ## QuickStart diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/respx-0.22.0/flake.lock new/respx-0.23.1/flake.lock --- old/respx-0.22.0/flake.lock 2024-12-19 23:29:26.000000000 +0100 +++ new/respx-0.23.1/flake.lock 2026-04-08 16:34:22.000000000 +0200 @@ -5,11 +5,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1710146030, - "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", "owner": "numtide", "repo": "flake-utils", - "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", "type": "github" }, "original": { @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1710777701, - "narHash": "sha256-hMyIBLJY2VjsM/dOmXta5XdyxcuQoKUkm4M/K0c0xlo=", + "lastModified": 1775549110, + "narHash": "sha256-gCXSLBI1drlFwYlNqqPS9cFXvraaEGyLS8Sq45p7b/Q=", "owner": "nixos", "repo": "nixpkgs", - "rev": "f78a4dcd452449992e526fd88a60a2d45e0ae969", + "rev": "a3db02183b5da6fbf728203492a5d1b9d109b7f9", "type": "github" }, "original": { @@ -49,13 +49,29 @@ "type": "github" } }, + "nixpkgs24": { + "locked": { + "lastModified": 1731603435, + "narHash": "sha256-CqCX4JG7UiHvkrBTpYC3wcEurvbtTADLbo3Ns2CEoL8=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "8b27c1239e5c421a2bbc2c65d52e4a6fbf2ff296", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "24.11", + "repo": "nixpkgs", + "type": "github" + } + }, "nixpkgsUnstable": { "locked": { - "lastModified": 1710734606, - "narHash": "sha256-rFJl+WXfksu2NkWJWKGd5Km17ZGEjFg9hOQNwstsoU8=", + "lastModified": 1775464765, + "narHash": "sha256-nex6TL2x1/sVHCyDWcvl1t/dbTedb9bAGC4DLf/pmYk=", "owner": "nixos", "repo": "nixpkgs", - "rev": "79bb4155141a5e68f2bdee2bf6af35b1d27d3a1d", + "rev": "83e29f2b8791f6dec20804382fcd9a666d744c07", "type": "github" }, "original": { @@ -70,6 +86,7 @@ "flakeUtils": "flakeUtils", "nixpkgs": "nixpkgs", "nixpkgs22": "nixpkgs22", + "nixpkgs24": "nixpkgs24", "nixpkgsUnstable": "nixpkgsUnstable" } }, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/respx-0.22.0/flake.nix new/respx-0.23.1/flake.nix --- old/respx-0.22.0/flake.nix 2024-12-19 23:29:26.000000000 +0100 +++ new/respx-0.23.1/flake.nix 2026-04-08 16:34:22.000000000 +0200 @@ -1,28 +1,32 @@ { inputs = { nixpkgs.url = "github:nixos/nixpkgs"; + nixpkgs24.url = "github:nixos/nixpkgs/24.11"; nixpkgs22.url = "github:nixos/nixpkgs/22.11"; nixpkgsUnstable.url = "github:nixos/nixpkgs/nixpkgs-unstable"; flakeUtils.url = "github:numtide/flake-utils"; }; - outputs = { self, nixpkgs, nixpkgs22, nixpkgsUnstable, flakeUtils }: + outputs = { self, nixpkgs, nixpkgs24, nixpkgs22, nixpkgsUnstable, flakeUtils }: flakeUtils.lib.eachDefaultSystem (system: let pkgs = nixpkgs.legacyPackages.${system}; + pkgs24 = nixpkgs24.legacyPackages.${system}; pkgs22 = nixpkgs22.legacyPackages.${system}; pkgsUnstable = nixpkgsUnstable.legacyPackages.${system}; in { packages = flakeUtils.lib.flattenTree { + python314 = pkgs.python314; python313 = pkgs.python313; python312 = pkgs.python312; python311 = pkgs.python311; - python310 = pkgs.python310; - python39 = pkgs.python39; + python310 = pkgs24.python310; + python39 = pkgs24.python39; python38 = pkgs22.python38; go-task = pkgsUnstable.go-task; }; devShell = pkgs.mkShell { buildInputs = with self.packages.${system}; [ + python314 python313 python312 python311 @@ -34,7 +38,7 @@ shellHook = '' [[ ! -d .venv ]] && \ echo "Creating virtualenv ..." && \ - ${pkgs.python310}/bin/python -m \ + ${pkgs24.python310}/bin/python -m \ venv --copies --upgrade-deps .venv > /dev/null source .venv/bin/activate ''; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/respx-0.22.0/noxfile.py new/respx-0.23.1/noxfile.py --- old/respx-0.22.0/noxfile.py 2024-12-19 23:29:26.000000000 +0100 +++ new/respx-0.23.1/noxfile.py 2026-04-08 16:34:22.000000000 +0200 @@ -5,7 +5,7 @@ nox.options.keywords = "test + mypy" [email protected](python=["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]) [email protected](python=["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]) def test(session): deps = ["pytest", "pytest-asyncio", "pytest-cov", "trio", "starlette", "flask"] session.install("--upgrade", *deps) @@ -17,7 +17,7 @@ session.run("pytest", *session.posargs) [email protected](python="3.8") [email protected](python="3.10") def mypy(session): session.install("--upgrade", "mypy") session.install("-e", ".") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/respx-0.22.0/respx/__version__.py new/respx-0.23.1/respx/__version__.py --- old/respx-0.22.0/respx/__version__.py 2024-12-19 23:29:26.000000000 +0100 +++ new/respx-0.23.1/respx/__version__.py 2026-04-08 16:34:22.000000000 +0200 @@ -1 +1 @@ -__version__ = "0.22.0" +__version__ = "0.23.1" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/respx-0.22.0/respx/models.py new/respx-0.23.1/respx/models.py --- old/respx-0.22.0/respx/models.py 2024-12-19 23:29:26.000000000 +0100 +++ new/respx-0.23.1/respx/models.py 2026-04-08 16:34:22.000000000 +0200 @@ -277,7 +277,7 @@ content: Optional[Content] = None, text: Optional[str] = None, html: Optional[str] = None, - json: Optional[Union[str, List, Dict]] = None, + json: Any = None, stream: Optional[Union[httpx.SyncByteStream, httpx.AsyncByteStream]] = None, content_type: Optional[str] = None, http_version: Optional[str] = None, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/respx-0.22.0/respx/patterns.py new/respx-0.23.1/respx/patterns.py --- old/respx-0.22.0/respx/patterns.py 2024-12-19 23:29:26.000000000 +0100 +++ new/respx-0.23.1/respx/patterns.py 2026-04-08 16:34:22.000000000 +0200 @@ -24,7 +24,6 @@ Union, ) from unittest.mock import ANY -from urllib.parse import urljoin import httpx @@ -289,21 +288,38 @@ value: Any def _multi_items( - self, value: Any, *, parse_any: bool = False + self, value: Any, *, parse_any: bool = False, encode_any: bool = False ) -> Tuple[Tuple[str, Tuple[Any, ...]], ...]: return tuple( ( key, tuple( - ANY if parse_any and v == str(ANY) else v + self._item_value(v, parse_any=parse_any, encode_any=encode_any) for v in value.get_list(key) ), ) for key in sorted(value.keys()) ) + def _item_value( + self, value: Any, parse_any: bool = False, encode_any: bool = False + ) -> Any: + return ( + ANY + if parse_any and value == str(ANY) + else str(ANY) + if encode_any and value is ANY + else value + ) + def __hash__(self): - return hash((self.__class__, self.lookup, self._multi_items(self.value))) + return hash( + ( + self.__class__, + self.lookup, + self._multi_items(self.value, encode_any=True), + ) + ) def _eq(self, value: Any) -> Match: value_items = self._multi_items(self.value, parse_any=True) @@ -429,7 +445,11 @@ else "".join(f"%{byte:02x}" for byte in char.encode("utf-8")).upper() for char in value ) - path = urljoin("/", path) # Ensure leading slash + # Ensure a leading slash. Note we don't use urljoin because its + # behaviour with multiple slashes in the path is incorrect - see + # https://github.com/lundberg/respx/issues/273 + if not path.startswith("/"): + path = f"/{path}" value = httpx.URL(path).path elif self.lookup is Lookup.REGEX and isinstance(value, str): value = re.compile(value) @@ -548,9 +568,17 @@ key = "data" value: MultiItems - def clean(self, value: Dict) -> MultiItems: + def _normalize_value(self, value: Any) -> Union[str, List[str]]: + if value is None: + return "" + elif isinstance(value, (tuple, list)): + return [str(v) for v in value] + else: + return str(value) + + def clean(self, value: Dict[str, Any]) -> MultiItems: return MultiItems( - (key, "" if value is None else str(value)) for key, value in value.items() + (key, self._normalize_value(value)) for key, value in value.items() ) def parse(self, request: httpx.Request) -> Any: @@ -563,7 +591,7 @@ key = "files" value: MultiItems - def _normalize_file_value(self, value: FileTypes) -> Tuple[Any, Any]: + def _normalize_file_value(self, value: FileTypes) -> Tuple[Tuple[Any, Any]]: # Mimic httpx `FileField` to normalize `files` kwarg to shortest tuple style if isinstance(value, tuple): filename, fileobj = value[:2] @@ -580,7 +608,16 @@ elif isinstance(fileobj, str): fileobj = fileobj.encode() - return filename, fileobj + return ((filename, fileobj),) + + def _item_value( + self, value: Tuple[Any, Any], parse_any: bool = False, encode_any: bool = False + ) -> Tuple[Any, Any]: + filename, data = value + return ( + super()._item_value(filename, parse_any=parse_any, encode_any=encode_any), + super()._item_value(data, parse_any=parse_any, encode_any=encode_any), + ) def clean(self, value: RequestFiles) -> MultiItems: if isinstance(value, Mapping): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/respx-0.22.0/respx/utils.py new/respx-0.23.1/respx/utils.py --- old/respx-0.22.0/respx/utils.py 2024-12-19 23:29:26.000000000 +0100 +++ new/respx-0.23.1/respx/utils.py 2026-04-08 16:34:22.000000000 +0200 @@ -1,9 +1,11 @@ import email +from collections import defaultdict from datetime import datetime from email.message import Message from typing import ( Any, Dict, + Iterable, List, Literal, NamedTuple, @@ -19,15 +21,24 @@ import httpx -class MultiItems(dict): +class MultiItems(defaultdict): + def __init__(self, values: Optional[Iterable[Tuple[str, Any]]] = None) -> None: + super().__init__(tuple) + if values is not None: + for key, value in values: + if isinstance(value, (tuple, list)): + self[key] += tuple(value) # Convert list to tuple and extend + else: + self[key] += (value,) # Extend with value + def get_list(self, key: str) -> List[Any]: - try: - return [self[key]] - except KeyError: # pragma: no cover - return [] + return list(self[key]) + + def multi_items(self) -> List[Tuple[str, str]]: + return [(key, value) for key, values in self.items() for value in values] - def multi_items(self) -> List[Tuple[str, Any]]: - return list(self.items()) + def append(self, key: str, value: Any) -> None: + self[key] += (value,) def _parse_multipart_form_data( @@ -45,16 +56,17 @@ for payload in email.message_from_bytes(form_data).get_payload(): payload = cast(Message, payload) name = payload.get_param("name", header="Content-Disposition") + assert isinstance(name, str) filename = payload.get_filename() content_type = payload.get_content_type() value = payload.get_payload(decode=True) assert isinstance(value, bytes) if content_type.startswith("text/") and filename is None: # Text field - data[name] = value.decode(payload.get_content_charset() or "utf-8") + data.append(name, value.decode(payload.get_content_charset() or "utf-8")) else: # File field - files[name] = filename, value + files.append(name, (filename, value)) return data, files diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/respx-0.22.0/setup.cfg new/respx-0.23.1/setup.cfg --- old/respx-0.22.0/setup.cfg 2024-12-19 23:29:26.000000000 +0100 +++ new/respx-0.23.1/setup.cfg 2026-04-08 16:34:22.000000000 +0200 @@ -26,6 +26,7 @@ --cov-fail-under 100 -rxXs asyncio_mode = auto +asyncio_default_fixture_loop_scope = session [coverage:run] source = respx,tests @@ -36,7 +37,7 @@ show_missing = True [mypy] -python_version = 3.8 +python_version = 3.10 files = respx,tests pretty = True diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/respx-0.22.0/setup.py new/respx-0.23.1/setup.py --- old/respx-0.22.0/setup.py 2024-12-19 23:29:26.000000000 +0100 +++ new/respx-0.23.1/setup.py 2026-04-08 16:34:22.000000000 +0200 @@ -29,6 +29,7 @@ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", ], project_urls={ "GitHub": "https://github.com/lundberg/respx", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/respx-0.22.0/tests/conftest.py new/respx-0.23.1/tests/conftest.py --- old/respx-0.22.0/tests/conftest.py 2024-12-19 23:29:26.000000000 +0100 +++ new/respx-0.23.1/tests/conftest.py 2026-04-08 16:34:22.000000000 +0200 @@ -2,7 +2,7 @@ import pytest import respx -from respx.fixtures import session_event_loop as event_loop # noqa: F401 +from respx.fixtures import * # noqa: F401, F403 pytest_plugins = ["pytester"] @@ -23,7 +23,7 @@ @pytest.fixture(scope="session") -async def mocked_foo(event_loop): # noqa: F811 +async def mocked_foo(session_event_loop): async with respx.mock( base_url="https://foo.api/api/", using="httpcore" ) as respx_mock: @@ -33,7 +33,7 @@ @pytest.fixture(scope="session") -async def mocked_ham(event_loop): # noqa: F811 +async def mocked_ham(session_event_loop): async with respx.mock(base_url="https://ham.api", using="httpcore") as respx_mock: respx_mock.get("/", name="index").respond(200) yield respx_mock diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/respx-0.22.0/tests/test_api.py new/respx-0.23.1/tests/test_api.py --- old/respx-0.22.0/tests/test_api.py 2024-12-19 23:29:26.000000000 +0100 +++ new/respx-0.23.1/tests/test_api.py 2026-04-08 16:34:22.000000000 +0200 @@ -276,6 +276,7 @@ with respx.mock: url = "https://foo.bar/" file = ("file", ("filename.txt", b"...", "text/plain", {"X-Foo": "bar"})) + respx.post(url + "other", files={"file": mock.ANY}) # Non-matching ANY route = respx.post(url, files={"file": mock.ANY}) % 201 response = httpx.post(url, files=[file]) assert response.status_code == 201 @@ -500,6 +501,10 @@ ) async def test_params_match(client, url, params, call_url, call_params): respx.get(url, params=params) % dict(content="spam spam") + + # Add an extra competing param but with non-matching value, reproduces #311 issue + respx.get(url, params={"foo": "<non-matching>"}) + response = await client.get(call_url, params=call_params) assert response.text == "spam spam" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/respx-0.22.0/tests/test_patterns.py new/respx-0.23.1/tests/test_patterns.py --- old/respx-0.22.0/tests/test_patterns.py 2024-12-19 23:29:26.000000000 +0100 +++ new/respx-0.23.1/tests/test_patterns.py 2026-04-08 16:34:22.000000000 +0200 @@ -224,6 +224,9 @@ path.base = Path("/foo/") assert path.strip_base("/foo/bar/") == "/bar/" + # Regression test for https://github.com/lundberg/respx/issues/273 + assert Path("foo//bar").clean("foo//bar") == "/foo//bar" + @pytest.mark.parametrize( ("lookup", "params", "url", "expected"), @@ -333,6 +336,12 @@ None, True, ), + ( + Lookup.EQUAL, + {"foo": "bar", "ham": ["spam", "egg"]}, + None, + True, + ), ( Lookup.EQUAL, {"foo": "bar", "ham": "spam"}, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/respx-0.22.0/tests/test_plugin.py new/respx-0.23.1/tests/test_plugin.py --- old/respx-0.22.0/tests/test_plugin.py 2024-12-19 23:29:26.000000000 +0100 +++ new/respx-0.23.1/tests/test_plugin.py 2026-04-08 16:34:22.000000000 +0200 @@ -1,3 +1,6 @@ +from textwrap import dedent + + def test_respx_mock_fixture(testdir): testdir.makepyfile( """ @@ -36,5 +39,14 @@ assert some_fixture == "foobar" """ ) + testdir.makeini( + dedent( + """ + [pytest] + asyncio-mode = auto + asyncio_default_fixture_loop_scope = session + """ + ) + ) result = testdir.runpytest("-p", "respx") result.assert_outcomes(passed=4) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/respx-0.22.0/tests/test_router.py new/respx-0.23.1/tests/test_router.py --- old/respx-0.22.0/tests/test_router.py 2024-12-19 23:29:26.000000000 +0100 +++ new/respx-0.23.1/tests/test_router.py 2026-04-08 16:34:22.000000000 +0200 @@ -338,7 +338,7 @@ _route = router.get("https://foo.bar/baz/", name="foobar") assert _route is route assert route.name == "foobar" - assert route.pattern != pattern + assert route.pattern != pattern # type: ignore[unreachable] route.return_value = httpx.Response(418) request = httpx.Request("GET", "https://foo.bar/baz/") response = router.handler(request)
