http2py 0.1.33__tar.gz → 0.1.34__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.33 → http2py-0.1.34}/PKG-INFO +3 -2
- http2py-0.1.34/conftest.py +18 -0
- {http2py-0.1.33 → http2py-0.1.34}/http2py/api_pkg_maker.py +61 -11
- {http2py-0.1.33 → http2py-0.1.34}/http2py/cli_maker.py +36 -8
- {http2py-0.1.33 → http2py-0.1.34}/http2py/example_cli.py +9 -4
- {http2py-0.1.33 → http2py-0.1.34}/pyproject.toml +16 -10
- http2py-0.1.34/tests/cli_goldens/mk_cli.json +10 -0
- http2py-0.1.34/tests/spec_fixture.py +89 -0
- {http2py-0.1.33 → http2py-0.1.34}/tests/test_ci_collection_contract.py +95 -19
- http2py-0.1.34/tests/test_cli_surface.py +171 -0
- http2py-0.1.33/conftest.py +0 -21
- {http2py-0.1.33 → http2py-0.1.34}/.gitattributes +0 -0
- {http2py-0.1.33 → http2py-0.1.34}/.github/workflows/ci.yml +0 -0
- {http2py-0.1.33 → http2py-0.1.34}/.gitignore +0 -0
- {http2py-0.1.33 → http2py-0.1.34}/LICENSE +0 -0
- {http2py-0.1.33 → http2py-0.1.34}/README.md +0 -0
- {http2py-0.1.33 → http2py-0.1.34}/http2py/__init__.py +0 -0
- {http2py-0.1.33 → http2py-0.1.34}/http2py/authentication.py +0 -0
- {http2py-0.1.33 → http2py-0.1.34}/http2py/client.py +0 -0
- {http2py-0.1.33 → http2py-0.1.34}/http2py/constants.py +0 -0
- {http2py-0.1.33 → http2py-0.1.34}/http2py/decorators.py +0 -0
- {http2py-0.1.33 → http2py-0.1.34}/http2py/default_configs.py +0 -0
- {http2py-0.1.33 → http2py-0.1.34}/http2py/global_state.py +0 -0
- {http2py-0.1.33 → http2py-0.1.34}/http2py/http2py_cls.py +0 -0
- {http2py-0.1.33 → http2py-0.1.34}/http2py/py2request.py +0 -0
- {http2py-0.1.33 → http2py-0.1.34}/http2py/resources/module_maker/func_tpl.txt +0 -0
- {http2py-0.1.33 → http2py-0.1.34}/http2py/testing_utils.py +0 -0
- {http2py-0.1.33 → http2py-0.1.34}/http2py/tests/__init__.py +0 -0
- {http2py-0.1.33 → http2py-0.1.34}/http2py/tests/api_pkg_maker_test.py +0 -0
- {http2py-0.1.33 → http2py-0.1.34}/http2py/tests/conftest.py +0 -0
- {http2py-0.1.33 → http2py-0.1.34}/http2py/tests/test_requirements.txt +0 -0
- {http2py-0.1.33 → http2py-0.1.34}/http2py/util.py +0 -0
- {http2py-0.1.33 → http2py-0.1.34}/tests/test_dependency_workarounds.py +0 -0
- {http2py-0.1.33 → http2py-0.1.34}/tests/test_smoke.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: http2py
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.34
|
|
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
|
|
@@ -16,7 +16,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
16
16
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
17
17
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
18
|
Requires-Python: >=3.10
|
|
19
|
-
Requires-Dist:
|
|
19
|
+
Requires-Dist: cw<0.2,>=0.1.1
|
|
20
20
|
Requires-Dist: dill
|
|
21
21
|
Requires-Dist: glom
|
|
22
22
|
Requires-Dist: i2
|
|
@@ -28,6 +28,7 @@ Provides-Extra: dev
|
|
|
28
28
|
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
29
29
|
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
30
30
|
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: setuptools; extra == 'dev'
|
|
31
32
|
Description-Content-Type: text/markdown
|
|
32
33
|
|
|
33
34
|
**Note: A new, lighterweight, more modern replacement: [ho](https://github.com/i2mint/ho)**
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Pytest configuration for the http2py repository.
|
|
2
|
+
|
|
3
|
+
``http2py/tests`` stands up a real ``py2http`` web service, so it depends on a
|
|
4
|
+
test-only package that is not one of http2py's declared dependencies. Under
|
|
5
|
+
``--doctest-modules`` an unimportable module is not a single failing test: it
|
|
6
|
+
aborts collection for the whole session. So that directory is excluded here.
|
|
7
|
+
|
|
8
|
+
CI excludes the same path via ``[tool.wads.ci.testing].exclude_paths`` in
|
|
9
|
+
pyproject.toml, which the wads ``run-tests-uv`` action turns into ``--ignore``
|
|
10
|
+
flags. Repeating it here is what makes a bare ``pytest`` (no flags, e.g. a local
|
|
11
|
+
run or an editor's test runner) behave the same way as CI.
|
|
12
|
+
``tests/test_ci_collection_contract.py`` asserts the two lists stay in
|
|
13
|
+
agreement, so they cannot drift apart silently.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
collect_ignore = [
|
|
17
|
+
"http2py/tests",
|
|
18
|
+
]
|
|
@@ -3,21 +3,32 @@
|
|
|
3
3
|
Given a spec (or the URL of one), :func:`mk_api_pkg` writes a small source
|
|
4
4
|
distribution whose functions are bound to the service's routes.
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
The sdist is built by running ``setup.py sdist`` in a subprocess. This module
|
|
7
|
+
used to call ``setuptools.sandbox.run_setup``, which modern setuptools no longer
|
|
8
|
+
ships -- so merely *importing* it raised ``ImportError`` and the
|
|
9
|
+
``api-pkg-maker`` console script could not start at all (i2mint/http2py#14).
|
|
10
|
+
The subprocess is also better behaved: ``sandbox.run_setup`` needed the process
|
|
11
|
+
to ``os.chdir`` into the build directory and never came back.
|
|
12
|
+
|
|
13
|
+
``setuptools`` is still needed to *run* :func:`mk_api_pkg`, but it is
|
|
14
|
+
deliberately NOT a declared dependency of http2py: it is needed by this one
|
|
15
|
+
function, not by ``import http2py``, and a virtualenv created by ``uv`` does not
|
|
16
|
+
ship it. :func:`_check_build_backend` turns its absence into an actionable
|
|
17
|
+
message instead of a subprocess exit code.
|
|
10
18
|
"""
|
|
11
19
|
|
|
12
|
-
import argh
|
|
13
20
|
import os
|
|
14
21
|
import shutil
|
|
22
|
+
import subprocess
|
|
23
|
+
import sys
|
|
15
24
|
import tempfile
|
|
16
25
|
from http2py.client import HttpClient
|
|
17
26
|
from datetime import datetime, timezone
|
|
18
|
-
from setuptools import sandbox
|
|
19
27
|
|
|
20
|
-
|
|
28
|
+
#: Where built packages are written. ``expanduser`` rather than
|
|
29
|
+
#: ``os.environ["HOME"]``: the latter is a module-import-time ``KeyError`` on
|
|
30
|
+
#: Windows, which would take the whole package down with it.
|
|
31
|
+
OUTPUT_DIR = os.path.join(os.path.expanduser("~"), "http2py", "api_pkgs")
|
|
21
32
|
|
|
22
33
|
INIT_FILE_TPL = """from .funcs import {funcs}
|
|
23
34
|
"""
|
|
@@ -111,6 +122,8 @@ def mk_api_pkg(
|
|
|
111
122
|
with open(filepath, "w") as file:
|
|
112
123
|
print(content, file=file)
|
|
113
124
|
|
|
125
|
+
_check_build_backend()
|
|
126
|
+
|
|
114
127
|
tempdir = tempfile.mkdtemp()
|
|
115
128
|
try:
|
|
116
129
|
api = HttpClient(openapi_spec=openapi_spec, url=openapi_url)
|
|
@@ -148,8 +161,20 @@ def mk_api_pkg(
|
|
|
148
161
|
filepath=os.path.join(tempdir, f"setup.cfg"), content=setup_cfg_content
|
|
149
162
|
)
|
|
150
163
|
create_file(filepath=os.path.join(tempdir, f"setup.py"), content=SETUP_PY)
|
|
151
|
-
|
|
152
|
-
|
|
164
|
+
build = subprocess.run(
|
|
165
|
+
[sys.executable, "setup.py", "sdist"],
|
|
166
|
+
cwd=tempdir,
|
|
167
|
+
capture_output=True,
|
|
168
|
+
text=True,
|
|
169
|
+
)
|
|
170
|
+
if build.returncode != 0:
|
|
171
|
+
# check=True would raise a CalledProcessError carrying only the exit
|
|
172
|
+
# status, discarding the captured output that says what went wrong.
|
|
173
|
+
raise RuntimeError(
|
|
174
|
+
f"`setup.py sdist` failed (exit {build.returncode}) while building "
|
|
175
|
+
f"{pkg_name!r}.\n--- stdout ---\n{build.stdout}"
|
|
176
|
+
f"\n--- stderr ---\n{build.stderr}"
|
|
177
|
+
)
|
|
153
178
|
pkg_filename = f"{pkg_name}-{pkg_version}.tar.gz"
|
|
154
179
|
if not os.path.exists(OUTPUT_DIR):
|
|
155
180
|
os.makedirs(OUTPUT_DIR)
|
|
@@ -161,9 +186,34 @@ def mk_api_pkg(
|
|
|
161
186
|
shutil.rmtree(tempdir)
|
|
162
187
|
|
|
163
188
|
|
|
189
|
+
def _check_build_backend():
|
|
190
|
+
"""Fail early and legibly when the interpreter cannot build an sdist.
|
|
191
|
+
|
|
192
|
+
``setup.py sdist`` needs setuptools in the *subprocess's* interpreter, which
|
|
193
|
+
is this one. Environments created by ``uv`` (and ``python -m venv
|
|
194
|
+
--without-pip``) do not have it. Without this check the user sees a bare
|
|
195
|
+
``CalledProcessError`` with an exit status and no cause.
|
|
196
|
+
"""
|
|
197
|
+
import importlib.util
|
|
198
|
+
|
|
199
|
+
if importlib.util.find_spec("setuptools") is None:
|
|
200
|
+
raise RuntimeError(
|
|
201
|
+
"mk_api_pkg builds a source distribution with `setup.py sdist`, "
|
|
202
|
+
f"which needs setuptools in {sys.executable}. Install it with: "
|
|
203
|
+
f"{sys.executable} -m pip install setuptools"
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
164
207
|
def main():
|
|
165
|
-
|
|
208
|
+
"""Entry point for the ``api-pkg-maker`` console script; returns the exit code.
|
|
209
|
+
|
|
210
|
+
``cw.dispatch`` returns the code rather than exiting, and
|
|
211
|
+
``[project.scripts]`` wraps this in ``sys.exit(main())``.
|
|
212
|
+
"""
|
|
213
|
+
import cw
|
|
214
|
+
|
|
215
|
+
return cw.dispatch(mk_api_pkg)
|
|
166
216
|
|
|
167
217
|
|
|
168
218
|
if __name__ == "__main__":
|
|
169
|
-
main()
|
|
219
|
+
raise SystemExit(main())
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
"""Turn an http-bound python object into a command line interface.
|
|
2
2
|
|
|
3
3
|
Signatures are first made argparse-friendly (:func:`mk_argparse_friendly`),
|
|
4
|
-
then
|
|
4
|
+
then turned into a parser by :func:`mk_cli` and run by :func:`dispatch_cli`.
|
|
5
|
+
|
|
6
|
+
The parser is built by ``cw``. Its ``BY_NAME_IF_KWONLY`` naming is what
|
|
7
|
+
reproduces the grammar this module has always produced -- see
|
|
8
|
+
:data:`CLI_CONVENTION`.
|
|
5
9
|
"""
|
|
6
10
|
|
|
7
|
-
import
|
|
11
|
+
import dataclasses
|
|
8
12
|
from functools import wraps
|
|
9
13
|
from glom import glom
|
|
10
14
|
from inspect import signature
|
|
@@ -15,11 +19,30 @@ import requests
|
|
|
15
19
|
from collections.abc import Callable, Iterable
|
|
16
20
|
import yaml
|
|
17
21
|
|
|
22
|
+
import cw
|
|
23
|
+
|
|
18
24
|
from i2.io_trans import JSONAnnotAndDfltIoTrans
|
|
19
25
|
from i2.signatures import set_signature_of_func, Sig, KO
|
|
20
26
|
from http2py import HttpClient
|
|
21
27
|
from http2py.authentication import mk_auth, DFLT_CONFIG_FILENAME
|
|
22
28
|
|
|
29
|
+
#: How the command line is derived from a signature.
|
|
30
|
+
#:
|
|
31
|
+
#: Every function :func:`register_cli_method` builds has KEYWORD-ONLY parameters
|
|
32
|
+
#: only (plus ``*args``/``**kwargs`` on a no-argument route), because
|
|
33
|
+
#: :func:`Sig.merge_with_sig` is called with ``kind=KO``. For that shape,
|
|
34
|
+
#: ``BY_NAME_IF_KWONLY`` is what reproduces the grammar this module produced
|
|
35
|
+
#: under argh: every parameter is an option, required exactly when it has no
|
|
36
|
+
#: default -- ``get-thing -u UID -p PID [-l LIMIT]``.
|
|
37
|
+
#:
|
|
38
|
+
#: cw's ``ARGH`` default (``BY_NAME_IF_HAS_DEFAULT``) would NOT: it turns a
|
|
39
|
+
#: keyword-only parameter with no default into a *positional*, so every required
|
|
40
|
+
#: API argument would silently change spelling. That is not a cw bug -- it
|
|
41
|
+
#: faithfully reproduces argh's *explicit* ``BY_NAME_IF_HAS_DEFAULT`` policy.
|
|
42
|
+
#: This module never selected a policy, and argh's no-policy legacy mode is the
|
|
43
|
+
#: one that matches ``BY_NAME_IF_KWONLY`` here.
|
|
44
|
+
CLI_CONVENTION = dataclasses.replace(cw.ARGH, naming=cw.BY_NAME_IF_KWONLY)
|
|
45
|
+
|
|
23
46
|
|
|
24
47
|
def mk_sig_argparse_friendly(sig):
|
|
25
48
|
"""Modifies a signature to change all leading underscores in param names
|
|
@@ -109,15 +132,20 @@ def mk_cli(
|
|
|
109
132
|
for methodname, method in client_details.__dict__.items()
|
|
110
133
|
if getattr(method, "method_spec", None)
|
|
111
134
|
]
|
|
112
|
-
|
|
113
|
-
parser.add_commands(cli_methods)
|
|
114
|
-
return parser
|
|
135
|
+
return cw.mk_parser(cli_methods, convention=CLI_CONVENTION)
|
|
115
136
|
|
|
116
137
|
|
|
117
138
|
def dispatch_cli(*args, **kwargs):
|
|
118
|
-
"""Makes a CLI parser with mk_cli and then
|
|
119
|
-
|
|
120
|
-
|
|
139
|
+
"""Makes a CLI parser with mk_cli and then runs it (see documentation of mk_cli)
|
|
140
|
+
|
|
141
|
+
Returns ``None`` when the command ran, and raises ``SystemExit(2)`` on a
|
|
142
|
+
command-line error -- the same two outcomes the previous argh-based
|
|
143
|
+
implementation produced. ``cw.run`` *returns* the exit code rather than
|
|
144
|
+
exiting, so the non-zero case is re-raised here.
|
|
145
|
+
"""
|
|
146
|
+
code = cw.run(mk_cli(*args, **kwargs))
|
|
147
|
+
if code:
|
|
148
|
+
raise SystemExit(code)
|
|
121
149
|
|
|
122
150
|
|
|
123
151
|
set_signature_of_func(dispatch_cli, signature(mk_cli))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""A tiny worked example of the CLI-making tools, used by the docs and by hand."""
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import cw
|
|
4
4
|
from collections.abc import Iterable
|
|
5
5
|
|
|
6
6
|
from http2py.cli_maker import mk_argparse_friendly
|
|
@@ -13,6 +13,11 @@ def myfun(a: int, b: Iterable[int]):
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
if __name__ == "__main__":
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
from http2py.cli_maker import CLI_CONVENTION
|
|
17
|
+
|
|
18
|
+
raise SystemExit(
|
|
19
|
+
cw.dispatch(
|
|
20
|
+
[mk_argparse_friendly(AnnotAndDfltIoTrans()(myfun))],
|
|
21
|
+
convention=CLI_CONVENTION,
|
|
22
|
+
)
|
|
23
|
+
)
|
|
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
|
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "http2py"
|
|
9
|
-
version = "0.1.
|
|
9
|
+
version = "0.1.34"
|
|
10
10
|
description = "Tools to create python binders to http web services."
|
|
11
11
|
readme = "README.md"
|
|
12
12
|
requires-python = ">=3.10"
|
|
@@ -34,7 +34,7 @@ dependencies = [
|
|
|
34
34
|
"i2",
|
|
35
35
|
"ju",
|
|
36
36
|
"requests",
|
|
37
|
-
"
|
|
37
|
+
"cw>=0.1.1,<0.2",
|
|
38
38
|
"PyYAML",
|
|
39
39
|
"importlib_resources",
|
|
40
40
|
# TEMPORARY, not imported by http2py: ju/oas.py has an unconditional (and
|
|
@@ -57,6 +57,11 @@ dev = [
|
|
|
57
57
|
"pytest>=7.0",
|
|
58
58
|
"pytest-cov>=4.0",
|
|
59
59
|
"ruff>=0.1.0",
|
|
60
|
+
# Not a runtime dependency of the package: `mk_api_pkg` shells out to
|
|
61
|
+
# `setup.py sdist`, so setuptools is needed to exercise it. A uv-created
|
|
62
|
+
# venv does not ship setuptools, so without this the end-to-end
|
|
63
|
+
# api_pkg_maker test would silently skip in CI.
|
|
64
|
+
"setuptools",
|
|
60
65
|
]
|
|
61
66
|
|
|
62
67
|
[tool.hatch.build.targets.wheel]
|
|
@@ -160,15 +165,16 @@ coverage_threshold = 0
|
|
|
160
165
|
coverage_report_format = [
|
|
161
166
|
"term",
|
|
162
167
|
]
|
|
163
|
-
# `http2py
|
|
164
|
-
#
|
|
165
|
-
#
|
|
166
|
-
#
|
|
167
|
-
#
|
|
168
|
-
#
|
|
169
|
-
#
|
|
168
|
+
# `http2py/tests` stands up a real py2http web service, so it needs a test-only
|
|
169
|
+
# dependency that CI does not install. Excluding it from collection is what keeps
|
|
170
|
+
# `pytest --doctest-modules` from aborting the session on the import.
|
|
171
|
+
# (`http2py/api_pkg_maker.py` used to be excluded here too, because
|
|
172
|
+
# `setuptools.sandbox` no longer exists and the module was unimportable. It is
|
|
173
|
+
# fixed and collected again -- see issue #14.)
|
|
174
|
+
# This path is repeated in the repo-root conftest.py so that a bare `pytest`
|
|
175
|
+
# behaves like CI -- tests/test_ci_collection_contract.py keeps the two lists
|
|
176
|
+
# in agreement.
|
|
170
177
|
exclude_paths = [
|
|
171
|
-
"http2py/api_pkg_maker.py",
|
|
172
178
|
"http2py/tests",
|
|
173
179
|
]
|
|
174
180
|
test_on_windows = false
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"recorded_from": "argh 0.31.3 via ArghParser().add_commands(...) with NO explicit name-mapping policy -- argh's legacy mode, which is what http2py.cli_maker.mk_cli used before the cw migration",
|
|
3
|
+
"note": "Normalised usage lines only: CPython rewrites --help bodies between versions and CI spans 3.10 and 3.12.",
|
|
4
|
+
"usages": {
|
|
5
|
+
"__top__": "usage: PROG [-h] {get-thing,make,plain} ...",
|
|
6
|
+
"get-thing": "usage: PROG get-thing [-h] -u UID -p PID [-v VERBOSE] [-l LIMIT] [-a API_KEY] [-c CONFIG]",
|
|
7
|
+
"make": "usage: PROG make [-h] -n NAME [--count COUNT] [-f FLAG] [-a API_KEY] [--config CONFIG]",
|
|
8
|
+
"plain": "usage: PROG plain [-h] [-a API_KEY] [-c CONFIG] [args ...]"
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""One OpenAPI spec exercising every argument shape ``mk_cli`` has to handle.
|
|
2
|
+
|
|
3
|
+
Path parameters (required, and typed), query parameters (optional, with and
|
|
4
|
+
without a schema default), a JSON request body with a ``required`` list, an
|
|
5
|
+
``apiKey`` security scheme (which adds an ``api_key`` CLI argument), and a route
|
|
6
|
+
with no arguments at all (which yields a ``*args``/``**kwargs`` signature).
|
|
7
|
+
|
|
8
|
+
Kept as a module rather than inline so the golden recorder and the tests are
|
|
9
|
+
provably looking at the same spec.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
CONTENT = "application/json"
|
|
13
|
+
|
|
14
|
+
SPEC = {
|
|
15
|
+
"openapi": "3.0.2",
|
|
16
|
+
"info": {"title": "example", "version": "0.1"},
|
|
17
|
+
"servers": [{"url": "https://example.com"}],
|
|
18
|
+
"security": {"apiKey": {}},
|
|
19
|
+
"paths": {
|
|
20
|
+
"/u/{uid}/p/{pid}": {
|
|
21
|
+
"get": {
|
|
22
|
+
"x-method_name": "get_thing",
|
|
23
|
+
"description": "Two path args and two query args.",
|
|
24
|
+
"parameters": [
|
|
25
|
+
{
|
|
26
|
+
"name": "uid",
|
|
27
|
+
"in": "path",
|
|
28
|
+
"required": True,
|
|
29
|
+
"schema": {"type": "string"},
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"name": "pid",
|
|
33
|
+
"in": "path",
|
|
34
|
+
"required": True,
|
|
35
|
+
"schema": {"type": "integer"},
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"name": "verbose",
|
|
39
|
+
"in": "query",
|
|
40
|
+
"required": False,
|
|
41
|
+
"schema": {"type": "boolean"},
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"name": "limit",
|
|
45
|
+
"in": "query",
|
|
46
|
+
"required": False,
|
|
47
|
+
"schema": {"type": "integer", "default": 10},
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
"responses": {
|
|
51
|
+
"200": {"description": "", "content": {CONTENT: {"schema": {}}}}
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"/make": {
|
|
56
|
+
"post": {
|
|
57
|
+
"x-method_name": "make",
|
|
58
|
+
"description": "A request body with one required property.",
|
|
59
|
+
"requestBody": {
|
|
60
|
+
"content": {
|
|
61
|
+
CONTENT: {
|
|
62
|
+
"schema": {
|
|
63
|
+
"type": "object",
|
|
64
|
+
"properties": {
|
|
65
|
+
"name": {"type": "string"},
|
|
66
|
+
"count": {"type": "integer"},
|
|
67
|
+
"flag": {"type": "boolean"},
|
|
68
|
+
},
|
|
69
|
+
"required": ["name"],
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"responses": {
|
|
75
|
+
"200": {"description": "", "content": {CONTENT: {"schema": {}}}}
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
"/plain": {
|
|
80
|
+
"get": {
|
|
81
|
+
"x-method_name": "plain",
|
|
82
|
+
"description": "No arguments at all.",
|
|
83
|
+
"responses": {
|
|
84
|
+
"200": {"description": "", "content": {CONTENT: {"schema": {}}}}
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
}
|
|
@@ -2,15 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
Under ``pytest --doctest-modules`` (what the CI test action runs) an unimportable
|
|
4
4
|
module is not one red test, it is a collection abort that takes the whole session
|
|
5
|
-
down.
|
|
5
|
+
down.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
``http2py/api_pkg_maker.py`` used to be in that state -- it imported
|
|
8
|
+
``setuptools.sandbox``, removed from modern setuptools. It now builds the sdist
|
|
9
|
+
in a subprocess instead, imports cleanly, and is collected again. What remains
|
|
10
|
+
excluded is ``http2py/tests``, whose fixtures stand up a real ``py2http`` web
|
|
11
|
+
service: ``py2http`` is a test-only requirement that CI does not install.
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
agree: ``[tool.wads.ci.testing].exclude_paths`` in pyproject.toml, and
|
|
13
|
+
The exclusion lives in two places that must agree:
|
|
14
|
+
``[tool.wads.ci.testing].exclude_paths`` in pyproject.toml, and
|
|
14
15
|
``collect_ignore`` in the repo-root conftest.py.
|
|
15
16
|
|
|
16
17
|
There is a third, subtler trap that these tests pin down: pytest eagerly imports
|
|
@@ -31,10 +32,11 @@ import pytest
|
|
|
31
32
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
32
33
|
|
|
33
34
|
# Modules that are known-unimportable and therefore excluded from collection.
|
|
34
|
-
#
|
|
35
|
-
|
|
35
|
+
# Empty, and it should stay that way: an unimportable module aborts the whole
|
|
36
|
+
# CI session rather than failing one test.
|
|
37
|
+
KNOWN_UNIMPORTABLE: set = set()
|
|
36
38
|
|
|
37
|
-
EXPECTED_EXCLUDED_PATHS = {"http2py/
|
|
39
|
+
EXPECTED_EXCLUDED_PATHS = {"http2py/tests"}
|
|
38
40
|
|
|
39
41
|
|
|
40
42
|
def _package_modules():
|
|
@@ -62,17 +64,27 @@ def test_every_package_module_imports_except_the_known_broken_one():
|
|
|
62
64
|
assert not failures, f"modules that would abort collection: {failures}"
|
|
63
65
|
|
|
64
66
|
|
|
65
|
-
def
|
|
66
|
-
"""
|
|
67
|
+
def test_api_pkg_maker_imports_and_the_console_script_can_start():
|
|
68
|
+
"""The former tripwire for issue #14, inverted now that the module is fixed.
|
|
67
69
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
``api-pkg-maker`` is this package's only console script, and it could not
|
|
71
|
+
start at all: ``from setuptools import sandbox`` raised ``ImportError`` at
|
|
72
|
+
import time, before ``main()`` was ever entered. Both halves are asserted --
|
|
73
|
+
the module imports, and the entry point the console script calls is there.
|
|
72
74
|
"""
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
module = importlib.import_module("http2py.api_pkg_maker")
|
|
76
|
+
assert callable(module.main)
|
|
77
|
+
assert callable(module.mk_api_pkg)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_no_module_is_excluded_for_being_unimportable():
|
|
81
|
+
"""Guard the invariant, not the past exception.
|
|
82
|
+
|
|
83
|
+
``KNOWN_UNIMPORTABLE`` is empty. If a future change adds a module that
|
|
84
|
+
cannot be imported, the honest fix is to fix the module -- not to grow this
|
|
85
|
+
set -- because an unimportable module aborts collection for everything.
|
|
86
|
+
"""
|
|
87
|
+
assert KNOWN_UNIMPORTABLE == set()
|
|
76
88
|
|
|
77
89
|
|
|
78
90
|
@pytest.mark.skipif(sys.version_info < (3, 11), reason="tomllib needs Python 3.11+")
|
|
@@ -137,3 +149,67 @@ def test_excluded_test_dir_conftest_imports_without_undeclared_deps():
|
|
|
137
149
|
sys.modules.pop("_http2py_tests_conftest", None)
|
|
138
150
|
|
|
139
151
|
assert hasattr(module, "ws_app")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_mk_api_pkg_builds_a_source_distribution(tmp_path, monkeypatch):
|
|
155
|
+
"""End-to-end proof that the ``setuptools.sandbox`` fix actually works.
|
|
156
|
+
|
|
157
|
+
Builds a real sdist from a spec dict -- no network, no live service -- and
|
|
158
|
+
checks the archive is where ``mk_api_pkg`` says it is. Also asserts the
|
|
159
|
+
working directory survives: the old implementation did a bare
|
|
160
|
+
``os.chdir(tempdir)`` and never came back, which quietly broke any caller
|
|
161
|
+
that used relative paths afterwards.
|
|
162
|
+
"""
|
|
163
|
+
import tarfile
|
|
164
|
+
|
|
165
|
+
pytest.importorskip(
|
|
166
|
+
"setuptools",
|
|
167
|
+
reason="mk_api_pkg shells out to `setup.py sdist`; setuptools is in the "
|
|
168
|
+
"dev extra, so this runs in CI but skips in a bare environment",
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
from http2py import api_pkg_maker
|
|
172
|
+
|
|
173
|
+
monkeypatch.setattr(api_pkg_maker, "OUTPUT_DIR", str(tmp_path))
|
|
174
|
+
cwd_before = Path.cwd()
|
|
175
|
+
|
|
176
|
+
spec = {
|
|
177
|
+
"openapi": "3.0.2",
|
|
178
|
+
"info": {"title": "d", "version": "0.1"},
|
|
179
|
+
"servers": [{"url": "http://localhost:3030"}],
|
|
180
|
+
"paths": {
|
|
181
|
+
"/foo": {
|
|
182
|
+
"post": {
|
|
183
|
+
"x-method_name": "foo",
|
|
184
|
+
"description": "foo.",
|
|
185
|
+
"requestBody": {
|
|
186
|
+
"required": True,
|
|
187
|
+
"content": {
|
|
188
|
+
"application/json": {
|
|
189
|
+
"schema": {
|
|
190
|
+
"type": "object",
|
|
191
|
+
"properties": {"a": {"type": "integer"}},
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
"responses": {
|
|
197
|
+
"200": {
|
|
198
|
+
"description": "",
|
|
199
|
+
"content": {"application/json": {"schema": {}}},
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
built = api_pkg_maker.mk_api_pkg(spec, pkg_name="probepkg", pkg_version="1.2.3")
|
|
208
|
+
|
|
209
|
+
assert Path(built).is_file()
|
|
210
|
+
assert Path(built).name == "probepkg-1.2.3.tar.gz"
|
|
211
|
+
assert Path(built).parent == tmp_path
|
|
212
|
+
assert Path.cwd() == cwd_before, "mk_api_pkg left the process in another directory"
|
|
213
|
+
with tarfile.open(built) as archive:
|
|
214
|
+
names = {n.split("/", 1)[-1] for n in archive.getnames()}
|
|
215
|
+
assert "probepkg/funcs.py" in names
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Pin ``mk_cli``'s generated command line against what argh produced.
|
|
2
|
+
|
|
3
|
+
``http2py.cli_maker`` builds a CLI out of an OpenAPI spec. The golden in
|
|
4
|
+
``tests/cli_goldens/mk_cli.json`` records the ``usage:`` line argh produced for
|
|
5
|
+
every command of ``tests/spec_fixture.SPEC``, recorded through
|
|
6
|
+
``ArghParser().add_commands(...)`` -- exactly the call ``mk_cli`` used to make --
|
|
7
|
+
before the migration to ``cw``.
|
|
8
|
+
|
|
9
|
+
Only usage lines are asserted, not full ``--help`` bodies: CPython rewrites the
|
|
10
|
+
option column between versions and CI spans 3.10 and 3.12. The full-body diff
|
|
11
|
+
(zero differences across all four surfaces) was done at migration time and is
|
|
12
|
+
recorded in the pull request.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import dataclasses
|
|
18
|
+
import json
|
|
19
|
+
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
import pytest
|
|
23
|
+
|
|
24
|
+
import cw
|
|
25
|
+
|
|
26
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
27
|
+
|
|
28
|
+
from spec_fixture import SPEC # noqa: E402
|
|
29
|
+
|
|
30
|
+
GOLDEN = json.loads(
|
|
31
|
+
(Path(__file__).parent / "cli_goldens" / "mk_cli.json").read_text(encoding="utf-8")
|
|
32
|
+
)
|
|
33
|
+
EXPECTED = GOLDEN["usages"]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _usages(parser):
|
|
37
|
+
"""``{command_name: normalised usage line}``, plus ``__top__`` for the parser."""
|
|
38
|
+
out = {"__top__": " ".join(parser.format_usage().split())}
|
|
39
|
+
for action in parser._actions:
|
|
40
|
+
if getattr(action, "choices", None):
|
|
41
|
+
for name, sub in action.choices.items():
|
|
42
|
+
out[name] = " ".join(sub.format_usage().split())
|
|
43
|
+
return out
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _parser(**kwargs):
|
|
47
|
+
from http2py.cli_maker import mk_cli
|
|
48
|
+
|
|
49
|
+
return mk_cli(openapi_spec=SPEC, **kwargs)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_mk_cli_returns_a_parser_with_one_command_per_route():
|
|
53
|
+
parser = _parser()
|
|
54
|
+
assert set(_usages(parser)) == {"__top__", "get-thing", "make", "plain"}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@pytest.mark.parametrize("command", sorted(EXPECTED))
|
|
58
|
+
def test_usage_matches_the_argh_recording(command):
|
|
59
|
+
"""Every command's grammar is byte-identical to what argh generated.
|
|
60
|
+
|
|
61
|
+
The recorded golden used ``prog="PROG"``; a parser built during a test run
|
|
62
|
+
derives its prog from ``sys.argv[0]``. Only what follows the prog is
|
|
63
|
+
compared, so both sides drop ``usage:`` plus the prog tokens.
|
|
64
|
+
"""
|
|
65
|
+
actual = _usages(_parser())
|
|
66
|
+
offset = 1 if command == "__top__" else 2 # subparser progs are two tokens
|
|
67
|
+
assert actual[command].split()[1 + offset :] == EXPECTED[command].split()[
|
|
68
|
+
1 + offset :
|
|
69
|
+
], f"{command}: {actual[command]!r} != {EXPECTED[command]!r}"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_required_api_arguments_are_options_not_positionals():
|
|
73
|
+
"""The load-bearing consequence of ``CLI_CONVENTION``.
|
|
74
|
+
|
|
75
|
+
Every function ``register_cli_method`` builds is all-keyword-only, so a
|
|
76
|
+
required argument like ``uid`` must be spelled ``-u UID`` / ``--uid UID``.
|
|
77
|
+
Under cw's ARGH default it would become a bare positional and every caller's
|
|
78
|
+
command line would silently change.
|
|
79
|
+
"""
|
|
80
|
+
usage = _usages(_parser())["get-thing"]
|
|
81
|
+
assert "-u UID" in usage and "-p PID" in usage
|
|
82
|
+
assert " uid" not in usage and " pid" not in usage
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_the_convention_is_load_bearing_and_this_test_can_fail():
|
|
86
|
+
"""Prove the check above would catch losing ``naming=BY_NAME_IF_KWONLY``.
|
|
87
|
+
|
|
88
|
+
If this stops holding, cw's default naming has become equivalent for this
|
|
89
|
+
signature shape and ``CLI_CONVENTION``'s comment is stale -- not the other
|
|
90
|
+
way round.
|
|
91
|
+
"""
|
|
92
|
+
from http2py.cli_maker import CLI_CONVENTION, register_cli_method
|
|
93
|
+
from http2py.client import HttpClient
|
|
94
|
+
|
|
95
|
+
client = HttpClient(SPEC)
|
|
96
|
+
methods = [
|
|
97
|
+
register_cli_method(SPEC, m, ["api_key"])
|
|
98
|
+
for _, m in client.__dict__.items()
|
|
99
|
+
if getattr(m, "method_spec", None)
|
|
100
|
+
]
|
|
101
|
+
default = cw.mk_parser(
|
|
102
|
+
methods,
|
|
103
|
+
convention=dataclasses.replace(CLI_CONVENTION, naming=cw.ARGH.naming),
|
|
104
|
+
prog="PROG",
|
|
105
|
+
)
|
|
106
|
+
usage = _usages(default)["get-thing"]
|
|
107
|
+
assert "-u UID" not in usage, (
|
|
108
|
+
"cw's default naming was expected to turn the required keyword-only "
|
|
109
|
+
f"arguments into positionals, but produced {usage!r}"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_every_parameter_register_cli_method_builds_is_keyword_only():
|
|
114
|
+
"""The premise ``CLI_CONVENTION`` rests on, asserted rather than assumed.
|
|
115
|
+
|
|
116
|
+
``BY_NAME_IF_KWONLY`` reproduces argh's legacy grammar *because* these
|
|
117
|
+
signatures are all keyword-only. If a future change introduced an ordinary
|
|
118
|
+
positional parameter, the two policies would diverge for it and the golden
|
|
119
|
+
above is what would catch it -- but this says so directly.
|
|
120
|
+
"""
|
|
121
|
+
import inspect
|
|
122
|
+
|
|
123
|
+
from http2py.cli_maker import register_cli_method
|
|
124
|
+
from http2py.client import HttpClient
|
|
125
|
+
|
|
126
|
+
client = HttpClient(SPEC)
|
|
127
|
+
for _, method in client.__dict__.items():
|
|
128
|
+
if not getattr(method, "method_spec", None):
|
|
129
|
+
continue
|
|
130
|
+
func = register_cli_method(SPEC, method, ["api_key"])
|
|
131
|
+
for param in inspect.signature(func).parameters.values():
|
|
132
|
+
assert param.kind in (
|
|
133
|
+
param.KEYWORD_ONLY,
|
|
134
|
+
param.VAR_POSITIONAL,
|
|
135
|
+
param.VAR_KEYWORD,
|
|
136
|
+
), f"{func.__name__}.{param.name} is {param.kind}"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_dispatch_cli_raises_SystemExit_on_a_command_line_error():
|
|
140
|
+
"""``cw.run`` RETURNS the exit code; ``dispatch_cli`` must still raise it.
|
|
141
|
+
|
|
142
|
+
argh's ``parser.dispatch()`` exited the process on a bad command line.
|
|
143
|
+
Without the re-raise, a broken invocation would look like success.
|
|
144
|
+
"""
|
|
145
|
+
from http2py.cli_maker import dispatch_cli
|
|
146
|
+
|
|
147
|
+
argv = sys.argv
|
|
148
|
+
sys.argv = ["PROG", "no-such-command"]
|
|
149
|
+
try:
|
|
150
|
+
with pytest.raises(SystemExit) as excinfo:
|
|
151
|
+
dispatch_cli(openapi_spec=SPEC)
|
|
152
|
+
assert excinfo.value.code == 2
|
|
153
|
+
finally:
|
|
154
|
+
sys.argv = argv
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def test_the_package_no_longer_imports_argh():
|
|
158
|
+
"""http2py no longer declares argh; nothing may import it."""
|
|
159
|
+
import subprocess
|
|
160
|
+
|
|
161
|
+
proc = subprocess.run(
|
|
162
|
+
[
|
|
163
|
+
sys.executable,
|
|
164
|
+
"-c",
|
|
165
|
+
"import sys, http2py, http2py.cli_maker, http2py.api_pkg_maker; "
|
|
166
|
+
"sys.exit(1 if 'argh' in sys.modules else 0)",
|
|
167
|
+
],
|
|
168
|
+
capture_output=True,
|
|
169
|
+
text=True,
|
|
170
|
+
)
|
|
171
|
+
assert proc.returncode == 0, f"argh was imported: {proc.stderr}"
|
http2py-0.1.33/conftest.py
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
"""Pytest configuration for the http2py repository.
|
|
2
|
-
|
|
3
|
-
``http2py.api_pkg_maker`` imports ``setuptools.sandbox``, which modern
|
|
4
|
-
setuptools no longer ships, so merely *importing* the module raises
|
|
5
|
-
``ImportError``. Under ``--doctest-modules`` that is not a single failing test:
|
|
6
|
-
it aborts collection for the whole session. The module is deliberately left in
|
|
7
|
-
place (whether to rewrite it or remove it is still open on issue #14), so it is
|
|
8
|
-
excluded from collection instead.
|
|
9
|
-
|
|
10
|
-
CI excludes the same two paths via ``[tool.wads.ci.testing].exclude_paths`` in
|
|
11
|
-
pyproject.toml, which the wads ``run-tests-uv`` action turns into ``--ignore``
|
|
12
|
-
flags. Repeating them here is what makes a bare ``pytest`` (no flags, e.g. a
|
|
13
|
-
local run or an editor's test runner) behave the same way as CI.
|
|
14
|
-
``tests/test_ci_collection_contract.py`` asserts the two lists stay in
|
|
15
|
-
agreement, so they cannot drift apart silently.
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
collect_ignore = [
|
|
19
|
-
"http2py/api_pkg_maker.py",
|
|
20
|
-
"http2py/tests",
|
|
21
|
-
]
|
|
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
|