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 @@
 
[![tests](https://img.shields.io/github/actions/workflow/status/lundberg/respx/test.yml?branch=master&label=tests&logo=github&logoColor=white&style=for-the-badge)](https://github.com/lundberg/respx/actions/workflows/test.yml)
 
[![codecov](https://img.shields.io/codecov/c/github/lundberg/respx?logo=codecov&logoColor=white&style=for-the-badge)](https://codecov.io/gh/lundberg/respx)
 [![PyPi 
Version](https://img.shields.io/pypi/v/respx?logo=pypi&logoColor=white&style=for-the-badge)](https://pypi.org/project/respx/)
+[![PyPI 
Downloads](https://img.shields.io/pypi/dm/respx?style=for-the-badge&logo=pypi&logoColor=white&color=mediumpurple)](https://pypi.org/project/respx/)
 [![Python 
Versions](https://img.shields.io/pypi/pyversions/respx?logo=python&logoColor=white&style=for-the-badge)](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.
 
-[![tests](https://img.shields.io/github/actions/workflow/status/lundberg/respx/test.yml?branch=master&label=tests&logo=github&logoColor=white&style=flat-square)](https://github.com/lundberg/respx/actions/workflows/test.yml)
 
[![codecov](https://img.shields.io/codecov/c/github/lundberg/respx?logo=codecov&logoColor=white&style=flat-square)](https://codecov.io/gh/lundberg/respx)
 [![PyPi 
Version](https://img.shields.io/pypi/v/respx?logo=pypi&logoColor=white&style=flat-square)](https://pypi.org/project/respx/)
 [![Python 
Versions](https://img.shields.io/pypi/pyversions/respx?logo=python&logoColor=white&style=flat-square)](https://pypi.org/project/respx/)
+[![tests](https://img.shields.io/github/actions/workflow/status/lundberg/respx/test.yml?branch=master&label=tests&logo=github&logoColor=white&style=flat-square)](https://github.com/lundberg/respx/actions/workflows/test.yml)
 
[![codecov](https://img.shields.io/codecov/c/github/lundberg/respx?logo=codecov&logoColor=white&style=flat-square)](https://codecov.io/gh/lundberg/respx)
 [![PyPi 
Version](https://img.shields.io/pypi/v/respx?logo=pypi&logoColor=white&style=flat-square)](https://pypi.org/project/respx/)
 [![PyPI 
Downloads](https://img.shields.io/pypi/dm/respx?style=flat-square&logo=pypi&logoColor=white&color=mediumpurple)](https://pypi.org/project/respx/)
 [![Python 
Versions](https://img.shields.io/pypi/pyversions/respx?logo=python&logoColor=white&style=flat-square)](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)

Reply via email to