Skip to content

Commit 12497e4

Browse files
sirosenkurtmckee
andauthored
Improve RegisteredResponse init doc & signature (#1126)
* Improve `RegisteredResponse` init doc & signature `RegisteredResponse.__init__` allowed any keyword arguments via `**kwargs` usage, but would later pass these to `responses.Response` via the `add()` or `replace()` methods, at which point there would be a `TypeError`. This can make it hard to understand what features are being exposed during development. e.g., "Was the parameter name `status` or `status_code`?" To resolve and provide an improved experience for development with `RegisteredResponse`, the init parameters are better documented and the signature is improved to explicitly enumerate supported args. 1. Add `:param ...:` docstring items 2. Explicitly enumerate parameters, removing `**kwargs: Any` 3. Document which parameters come from `responses.Response`, which come from `responses.BaseResponse`, and which ones are omitted (and why) 4. Update `service` and `method` to use Literals to show the values In terms of lost functionality, technically the following three parameters are removed in this change: - auto_calculate_content_length - passthrough - match_querystring However, given that `_testing` has a weaker backwards compatibility contract than the rest of the SDK and there are no known usages for these, this seems like a good change rather than a bad one. We can always add `auto_calculate_content_length` in the future. The other two would require stronger justification, since - match_querystring is deprecated in `responses` - passthrough doesn't seem applicable to any normal uses of `globus_sdk._testing` - we don't forbid anyone from using `responses` directly * Update src/globus_sdk/_testing/models.py Co-authored-by: Kurt McKee <contactme@kurtmckee.org> * Enforce `RegisteredResponse.method` correctness Compare in a test against the options supported by `http.HTTPMethod`, and thereby compare against the strings supported by `responses`. Add `OPTIONS`, `CONNECT`, and `TRACE` for completeness. * Make `RegisteredResponse.match` default to None In order to ensure that we aren't overriding a potentially new default in `responses` by passing `()` explicitly to it, simply default to `None` and only pass a value if it is non-null. Also refine docstrings. --------- Co-authored-by: Kurt McKee <contactme@kurtmckee.org>
1 parent bf6dbf1 commit 12497e4

File tree

2 files changed

+97
-8
lines changed

2 files changed

+97
-8
lines changed

src/globus_sdk/_testing/models.py

Lines changed: 77 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,25 @@ class RegisteredResponse:
1919
A mock response along with descriptive metadata to let a fixture "pass data
2020
forward" to the consuming test cases. (e.g. a ``GET Task`` fixture which
2121
shares the ``task_id`` it uses with consumers via ``.metadata["task_id"]``)
22+
23+
When initializing a ``RegisteredResponse``, you can use ``path`` and ``service``
24+
to describe a path on a Globus service rather than a full URL. The ``metadata``
25+
data container is also globus-sdk specific. Most other parameters are wrappers
26+
over ``responses`` response characteristics.
27+
28+
:param path: Path on the target service or full URL if service is null
29+
:param service: A known service name like ``"transfer"`` or ``"compute"``. This will
30+
be used to deduce the base URL onto which ``path`` should be joined
31+
:param method: A string HTTP Method
32+
:param headers: HTTP headers for the response
33+
:param json: A dict or list structure for a JSON response (mutex with ``body``)
34+
:param body: A string response body (mutex with ``json``)
35+
:param status: The HTTP status code for the response
36+
:param content_type: A Content-Type header value for the response
37+
:param match: A tuple or list of ``responses`` matchers
38+
:param metadata: A dict of data to store on the response, which allows the usage
39+
site which declares the response to pass information forward to the site which
40+
activates and tests against the response.
2241
"""
2342

2443
_url_map = {
@@ -40,14 +59,54 @@ class RegisteredResponse:
4059
def __init__(
4160
self,
4261
*,
62+
# path and service are glbous-sdk specific
63+
# in `responses`, these are just `url`
4364
path: str,
44-
service: str | None = None,
45-
method: str = responses.GET,
65+
service: (
66+
Literal[
67+
"auth",
68+
"nexus",
69+
"transfer",
70+
"search",
71+
"gcs",
72+
"groups",
73+
"timer",
74+
"flows",
75+
"compute",
76+
]
77+
| None
78+
) = None,
79+
# method will be passed through to `responses.Response`, so we
80+
# support all of the values which it supports
81+
method: Literal[
82+
"GET",
83+
"PUT",
84+
"POST",
85+
"PATCH",
86+
"HEAD",
87+
"DELETE",
88+
"OPTIONS",
89+
"CONNECT",
90+
"TRACE",
91+
] = "GET",
92+
# these parameters are passed through to `response.Response` (or omitted)
93+
body: str | None = None,
94+
content_type: str | None = None,
4695
headers: dict[str, str] | None = None,
47-
metadata: dict[str, t.Any] | None = None,
4896
json: None | list[t.Any] | dict[str, t.Any] = None,
49-
body: str | None = None,
50-
**kwargs: t.Any,
97+
status: int = 200,
98+
stream: bool | None = None,
99+
match: t.Sequence[t.Callable[..., tuple[bool, str]]] | None = None,
100+
# metadata is globus-sdk specific
101+
metadata: dict[str, t.Any] | None = None,
102+
# the following are known parameters to `responses.Response` which
103+
# `RegisteredResponse` does not support:
104+
# - url: calculated from (path, service)
105+
# - auto_calculate_content_length: a bool setting, usually not needed and can
106+
# be achieved in user code via `headers`
107+
# - passthrough: bool setting allowing calls to be emitted to the services
108+
# (undesirable in any ordinary cases)
109+
# - match_querystring: legacy param which has been replaced with `match`
51110
) -> None:
52111
self.service = service
53112

@@ -69,12 +128,17 @@ def __init__(
69128
# correctly -- method matching is case sensitive but we don't need to expose the
70129
# possibility of a non-uppercase method
71130
self.method = method.upper()
72-
self.json = json
131+
73132
self.body = body
133+
self.content_type = content_type
74134
self.headers = headers
135+
self.json = json
136+
self.status = status
137+
self.stream = stream
138+
139+
self.match = match
75140

76141
self._metadata = metadata
77-
self.kwargs = kwargs
78142

79143
self.parent: ResponseSet | ResponseList | None = None
80144

@@ -94,13 +158,18 @@ def _add_or_replace(
94158
) -> RegisteredResponse:
95159
kwargs: dict[str, t.Any] = {
96160
"headers": self.headers,
161+
"status": self.status,
162+
"stream": self.stream,
97163
"match_querystring": None,
98-
**self.kwargs,
99164
}
100165
if self.json is not None:
101166
kwargs["json"] = self.json
102167
if self.body is not None:
103168
kwargs["body"] = self.body
169+
if self.content_type is not None:
170+
kwargs["content_type"] = self.content_type
171+
if self.match is not None:
172+
kwargs["match"] = self.match
104173

105174
if requests_mock is None:
106175
use_requests_mock: responses.RequestsMock | types.ModuleType = responses
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import http
2+
import sys
3+
import typing as t
4+
5+
import pytest
6+
7+
from globus_sdk._testing import RegisteredResponse
8+
9+
10+
@pytest.mark.skipif(
11+
sys.version_info < (3, 11), reason="test requires http.HTTPMethod (Python 3.11+)"
12+
)
13+
def test_registered_response_method_literal_type_is_correct():
14+
all_known_methods = [m.value for m in http.HTTPMethod]
15+
init_signature = t.get_type_hints(RegisteredResponse.__init__)
16+
17+
method_arg_type = init_signature["method"]
18+
expected_method_arg_type = t.Literal[tuple(all_known_methods)]
19+
20+
assert method_arg_type == expected_method_arg_type

0 commit comments

Comments
 (0)