http2py 0.1.38__tar.gz → 0.1.40__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {http2py-0.1.38 → http2py-0.1.40}/PKG-INFO +1 -1
- {http2py-0.1.38 → http2py-0.1.40}/http2py/api_pkg_maker.py +3 -2
- {http2py-0.1.38 → http2py-0.1.40}/http2py/client.py +44 -0
- {http2py-0.1.38 → http2py-0.1.40}/http2py/py2request.py +46 -2
- {http2py-0.1.38 → http2py-0.1.40}/pyproject.toml +1 -1
- http2py-0.1.40/tests/test_response_decoding.py +166 -0
- {http2py-0.1.38 → http2py-0.1.40}/tests/test_route_namespace.py +108 -0
- {http2py-0.1.38 → http2py-0.1.40}/.gitattributes +0 -0
- {http2py-0.1.38 → http2py-0.1.40}/.github/workflows/ci.yml +0 -0
- {http2py-0.1.38 → http2py-0.1.40}/.gitignore +0 -0
- {http2py-0.1.38 → http2py-0.1.40}/LICENSE +0 -0
- {http2py-0.1.38 → http2py-0.1.40}/README.md +0 -0
- {http2py-0.1.38 → http2py-0.1.40}/conftest.py +0 -0
- {http2py-0.1.38 → http2py-0.1.40}/http2py/__init__.py +0 -0
- {http2py-0.1.38 → http2py-0.1.40}/http2py/authentication.py +0 -0
- {http2py-0.1.38 → http2py-0.1.40}/http2py/cli_maker.py +0 -0
- {http2py-0.1.38 → http2py-0.1.40}/http2py/constants.py +0 -0
- {http2py-0.1.38 → http2py-0.1.40}/http2py/decorators.py +0 -0
- {http2py-0.1.38 → http2py-0.1.40}/http2py/default_configs.py +0 -0
- {http2py-0.1.38 → http2py-0.1.40}/http2py/example_cli.py +0 -0
- {http2py-0.1.38 → http2py-0.1.40}/http2py/global_state.py +0 -0
- {http2py-0.1.38 → http2py-0.1.40}/http2py/http2py_cls.py +0 -0
- {http2py-0.1.38 → http2py-0.1.40}/http2py/resources/module_maker/func_tpl.txt +0 -0
- {http2py-0.1.38 → http2py-0.1.40}/http2py/testing_utils.py +0 -0
- {http2py-0.1.38 → http2py-0.1.40}/http2py/tests/__init__.py +0 -0
- {http2py-0.1.38 → http2py-0.1.40}/http2py/tests/api_pkg_maker_test.py +0 -0
- {http2py-0.1.38 → http2py-0.1.40}/http2py/tests/conftest.py +0 -0
- {http2py-0.1.38 → http2py-0.1.40}/http2py/tests/test_requirements.txt +0 -0
- {http2py-0.1.38 → http2py-0.1.40}/http2py/util.py +0 -0
- {http2py-0.1.38 → http2py-0.1.40}/tests/cli_goldens/mk_cli.json +0 -0
- {http2py-0.1.38 → http2py-0.1.40}/tests/spec_fixture.py +0 -0
- {http2py-0.1.38 → http2py-0.1.40}/tests/test_binary_payloads.py +0 -0
- {http2py-0.1.38 → http2py-0.1.40}/tests/test_ci_collection_contract.py +0 -0
- {http2py-0.1.38 → http2py-0.1.40}/tests/test_cli_surface.py +0 -0
- {http2py-0.1.38 → http2py-0.1.40}/tests/test_error_statuses.py +0 -0
- {http2py-0.1.38 → http2py-0.1.40}/tests/test_generated_clients.py +0 -0
- {http2py-0.1.38 → http2py-0.1.40}/tests/test_smoke.py +0 -0
|
@@ -84,8 +84,9 @@ def {func_name}(*args, **kwargs):
|
|
|
84
84
|
return api.routes[{func_name!r}](*args, **kwargs)
|
|
85
85
|
"""
|
|
86
86
|
|
|
87
|
-
# The first http2py release
|
|
88
|
-
|
|
87
|
+
# The first http2py release whose route names are what generated packages call:
|
|
88
|
+
# ``HttpClient.routes``, with names unique among routes (``get_a_b_2``) and ``get_root``.
|
|
89
|
+
MIN_HTTP2PY_VERSION = "0.1.40"
|
|
89
90
|
|
|
90
91
|
SETUP_PY = """from setuptools import setup
|
|
91
92
|
|
|
@@ -13,6 +13,7 @@ from i2.errors import AuthorizationError
|
|
|
13
13
|
from http2py.authentication import mk_auth, DFLT_CONFIG_FILENAME
|
|
14
14
|
from http2py.py2request import (
|
|
15
15
|
DFLT_BINARY_DUMPS,
|
|
16
|
+
_route_name,
|
|
16
17
|
mk_method_spec_from_openapi_method_spec,
|
|
17
18
|
mk_request_function,
|
|
18
19
|
)
|
|
@@ -112,6 +113,21 @@ class HttpClient:
|
|
|
112
113
|
if self.refresh_inputs is None:
|
|
113
114
|
raise ValueError("No refresh inputs provided when instantiating HttpClient")
|
|
114
115
|
self.routes = {}
|
|
116
|
+
# Names derived from the spec must avoid every explicit name, and a suffixed
|
|
117
|
+
# name must not take the natural name of a route that comes later.
|
|
118
|
+
self._explicit_route_names, self._natural_route_names = set(), set()
|
|
119
|
+
for pathname, path_spec in openapi_spec["paths"].items():
|
|
120
|
+
for http_method, spec in path_spec.items():
|
|
121
|
+
if not isinstance(spec, dict):
|
|
122
|
+
continue
|
|
123
|
+
if spec.get("x-method_name"):
|
|
124
|
+
self._explicit_route_names.add(spec["x-method_name"])
|
|
125
|
+
else:
|
|
126
|
+
self._natural_route_names.add(
|
|
127
|
+
_route_name(
|
|
128
|
+
spec, http_method, pathname, self.base_url + pathname
|
|
129
|
+
)
|
|
130
|
+
)
|
|
115
131
|
for pathname, path_spec in openapi_spec["paths"].items():
|
|
116
132
|
url_template = self.base_url + pathname
|
|
117
133
|
for http_method, openapi_method_spec in path_spec.items():
|
|
@@ -160,6 +176,10 @@ class HttpClient:
|
|
|
160
176
|
content_type=content_type,
|
|
161
177
|
pathname=pathname,
|
|
162
178
|
)
|
|
179
|
+
if not openapi_method_spec.get("x-method_name"):
|
|
180
|
+
method_spec["method_name"] = self._unique_derived_name(
|
|
181
|
+
method_spec["method_name"], url_template
|
|
182
|
+
)
|
|
163
183
|
func = mk_request_function(
|
|
164
184
|
method_spec,
|
|
165
185
|
dispatch=self.handle_request,
|
|
@@ -191,6 +211,30 @@ class HttpClient:
|
|
|
191
211
|
stacklevel=3,
|
|
192
212
|
)
|
|
193
213
|
|
|
214
|
+
def _unique_derived_name(self, name, url_template):
|
|
215
|
+
"""``name``, a name derived from the spec (operationId, or method and path),
|
|
216
|
+
made unique among the routes so far and the explicit ``x-method_name`` ones: the
|
|
217
|
+
first route keeps it, later ones get ``_2``, ``_3``, ... in spec order (so
|
|
218
|
+
a route added before another with the same name renames the other; give
|
|
219
|
+
routes an ``x-method_name`` to pin their names). Explicit names are never
|
|
220
|
+
changed. A client that skips ``__init__`` knows no explicit names yet."""
|
|
221
|
+
taken = set(getattr(self, "routes", ())) | getattr(
|
|
222
|
+
self, "_explicit_route_names", set()
|
|
223
|
+
)
|
|
224
|
+
if name not in taken:
|
|
225
|
+
return name
|
|
226
|
+
taken |= getattr(self, "_natural_route_names", set())
|
|
227
|
+
n = 2
|
|
228
|
+
while f"{name}_{n}" in taken:
|
|
229
|
+
n += 1
|
|
230
|
+
unique = f"{name}_{n}"
|
|
231
|
+
warnings.warn(
|
|
232
|
+
f"Another route is already named {name!r}: {url_template!r} is named "
|
|
233
|
+
f"{unique!r} (give each route its own x-method_name to choose).",
|
|
234
|
+
stacklevel=4,
|
|
235
|
+
)
|
|
236
|
+
return unique
|
|
237
|
+
|
|
194
238
|
def _can_be_route_attribute(self, name):
|
|
195
239
|
"""Whether a route called ``name`` may be set as an attribute of the client:
|
|
196
240
|
the name is public, and is not an attribute the client works with already
|
|
@@ -200,6 +200,11 @@ def mk_request_function(
|
|
|
200
200
|
|
|
201
201
|
original_spec = method_spec
|
|
202
202
|
method_spec = method_spec.copy() # make a copy
|
|
203
|
+
if "url_template" in method_spec:
|
|
204
|
+
check_url_template(
|
|
205
|
+
method_spec["url_template"],
|
|
206
|
+
query_arg_names=_ensure_list(method_spec.get("query_arg_names", [])),
|
|
207
|
+
)
|
|
203
208
|
method_spec["input_trans"] = method_spec.get("input_trans", None) or {}
|
|
204
209
|
|
|
205
210
|
request_kwargs = method_spec.get("request_kwargs", {}).copy()
|
|
@@ -285,7 +290,7 @@ def mk_request_function(
|
|
|
285
290
|
_request_kwargs["data"] = json.dumps(json_kwargs)
|
|
286
291
|
elif content_type == BINARY_CONTENT_TYPE:
|
|
287
292
|
_request_kwargs["data"] = binary_dumps(kwargs)
|
|
288
|
-
|
|
293
|
+
if content_type == FORM_CONTENT_TYPE: # (it used to be sent with GET only)
|
|
289
294
|
fields = json.dumps(json_kwargs).encode("utf-8")
|
|
290
295
|
binaries = {k: (f"{k}.bin", v) for k, v in binary_kwargs.items()}
|
|
291
296
|
files_data = dict(binaries, __fields=fields)
|
|
@@ -502,6 +507,44 @@ def _difference_conserving_order(a, b):
|
|
|
502
507
|
return [x for x in a if x not in b]
|
|
503
508
|
|
|
504
509
|
|
|
510
|
+
def _reads_into_arguments(field_name: str) -> bool:
|
|
511
|
+
"""Whether a ``str.format`` field looks up an attribute or item of an argument;
|
|
512
|
+
any other name (``user-id``, ``0``...) only selects an argument."""
|
|
513
|
+
return "." in field_name or "[" in field_name
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def check_url_template(url_template: str, *, query_arg_names=()) -> str:
|
|
517
|
+
"""Return ``url_template`` if no placeholder in it, nor any of the
|
|
518
|
+
``query_arg_names`` (appended as ``?name={name}`` when calling), reads into an
|
|
519
|
+
argument.
|
|
520
|
+
|
|
521
|
+
A template is filled with ``str.format(**arguments)``, and it may come from a spec
|
|
522
|
+
fetched from a server. ``{a.__class__}`` or ``{a[0]}`` would read attributes or
|
|
523
|
+
items of the caller's arguments into the URL, so they are refused. Other names,
|
|
524
|
+
such as OpenAPI's hyphenated ``{user-id}``, are fine.
|
|
525
|
+
|
|
526
|
+
>>> check_url_template('http://h/u/{user-id}?q={q}')
|
|
527
|
+
'http://h/u/{user-id}?q={q}'
|
|
528
|
+
>>> check_url_template('http://h/{a.__init__.__globals__}')
|
|
529
|
+
Traceback (most recent call last):
|
|
530
|
+
...
|
|
531
|
+
ValueError: URL template placeholder '{a.__init__.__globals__}' reads into an argument
|
|
532
|
+
"""
|
|
533
|
+
fields = [
|
|
534
|
+
(field_name, format_spec)
|
|
535
|
+
for _, field_name, format_spec, _ in str_formatter.parse(url_template)
|
|
536
|
+
if field_name is not None
|
|
537
|
+
]
|
|
538
|
+
fields += [(name, "") for name in query_arg_names]
|
|
539
|
+
for field_name, format_spec in fields:
|
|
540
|
+
if _reads_into_arguments(field_name) or "{" in (format_spec or ""):
|
|
541
|
+
raise ValueError(
|
|
542
|
+
f"URL template placeholder {'{' + field_name + '}'!r} reads into an "
|
|
543
|
+
"argument"
|
|
544
|
+
)
|
|
545
|
+
return url_template
|
|
546
|
+
|
|
547
|
+
|
|
505
548
|
def extract_format_names(string_template):
|
|
506
549
|
"""
|
|
507
550
|
Extracts the names of the fields in a string template, returning a list of
|
|
@@ -630,7 +673,8 @@ def _route_name(openapi_method_spec, method, pathname, url_template):
|
|
|
630
673
|
name = _identifier(openapi_method_spec.get("operationId") or "")
|
|
631
674
|
if pathname is None:
|
|
632
675
|
pathname = urlparse(url_template).path
|
|
633
|
-
|
|
676
|
+
# the root path would be named after the bare method (``get``)
|
|
677
|
+
return name or default_path_to_func_name(method, pathname.strip("/") or "root")
|
|
634
678
|
|
|
635
679
|
|
|
636
680
|
def mk_method_spec_from_openapi_method_spec(
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""How responses are decoded (``http2py.decorators``) and how a client's requests are
|
|
2
|
+
built from a spec it may have fetched from a server (``mk_request_function``)."""
|
|
3
|
+
|
|
4
|
+
import pickle
|
|
5
|
+
import warnings
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from http2py.decorators import (
|
|
10
|
+
BinaryResponseNotDecoded,
|
|
11
|
+
check_loads,
|
|
12
|
+
handle_binary_resp,
|
|
13
|
+
unsafe_pickle_loads,
|
|
14
|
+
)
|
|
15
|
+
from http2py.py2request import mk_request_function
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Resp:
|
|
19
|
+
def __init__(self, content=b"", *, status_code=200, text="", reason="OK"):
|
|
20
|
+
self.content, self.status_code, self.text, self.reason = (
|
|
21
|
+
content,
|
|
22
|
+
status_code,
|
|
23
|
+
text,
|
|
24
|
+
reason,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _identity(x):
|
|
29
|
+
return x
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_binary_response_needs_an_explicit_decoder():
|
|
33
|
+
with pytest.raises(BinaryResponseNotDecoded):
|
|
34
|
+
handle_binary_resp(_identity)(Resp(pickle.dumps(1)))
|
|
35
|
+
assert handle_binary_resp(_identity, loads=bytes)(Resp(b"raw")) == b"raw"
|
|
36
|
+
trusted = handle_binary_resp(_identity, loads=unsafe_pickle_loads)
|
|
37
|
+
assert trusted(Resp(pickle.dumps({"a": 1}))) == {"a": 1}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_check_loads():
|
|
41
|
+
check_loads(None)
|
|
42
|
+
check_loads(bytes)
|
|
43
|
+
with pytest.raises(TypeError):
|
|
44
|
+
check_loads("pickle.loads")
|
|
45
|
+
with pytest.warns(UserWarning, match="unsafe_pickle_loads"):
|
|
46
|
+
check_loads(pickle.loads)
|
|
47
|
+
with warnings.catch_warnings():
|
|
48
|
+
warnings.simplefilter("error")
|
|
49
|
+
check_loads(unsafe_pickle_loads) # the explicit opt-in does not warn
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_non_200_binary_response_raises_the_mapped_error():
|
|
53
|
+
from i2.errors import NotFoundError
|
|
54
|
+
|
|
55
|
+
with pytest.raises(NotFoundError):
|
|
56
|
+
handle_binary_resp(_identity, loads=bytes)(Resp(status_code=404, text="no"))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _captured_request(method_spec, **call_kwargs):
|
|
60
|
+
"""The (method, url, request kwargs) a request function would dispatch."""
|
|
61
|
+
calls = []
|
|
62
|
+
|
|
63
|
+
def dispatch(method, url, **kwargs):
|
|
64
|
+
calls.append((method, url, kwargs))
|
|
65
|
+
return Resp(b"{}")
|
|
66
|
+
|
|
67
|
+
func = mk_request_function(method_spec, dispatch=dispatch)
|
|
68
|
+
func(None, **call_kwargs)
|
|
69
|
+
return calls[0]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_binary_request_bodies_use_binary_dumps():
|
|
73
|
+
spec = {
|
|
74
|
+
"method": "POST",
|
|
75
|
+
"url_template": "http://h/f",
|
|
76
|
+
"body_arg_names": ["x"],
|
|
77
|
+
"content_type": "application/octet-stream",
|
|
78
|
+
"output_trans": _identity,
|
|
79
|
+
}
|
|
80
|
+
_, _, kwargs = _captured_request(spec, x=1)
|
|
81
|
+
assert pickle.loads(kwargs["data"]) == {"x": 1}
|
|
82
|
+
calls = []
|
|
83
|
+
func = mk_request_function(
|
|
84
|
+
spec,
|
|
85
|
+
dispatch=lambda m, u, **kw: calls.append(kw) or Resp(),
|
|
86
|
+
binary_dumps=lambda d: b"custom",
|
|
87
|
+
)
|
|
88
|
+
func(None, x=1)
|
|
89
|
+
assert calls[0]["data"] == b"custom"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_path_args_are_url_encoded():
|
|
93
|
+
spec = {
|
|
94
|
+
"method": "GET",
|
|
95
|
+
"url_template": "http://h/u/{uid}",
|
|
96
|
+
"path_arg_names": ["uid"],
|
|
97
|
+
"output_trans": _identity,
|
|
98
|
+
}
|
|
99
|
+
_, url, _ = _captured_request(spec, uid="a/b c")
|
|
100
|
+
assert url == "http://h/u/a%2Fb%20c"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@pytest.mark.parametrize(
|
|
104
|
+
"template",
|
|
105
|
+
[
|
|
106
|
+
"http://h/{x.__class__}",
|
|
107
|
+
"http://h/{x[0]}",
|
|
108
|
+
"http://h/{x:{x.__class__}}",
|
|
109
|
+
"http://h/{x!r:{y}}",
|
|
110
|
+
],
|
|
111
|
+
)
|
|
112
|
+
def test_url_templates_may_only_name_arguments(template):
|
|
113
|
+
"""A spec served by a remote must not read attributes of the caller's
|
|
114
|
+
arguments into the URL."""
|
|
115
|
+
spec = {"method": "GET", "url_template": template, "path_arg_names": ["x"]}
|
|
116
|
+
with pytest.raises(ValueError, match="reads into an argument"):
|
|
117
|
+
mk_request_function(spec)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_query_arg_names_may_only_name_arguments():
|
|
121
|
+
spec = {
|
|
122
|
+
"method": "GET",
|
|
123
|
+
"url_template": "http://h/x",
|
|
124
|
+
"query_arg_names": ["obj", "obj.token"],
|
|
125
|
+
}
|
|
126
|
+
with pytest.raises(ValueError, match="reads into an argument"):
|
|
127
|
+
mk_request_function(spec)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_hyphenated_placeholders_still_work():
|
|
131
|
+
spec = {"method": "GET", "url_template": "http://h/u/{user-id}"}
|
|
132
|
+
calls = []
|
|
133
|
+
func = mk_request_function(
|
|
134
|
+
spec, dispatch=lambda m, u, **kw: calls.append(u) or Resp(b"")
|
|
135
|
+
)
|
|
136
|
+
func(None, **{"user-id": "42"})
|
|
137
|
+
assert calls == ["http://h/u/42"]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_spec_with_attribute_placeholder_is_refused_by_the_client():
|
|
141
|
+
import copy
|
|
142
|
+
import sys
|
|
143
|
+
from pathlib import Path
|
|
144
|
+
|
|
145
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
146
|
+
from spec_fixture import SPEC
|
|
147
|
+
|
|
148
|
+
from http2py import HttpClient
|
|
149
|
+
|
|
150
|
+
spec = copy.deepcopy(SPEC)
|
|
151
|
+
spec["paths"]["/x/{uid.__class__.__mro__}"] = spec["paths"].pop("/u/{uid}/p/{pid}")
|
|
152
|
+
with pytest.raises(ValueError, match="reads into an argument"):
|
|
153
|
+
HttpClient(spec, session_state={"session": object(), "refresh_inputs": {}})
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@pytest.mark.parametrize("method", ["GET", "POST"])
|
|
157
|
+
def test_form_requests_send_their_fields_whatever_the_method(method):
|
|
158
|
+
spec = {
|
|
159
|
+
"method": method,
|
|
160
|
+
"url_template": "http://h/f",
|
|
161
|
+
"body_arg_names": ["x", "blob"],
|
|
162
|
+
"content_type": "multipart/form-data",
|
|
163
|
+
"output_trans": _identity,
|
|
164
|
+
}
|
|
165
|
+
_, _, kwargs = _captured_request(spec, x=1, blob=b"\x00")
|
|
166
|
+
assert set(kwargs["files"]) == {"blob", "__fields"}
|
|
@@ -83,6 +83,7 @@ def test_cli_calls_the_route_not_the_client_attribute(monkeypatch):
|
|
|
83
83
|
spec = _spec_naming_plain("ensure_login")
|
|
84
84
|
with pytest.warns(UserWarning):
|
|
85
85
|
route = _client(spec).routes["ensure_login"]
|
|
86
|
+
|
|
86
87
|
class Sent(Exception):
|
|
87
88
|
pass
|
|
88
89
|
|
|
@@ -162,3 +163,110 @@ def test_unusable_operation_id_falls_back_to_method_and_path(operation_id):
|
|
|
162
163
|
spec = _spec_without_names()
|
|
163
164
|
spec["paths"]["/plain"]["get"]["operationId"] = operation_id
|
|
164
165
|
assert "get_plain" in _client(spec).routes
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def test_a_second_route_of_the_same_name_replaces_the_first_with_a_warning():
|
|
169
|
+
spec = copy.deepcopy(SPEC)
|
|
170
|
+
other = copy.deepcopy(spec["paths"]["/plain"])
|
|
171
|
+
spec["paths"]["/plain2"] = other
|
|
172
|
+
with pytest.warns(UserWarning, match="Two routes are named"):
|
|
173
|
+
client = _client(spec)
|
|
174
|
+
name = other["get"]["x-method_name"]
|
|
175
|
+
assert client.routes[name].method_spec["url_template"].endswith("/plain2")
|
|
176
|
+
assert getattr(client, name) is client.routes[name]
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _spec_with_routes(*routes):
|
|
180
|
+
"""A spec whose routes are ``(path, operationId or None, x-method_name or None)``."""
|
|
181
|
+
spec = copy.deepcopy(SPEC)
|
|
182
|
+
template = spec["paths"]["/plain"]["get"]
|
|
183
|
+
spec["paths"] = {}
|
|
184
|
+
for path, operation_id, method_name in routes:
|
|
185
|
+
op = copy.deepcopy(template)
|
|
186
|
+
op.pop("x-method_name", None)
|
|
187
|
+
if operation_id:
|
|
188
|
+
op["operationId"] = operation_id
|
|
189
|
+
if method_name:
|
|
190
|
+
op["x-method_name"] = method_name
|
|
191
|
+
spec["paths"][path] = {"get": op}
|
|
192
|
+
return spec
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_derived_names_that_collide_are_suffixed_in_spec_order():
|
|
196
|
+
# i2mint/http2py#28
|
|
197
|
+
spec = _spec_with_routes(
|
|
198
|
+
("/a-b", None, None),
|
|
199
|
+
("/a_b", None, None),
|
|
200
|
+
("/a/b", None, None),
|
|
201
|
+
("/u/{id}", None, None),
|
|
202
|
+
("/u/id", None, None),
|
|
203
|
+
("/x", "list-things", None),
|
|
204
|
+
("/y", "list_things", None),
|
|
205
|
+
("/z", "get_zz", None),
|
|
206
|
+
("/zz", None, None),
|
|
207
|
+
)
|
|
208
|
+
with pytest.warns(UserWarning, match="get_a_b_2"):
|
|
209
|
+
client = _client(spec)
|
|
210
|
+
assert list(client.routes) == [
|
|
211
|
+
"get_a_b",
|
|
212
|
+
"get_a_b_2",
|
|
213
|
+
"get_a_b_3",
|
|
214
|
+
"get_u_id",
|
|
215
|
+
"get_u_id_2",
|
|
216
|
+
"list_things",
|
|
217
|
+
"list_things_2",
|
|
218
|
+
"get_zz",
|
|
219
|
+
"get_zz_2",
|
|
220
|
+
]
|
|
221
|
+
for name, route in client.routes.items():
|
|
222
|
+
assert route.__name__ == name and getattr(client, name) is route
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def test_explicit_names_are_never_suffixed_and_keep_their_name_over_derived():
|
|
226
|
+
spec = _spec_with_routes(
|
|
227
|
+
("/a-b", None, None), # derived get_a_b, but an explicit one claims it
|
|
228
|
+
("/other", None, "get_a_b"),
|
|
229
|
+
)
|
|
230
|
+
client = _client(spec)
|
|
231
|
+
assert list(client.routes) == ["get_a_b_2", "get_a_b"]
|
|
232
|
+
assert client.routes["get_a_b"].method_spec["url_template"].endswith("/other")
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def test_two_explicit_names_that_collide_still_warn_and_the_last_wins():
|
|
236
|
+
spec = _spec_with_routes(("/p", None, "same"), ("/q", None, "same"))
|
|
237
|
+
with pytest.warns(UserWarning, match="Two routes are named 'same'"):
|
|
238
|
+
client = _client(spec)
|
|
239
|
+
assert list(client.routes) == ["same"]
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def test_api_pkg_files_keeps_every_colliding_route():
|
|
243
|
+
spec = _spec_with_routes(("/a-b", None, None), ("/a_b", None, None))
|
|
244
|
+
with pytest.warns(UserWarning):
|
|
245
|
+
files = api_pkg_files(spec, pkg_name="genpkg_d")
|
|
246
|
+
funcs = files["genpkg_d/funcs.py"]
|
|
247
|
+
assert "def get_a_b(" in funcs and "def get_a_b_2(" in funcs
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def test_root_path_is_not_named_after_the_bare_method():
|
|
251
|
+
client = _client(_spec_with_routes(("/", None, None)))
|
|
252
|
+
assert list(client.routes) == ["get_root"]
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def test_a_suffix_does_not_take_the_natural_name_of_a_later_route():
|
|
256
|
+
client = _client(
|
|
257
|
+
_spec_with_routes(("/a-b", None, None), ("/a_b", None, None), ("/a_b_2", None, None))
|
|
258
|
+
)
|
|
259
|
+
assert list(client.routes) == ["get_a_b", "get_a_b_3", "get_a_b_2"]
|
|
260
|
+
assert client.routes["get_a_b_2"].method_spec["url_template"].endswith("/a_b_2")
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def test_root_path_and_a_root_route_are_told_apart():
|
|
264
|
+
client = _client(_spec_with_routes(("/root", None, None), ("/", None, None)))
|
|
265
|
+
assert sorted(client.routes) == ["get_root", "get_root_2"]
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def test_openapi_to_py_keeps_the_bare_root_name():
|
|
269
|
+
from http2py.py2request import openapi_to_py
|
|
270
|
+
|
|
271
|
+
spec = _spec_with_routes(("/", None, None), ("/root", None, None))
|
|
272
|
+
assert sorted(openapi_to_py(spec)) == ["get", "get_root"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|