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.
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)