Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Calls info in BaseResponse #664

Merged
merged 17 commits into from
Oct 30, 2023
Merged

Conversation

zeezdev
Copy link
Contributor

@zeezdev zeezdev commented Aug 17, 2023

Sometimes it is more convenient to get information about the calls made directly from the mockend request, instead of calculating the order of your calls in the global list (responses.calls[n]).

What do you think of such a possibility?

@zeezdev zeezdev marked this pull request as ready for review August 17, 2023 21:33
@codecov
Copy link

codecov bot commented Aug 22, 2023

Codecov Report

All modified and coverable lines are covered by tests ✅

Files Coverage Δ
responses/__init__.py 100.00% <100.00%> (ø)
responses/tests/test_responses.py 100.00% <100.00%> (ø)

📢 Thoughts on this report? Let us know!.

self._calls.add(request, response)
match.calls.add_call(self._calls[next_index])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you have the request and response, why do you need to read out of self._calls? Wouldn't match.calls.add() work?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, that's what I did initially. But then an idea came up:

  • reuse an already created call from the "global" list self._calls
  • have the same call instance in the global list and in the individual response list (match.calls)

(using match.calls.add() we'll create a separate instance of the same call)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, wanting to reuse the objects seems reasonable to me. What do you think of something like

call = Call(request, response)
self._calls.add_call(call)
match.calls.add_call(call)

That would save us having to index/count call objects.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, thanks, Let me try...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I replaced the index with

        call = Call(request, response)
        self._calls.add_call(call)
        match.calls.add_call(call)

and added an example of usage Response.calls into the README.

Copy link
Collaborator

@beliaev-maksim beliaev-maksim left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, CHANGELOG entry is missing

README.rst Outdated Show resolved Hide resolved
README.rst Outdated Show resolved Hide resolved
README.rst Outdated
Comment on lines 1108 to 1126
assert len(responses.calls) == 3 # total calls count
# Assert calls to the 1st resource ("http://www.example.com"):
assert rsp.call_count == 2
assert rsp.calls[0] is responses.calls[0]
assert rsp.calls[1] is responses.calls[1]
request = rsp.calls[0].request
assert request.url == "http://www.example.com/"
assert request.method == "GET"
request = rsp.calls[1].request
assert request.url == "http://www.example.com/?hello=world"
assert request.method == "GET"
# Assert calls to the 2nd resource (http://foo.bar):
assert rsp2.call_count == 1
assert rsp2.calls[0] is responses.calls[2]
request = rsp2.calls[0].request
assert request.url == "http://foo.bar/42/"
assert request.method == "PUT"
request_payload = json.loads(request.body)
assert request_payload == {"name": "Bazz"}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could be sparse to increase readability

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example has been replaced. Is this still relevant?

@zeezdev
Copy link
Contributor Author

zeezdev commented Sep 26, 2023

also, CHANGELOG entry is missing

Added the CHANGELOG entry.

Copy link
Collaborator

@beliaev-maksim beliaev-maksim left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@markstory
Copy link
Member

This is looking good, a few small formatting issues in the README that need to be fixed up before this can be merged.

@zeezdev
Copy link
Contributor Author

zeezdev commented Oct 20, 2023

This is looking good, a few small formatting issues in the README that need to be fixed up before this can be merged.

Fixed.

@zeezdev
Copy link
Contributor Author

zeezdev commented Oct 20, 2023

It looks like mypy problems is related to v1.6.1 that released several days ago.

I added several ignore comments related to my changes and old code. Please review again.

@@ -13,7 +13,7 @@
from urllib.parse import urlparse

from requests import PreparedRequest
from requests.packages.urllib3.util.url import parse_url
from requests.packages.urllib3.util.url import parse_url # type: ignore[import-untyped]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is that ?

@@ -252,11 +258,11 @@ def __len__(self) -> int:
return len(self._calls)

@overload
def __getitem__(self, idx: int) -> Call:
def __getitem__(self, idx: int) -> Call: # type: ignore[misc]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

too many ignores, something looks to be incorrect

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed in #684

first merge #684, then current PR and we are good to ship the release

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merged, thanks!

# Conflicts:
#	responses/__init__.py
#	responses/matchers.py
Comment on lines 2073 to 2077
assert rsp.call_count == 2
request = rsp.calls[0].request
assert request.url == "http://www.example.com/"
assert request.method == "GET"
request = rsp.calls[1].request
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can make it a bit more readable by enumerating request objects as well

Suggested change
assert rsp.call_count == 2
request = rsp.calls[0].request
assert request.url == "http://www.example.com/"
assert request.method == "GET"
request = rsp.calls[1].request
assert rsp.call_count == 2
request1 = rsp.calls[0].request
assert request.url == "http://www.example.com/"
assert request.method == "GET"
request2 = rsp.calls[1].request

and so on

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make sense. Made tests readable. Please take a look

request = rsp2.calls[0].request
assert request.url == "http://www.foo.bar/42/"
assert request.method == "PUT"
request_payload = json.loads(request.body)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what exactly do we test here ?

why do we need to test that request body could be converted?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I changed the test a little)

