http2py 0.1.39__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.
Files changed (37) hide show
  1. {http2py-0.1.39 → http2py-0.1.40}/PKG-INFO +1 -1
  2. {http2py-0.1.39 → http2py-0.1.40}/http2py/api_pkg_maker.py +3 -2
  3. {http2py-0.1.39 → http2py-0.1.40}/http2py/client.py +44 -0
  4. {http2py-0.1.39 → http2py-0.1.40}/http2py/py2request.py +2 -1
  5. {http2py-0.1.39 → http2py-0.1.40}/pyproject.toml +1 -1
  6. {http2py-0.1.39 → http2py-0.1.40}/tests/test_route_namespace.py +96 -0
  7. {http2py-0.1.39 → http2py-0.1.40}/.gitattributes +0 -0
  8. {http2py-0.1.39 → http2py-0.1.40}/.github/workflows/ci.yml +0 -0
  9. {http2py-0.1.39 → http2py-0.1.40}/.gitignore +0 -0
  10. {http2py-0.1.39 → http2py-0.1.40}/LICENSE +0 -0
  11. {http2py-0.1.39 → http2py-0.1.40}/README.md +0 -0
  12. {http2py-0.1.39 → http2py-0.1.40}/conftest.py +0 -0
  13. {http2py-0.1.39 → http2py-0.1.40}/http2py/__init__.py +0 -0
  14. {http2py-0.1.39 → http2py-0.1.40}/http2py/authentication.py +0 -0
  15. {http2py-0.1.39 → http2py-0.1.40}/http2py/cli_maker.py +0 -0
  16. {http2py-0.1.39 → http2py-0.1.40}/http2py/constants.py +0 -0
  17. {http2py-0.1.39 → http2py-0.1.40}/http2py/decorators.py +0 -0
  18. {http2py-0.1.39 → http2py-0.1.40}/http2py/default_configs.py +0 -0
  19. {http2py-0.1.39 → http2py-0.1.40}/http2py/example_cli.py +0 -0
  20. {http2py-0.1.39 → http2py-0.1.40}/http2py/global_state.py +0 -0
  21. {http2py-0.1.39 → http2py-0.1.40}/http2py/http2py_cls.py +0 -0
  22. {http2py-0.1.39 → http2py-0.1.40}/http2py/resources/module_maker/func_tpl.txt +0 -0
  23. {http2py-0.1.39 → http2py-0.1.40}/http2py/testing_utils.py +0 -0
  24. {http2py-0.1.39 → http2py-0.1.40}/http2py/tests/__init__.py +0 -0
  25. {http2py-0.1.39 → http2py-0.1.40}/http2py/tests/api_pkg_maker_test.py +0 -0
  26. {http2py-0.1.39 → http2py-0.1.40}/http2py/tests/conftest.py +0 -0
  27. {http2py-0.1.39 → http2py-0.1.40}/http2py/tests/test_requirements.txt +0 -0
  28. {http2py-0.1.39 → http2py-0.1.40}/http2py/util.py +0 -0
  29. {http2py-0.1.39 → http2py-0.1.40}/tests/cli_goldens/mk_cli.json +0 -0
  30. {http2py-0.1.39 → http2py-0.1.40}/tests/spec_fixture.py +0 -0
  31. {http2py-0.1.39 → http2py-0.1.40}/tests/test_binary_payloads.py +0 -0
  32. {http2py-0.1.39 → http2py-0.1.40}/tests/test_ci_collection_contract.py +0 -0
  33. {http2py-0.1.39 → http2py-0.1.40}/tests/test_cli_surface.py +0 -0
  34. {http2py-0.1.39 → http2py-0.1.40}/tests/test_error_statuses.py +0 -0
  35. {http2py-0.1.39 → http2py-0.1.40}/tests/test_generated_clients.py +0 -0
  36. {http2py-0.1.39 → http2py-0.1.40}/tests/test_response_decoding.py +0 -0
  37. {http2py-0.1.39 → http2py-0.1.40}/tests/test_smoke.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: http2py
3
- Version: 0.1.39
3
+ Version: 0.1.40
4
4
  Summary: Tools to create python binders to http web services.
5
5
  Project-URL: Homepage, https://github.com/i2mint/http2py
6
6
  Project-URL: Repository, https://github.com/i2mint/http2py
@@ -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 with ``HttpClient.routes``, which generated packages call.
88
- MIN_HTTP2PY_VERSION = "0.1.37"
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
@@ -673,7 +673,8 @@ def _route_name(openapi_method_spec, method, pathname, url_template):
673
673
  name = _identifier(openapi_method_spec.get("operationId") or "")
674
674
  if pathname is None:
675
675
  pathname = urlparse(url_template).path
676
- return name or default_path_to_func_name(method, pathname)
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")
677
678
 
678
679
 
679
680
  def mk_method_spec_from_openapi_method_spec(
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
 
7
7
  [project]
8
8
  name = "http2py"
9
- version = "0.1.39"
9
+ version = "0.1.40"
10
10
  description = "Tools to create python binders to http web services."
11
11
  readme = "README.md"
12
12
  requires-python = ">=3.10"
@@ -174,3 +174,99 @@ def test_a_second_route_of_the_same_name_replaces_the_first_with_a_warning():
174
174
  name = other["get"]["x-method_name"]
175
175
  assert client.routes[name].method_spec["url_template"].endswith("/plain2")
176
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