respx icon indicating copy to clipboard operation
respx copied to clipboard

`unittest.mock.ANY` is unusable in a `files` lookup

Open mikenerone opened this issue 1 year ago • 4 comments

Python 3.12.7 respx 0.21.1

The documentation includes both of the following examples for files lookups:

respx.post("https://example.org/", files={"some_file": ANY})
respx.post("https://example.org/", files={"some_file": ("filename.txt", ANY)})

These don't actually work because since e670690fc547cddeb38867ffc4d37afdb1fa7a61, routes are compared via their hash(), and adding a route triggers a comparison to existing routes. ANY is unhashable, so any route containing it (at least in a files lookup) is also unhashable. Here's a minimal test to demonstrate (but to be clear, the same thing happens with the tuple form if it contains an ANY):

from unittest.mock import ANY

import respx


def test_example():
    # This first one succeeds because there are no existing routes yet, so no comparisons happen.
    respx.post("/path", files={"filename1": ANY})
    # This one causes a hash error when a comparison to the existing route is attempted
    respx.post("/path", files={"filename2": ANY})

Output:

F                                                                                                                                                                                                        [100%]
=================================================================================================== FAILURES ===================================================================================================
_________________________________________________________________________________________________ test_example _________________________________________________________________________________________________

    def test_example():
        # This first one succeeds because there are no existing routes yet, so no comparisons happen.
        respx.post("/path", files={"filename1": ANY})
        # This one causes a hash error when a comparison to the existing route is attempted
>       respx.post("/path", files={"filename2": ANY})

tests/test_example.py:10:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
.venv/lib/python3.12/site-packages/respx/api.py:81: in post
    return mock.post(url, name=name, **lookups)
.venv/lib/python3.12/site-packages/respx/router.py:182: in post
    return self.request(method="POST", url=url, name=name, **lookups)
.venv/lib/python3.12/site-packages/respx/router.py:164: in request
    return self.route(method=method, url=url, name=name, **lookups)
.venv/lib/python3.12/site-packages/respx/router.py:132: in route
    return self.add(route, name=name)
.venv/lib/python3.12/site-packages/respx/router.py:145: in add
    route = self.routes.add(route, name=name)
.venv/lib/python3.12/site-packages/respx/models.py:480: in add
    if route in self._routes:
.venv/lib/python3.12/site-packages/respx/models.py:149: in __eq__
    return self.pattern == other.pattern
.venv/lib/python3.12/site-packages/respx/patterns.py:133: in __eq__
    return hash(self) == hash(other)
.venv/lib/python3.12/site-packages/respx/patterns.py:130: in __hash__
    return hash((self.__class__, self.lookup, self.value))
.venv/lib/python3.12/site-packages/respx/patterns.py:130: in __hash__
    return hash((self.__class__, self.lookup, self.value))
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <Files contains {'filename1': (<ANY>, <ANY>)}>

    def __hash__(self):
>       return hash((self.__class__, self.lookup, self._multi_items(self.value)))
E       TypeError: unhashable type: '_ANY'

.venv/lib/python3.12/site-packages/respx/patterns.py:306: TypeError
=========================================================================================== short test summary info ============================================================================================
FAILED tests/test_example.py::test_example - TypeError: unhashable type: '_ANY'
1 failed in 0.18s

Note that this problem doesn't appear to strike when using ANY with a params lookup instead of files, so however it's handled there may be applicable here.

mikenerone avatar Nov 15 '24 04:11 mikenerone

Thanks for reporting .. there are tests with ANY, will try and add your specific case and see what breaks

lundberg avatar Dec 19 '24 11:12 lundberg

I found the problem and it probably affected headers, params and data patterns as well, which uses the same MultiItemsMixin base class.

@mikenerone, if possible, please try #289 before I merge.

lundberg avatar Jan 21 '25 08:01 lundberg

@lundberg I no longer have my original situation where this cropped up, but I constructed this test to confirm the fix (via the pytest fixture):

def test_example(respx_mock):
    respx_mock.post("/path", files={"filename1": ANY}).respond(200)
    respx_mock.post("/path", files={"filename2": ANY}).respond(201)
    assert httpx.post("http://www.blah.com/path", files={"filename2": b""}).status_code == 201
    assert httpx.post("http://www.blah.com/path", files={"filename1": b""}).status_code == 200

I can confirm that this test fails in current master with TypeError: unhashable type: '_ANY', but in the branch from #289 (fix/276), the error does not occur and the matches are distinguished properly.

mikenerone avatar Mar 02 '25 22:03 mikenerone

Just bumped into this problem myself, are you still planning on merging the fixing PR? (Thanks for the great library, makes life a lot easier!)

oelhammouchi avatar Aug 31 '25 21:08 oelhammouchi