Here we validate that request rsp2_request1 = rsp2.calls[0].request obtained from the mock's call contains expected payload request_payload = {"name": "Bazz"}, that was used on the put request.

request body could be converted?

PreparedRequest contains only encoded data in the body field, so we decode it to compare with the original data - request_payload.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PreparedRequest contains only encoded data in the body field, so we decode it to compare with the original data - request_payload.

correct, but do we need to check it at all as part of responses ?

we attach request object as is without modification

response.request = request

and in the case of this PR we can skip this check since it does not add a value.

although, you can fire a parallel PR that can add a separate test to validate that we have all the required data is attached to a request
we still will need to discuss if that is required or not but this we can do in a separate thread

Copy link
Contributor Author

@zeezdev zeezdev Oct 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we attach request object as is without modification

You are right, I missed this moment.

So, in other words, in the current PR it is enough to test that calls of local mocks are identical to calls of the global list without deep validation of rsp.calls[N].request & rsp.calls[N].response?
I.e. merge all new tests (test_response_and_requests_mock_calls_are_equal, test_response_call_request, test_response_call_response) to something like this:

def test_response_calls_and_registry_calls_are_equal():
    @responses.activate
    def run():
        rsp1 = responses.add(responses.GET, "http://www.example.com")
        rsp2 = responses.add(responses.GET, "http://www.example.com/1")

        requests.get("http://www.example.com")
        requests.get("http://www.example.com/1")
        requests.get("http://www.example.com")

        assert len(responses.calls) == len(rsp1.calls) + len(rsp2.calls)
        assert rsp1.call_count == 2
        assert len(rsp1.calls) == 2
        assert rsp1.calls[0] is responses.calls[0]
        assert rsp1.calls[1] is responses.calls[2]
        assert rsp2.call_count == 1
        assert len(rsp2.calls) == 1
        assert rsp2.calls[0] is responses.calls[1]

    run()
    assert_reset()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@beliaev-maksim and what about this question?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zeezdev sorry, missed this one in a long thread. Yes.
And if for some reason we have missed the tests that ensure the quality of Call objects. Then I would recommend to create separate tests for those and open it in a separate PR.

assert response.content == b"test"
assert response.status_code == 200
assert rsp2.call_count == 1
response = rsp2.calls[0].response
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here, could be enumerated. Something like

Suggested change
response = rsp2.calls[0].response
response2 = rsp2.calls[0].response

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree, done

Copy link
Collaborator

@beliaev-maksim beliaev-maksim left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we are very close to the conclusion

technically all looks good, just need to simplify user facing docs



@responses.activate
def test_assert_calls_on_resp():
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that this example might look very close to what you have in production. But can we unload it a bit and reduce complexity?

we probably still want to show an example of concurrent since this could be one of the main usages, but we do not need all those complicated matrices of settings to show the usage

can you please simplify it to remove additional arguments and have more clear names like
rsp1, rsp2, rsp3

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got you, I'll try to simplify this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

simplified, please take a look

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zeezdev now looks a way better, thank you!

README.rst Outdated
for future in concurrent.futures.as_completed(future_to_uid):
uid = future_to_uid[future]
response = future.result()
print("%s updated with %d status code" % (uid, response.status_code))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please update it with f-string

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done



@responses.activate
def test_assert_calls_on_resp():
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zeezdev now looks a way better, thank you!

@beliaev-maksim
Copy link
Collaborator

@markstory can you please do a final pass here

rsp2 = responses.add(responses.GET, "http://www.example.com/1")
rsp3 = responses.add(
responses.GET, "http://www.example.com/2"
) # won't be requested
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that comment is probably true only with the default registry, or?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, this mocked resource (http://www.example.com/2) isn't requested during the test to explain why we get empty rsp3.calls.
I can remove this if it confuses.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that is fine to keep it for the testing purpose.

I just mean that if the user applies a different registry or we switch the default registry (which is unlikely), then this comment will not be correct

https://github.com/getsentry/responses#ordered-registry

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but this comment relates to the specific scope of the test. And is it relevant in this case?

If I were to initialize the test with
@responses.activate(registry=OrderedRegistry)

then it would be a different case with different expectations, wouldn’t it?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that is more of a note rather than a call for an action :)

I am +1 with the current state of PR

now wait for @markstory to make his round of review

@markstory markstory merged commit bcad0cd into getsentry:master Oct 30, 2023
12 checks passed
@beliaev-maksim
Copy link
Collaborator

@zeezdev great work, thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

4 participants