Search code examples
pythonpython-requestspython-responses

Tracking down cause of error in unittest/mock


I'm using the Python Responses module (latest v0.23.3) alongside Requests (v2.31.0) in Python 3.11. It's part of a fairly large application, and I'm using Responses to simulate a device which I communicate with via Requests sending simple HTML GET strings e.g. "action=instruction&pin=5".

My standard "Device" module (containing a Device class) has a send_request method, which basically does the following:

            r = requests.post(f'http://{self.ip}:{self.node_port}/{target}',
                          data=data,
                          timeout=self.server_timeout,
                          headers={'Connection': 'close'})

where data is a dict containing the key-value pairs needed to make up those GET strings. The only other stuff in that send_request method is some logging and exception handling.

I then have a "Device_Sim" module (and Device_Sim class) which extends the "Device" class, and overrides the send_request method like this:

    @responses.activate
def send_request(self, target: str, data: dict):
    """ Intercept calls to Device.send_request, implementing a Responses library callback for simulated responses.

    All parameters are the same as in Device.send_request, and responses are simulated so should be the same.
    However, rather than sending a message to the Device itself, the Responses library redirects that message to
    the handle_post_requests method below, giving full control over how the Simulated Device behaves and responds
    to any command.

    Note that the Device will only respond if powered on
    """

    if self.sim_actual_power_on:
        # The callback here will override any Request library action, preventing the Request library from attempting
        # communication via the network, and passing it directly to the callback method below.  This is only used
        # for simulated Devices - allowing easy testing of behaviours without needing "real" test Devices.
        responses.add_callback(responses.POST,
                               url=f'http://{self.ip}:{self.node_port}/{target}',
                               callback=self.handle_post_requests,
                               content_type='text/plain')

    # Once the callback is setup, call the parent send_request() method as normal to handle everything else.
    super().send_request(target=target, data=data)

The above is the only place I use the Responses module.

When I run the code, I intermittently get an exception which creates the following Traceback:

Traceback (most recent call last):
  File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/threading.py", line 975, in run
    self._target(*self._args, **self._kwargs)
  File "/Users/dave/Documents/Development/KDeHome/Python/devices.py", line 522, in send_query
    return self.send_request(target='query', data=data)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/responses/__init__.py", line 225, in wrapper
    with assert_mock, responses:
  File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/unittest/mock.py", line 1564, in __exit__
    if self.is_local and self.temp_original is not DEFAULT:
       ^^^^^^^^^^^^^
AttributeError: '_patch' object has no attribute 'is_local'. Did you mean: 'has_local'?

Searching for the error and issues with the Responses module have drawn a blank - it doesn't seem to be a specific issue with Responses, as far as I can tell. But I'm also struggling to see what I can have done wrong in my own code - as I said, the above is the only place I use the Responses module, and my code seems fairly standard.


Solution

  • Thanks to the hint from Jon, I explored how Responses works with multiple threads. I couldn't find many good examples, but confirmed that removing the decorator @responses.activate and instead adding a responses context handler within my method creates a separate responses instance for each Device instance, and seems to resolve the issue:

            with responses.RequestsMock() as resp:
    
                resp.add_callback(responses.POST,
                                  url=f'http://{self.ip}:{self.node_port}/{target}',
                                  callback=self.handle_post_requests,
                                  content_type='text/plain')
    
                super().send_request(target=target, data=data)