nebius/SWE-bench-extra huggingface dataset:
Repository: lundberg/respx

Issue:
Cannot create a lookup to match empty params
https://github.com/lundberg/respx/blob/c76f632690cdfb2c878fe8fea999df0a26083eb3/respx/patterns.py#L502-L507

E.g. `respx_mock.get("https://example.com", params__eq={}).mock(...)` matches request with any query params including empty.
Looks like the value check must be more accurate.

respx version 0.19.2


Comments:

Patch:
diff --git a/respx/patterns.py b/respx/patterns.py
index 37ab1d4..59a9e4d 100644
--- a/respx/patterns.py
+++ b/respx/patterns.py
@@ -504,9 +504,6 @@ def M(*patterns: Pattern, **lookups: Any) -> Optional[Pattern]:
     extras = None
 
     for pattern__lookup, value in lookups.items():
-        if not value:
-            continue
-
         # Handle url pattern
         if pattern__lookup == "url":
             extras = parse_url_patterns(value)
@@ -535,6 +532,10 @@ def M(*patterns: Pattern, **lookups: Any) -> Optional[Pattern]:
             lookup = Lookup(lookup_name) if lookup_name else None
             pattern = P(value, lookup=lookup)
 
+        # Skip patterns with no value, exept when using equal lookup
+        if not pattern.value and pattern.lookup is not Lookup.EQUAL:
+            continue
+
         patterns += (pattern,)
 
     # Combine and merge patterns


Test patch:
diff --git a/tests/test_patterns.py b/tests/test_patterns.py
index 0f06293..f90c561 100644
--- a/tests/test_patterns.py
+++ b/tests/test_patterns.py
@@ -66,6 +66,19 @@ def test_match_context():
     assert match.context == {"host": "foo.bar", "slug": "baz"}
 
 
+@pytest.mark.parametrize(
+    "kwargs,url,expected",
+    [
+        ({"params__eq": {}}, "https://foo.bar/", True),
+        ({"params__eq": {}}, "https://foo.bar/?x=y", False),
+        ({"params__contains": {}}, "https://foo.bar/?x=y", True),
+    ],
+)
+def test_m_pattern(kwargs, url, expected):
+    request = httpx.Request("GET", url)
+    assert bool(M(host="foo.bar", **kwargs).match(request)) is expected
+
+
 @pytest.mark.parametrize(
     "lookup,value,expected",
     [
@@ -217,6 +230,8 @@ def test_path_pattern():
         (Lookup.EQUAL, "y=2", "https://foo.bar/?x=1", False),
         (Lookup.EQUAL, {"x": ANY}, "https://foo.bar/?x=1", True),
         (Lookup.EQUAL, {"y": ANY}, "https://foo.bar/?x=1", False),
+        (Lookup.EQUAL, {}, "https://foo.bar/?x=1", False),
+        (Lookup.EQUAL, {}, "https://foo.bar/", True),
         (Lookup.EQUAL, "x=1&y=2", "https://foo.bar/?x=1", False),
         (Lookup.EQUAL, "y=2&x=1", "https://foo.bar/?x=1&y=2", True),
         (Lookup.EQUAL, "y=3&x=2&x=1", "https://foo.bar/?x=1&x=2&y=3", False),  # ordered
Repository: lundberg/respx

Issue:
None type doesn't work in data matching as of v0.21.0
As of the latest version `v0.21.0`, None values in data lookups don't work:

```python
import httpx
import respx


@respx.mock
def test():
    data = {"test": None}

    respx.post("http://test.com", data=data).respond()

    response = httpx.post("http://test.com", data=data)

    assert response.status_code == 200


if __name__ == "__main__":
    test()
```
output:
```python
respx.models.AllMockedAssertionError: RESPX: <Request('POST', 'http://test.com/')> not mocked!
```

This test passes successfully in `v0.20.2`


The following _do_ work:
```python
    data = {"test": ""}
    respx.post("http://test.com", data=data).respond()
    response = httpx.post("http://test.com", data=data)
```

```python 
    respx.post("http://test.com", data={"test": ""}).respond()
    response = httpx.post("http://test.com", data={"test": None})
```

So there's just a bug somewhere translating None to ""



Comments:

Patch:
diff --git a/respx/patterns.py b/respx/patterns.py
index d8b00b5..8d80148 100644
--- a/respx/patterns.py
+++ b/respx/patterns.py
@@ -548,7 +548,9 @@ class Data(MultiItemsMixin, Pattern):
     value: MultiItems
 
     def clean(self, value: Dict) -> MultiItems:
-        return MultiItems(value)
+        return MultiItems(
+            (key, "" if value is None else str(value)) for key, value in value.items()
+        )
 
     def parse(self, request: httpx.Request) -> Any:
         data, _ = decode_data(request)
diff --git a/respx/utils.py b/respx/utils.py
index 5a6ce3a..5eb4715 100644
--- a/respx/utils.py
+++ b/respx/utils.py
@@ -1,7 +1,18 @@
 import email
 from datetime import datetime
 from email.message import Message
-from typing import Dict, List, NamedTuple, Optional, Tuple, Type, TypeVar, Union, cast
+from typing import (
+    Any,
+    Dict,
+    List,
+    NamedTuple,
+    Optional,
+    Tuple,
+    Type,
+    TypeVar,
+    Union,
+    cast,
+)
 from urllib.parse import parse_qsl
 
 try:
@@ -13,13 +24,13 @@ import httpx
 
 
 class MultiItems(dict):
-    def get_list(self, key: str) -> List[str]:
+    def get_list(self, key: str) -> List[Any]:
         try:
             return [self[key]]
         except KeyError:  # pragma: no cover
             return []
 
-    def multi_items(self) -> List[Tuple[str, str]]:
+    def multi_items(self) -> List[Tuple[str, Any]]:
         return list(self.items())
 
 


Test patch:
diff --git a/tests/test_patterns.py b/tests/test_patterns.py
index dda7914..4b119aa 100644
--- a/tests/test_patterns.py
+++ b/tests/test_patterns.py
@@ -350,6 +350,18 @@ def test_content_pattern(lookup, content, expected):
             None,
             True,
         ),
+        (
+            Lookup.EQUAL,
+            {"none_value": None},
+            None,
+            True,
+        ),
+        (
+            Lookup.EQUAL,
+            {"non_str": 123},
+            None,
+            True,
+        ),
         (
             Lookup.EQUAL,
             {"x": "a"},