py2http 0.1.67__tar.gz → 0.1.68__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.
- {py2http-0.1.67 → py2http-0.1.68}/PKG-INFO +1 -1
- {py2http-0.1.67 → py2http-0.1.68}/py2http/service.py +5 -5
- py2http-0.1.68/py2http/tests/test_positional_only.py +150 -0
- {py2http-0.1.67 → py2http-0.1.68}/py2http/util.py +80 -1
- {py2http-0.1.67 → py2http-0.1.68}/py2http.egg-info/PKG-INFO +1 -1
- {py2http-0.1.67 → py2http-0.1.68}/py2http.egg-info/SOURCES.txt +1 -0
- {py2http-0.1.67 → py2http-0.1.68}/setup.cfg +1 -1
- {py2http-0.1.67 → py2http-0.1.68}/LICENSE +0 -0
- {py2http-0.1.67 → py2http-0.1.68}/README.md +0 -0
- {py2http-0.1.67 → py2http-0.1.68}/py2http/__init__.py +0 -0
- {py2http-0.1.67 → py2http-0.1.68}/py2http/bottle_plugins.py +0 -0
- {py2http-0.1.67 → py2http-0.1.68}/py2http/config.py +0 -0
- {py2http-0.1.67 → py2http-0.1.68}/py2http/constants.py +0 -0
- {py2http-0.1.67 → py2http-0.1.68}/py2http/decorators.py +0 -0
- {py2http-0.1.67 → py2http-0.1.68}/py2http/default_configs.py +0 -0
- {py2http-0.1.67 → py2http-0.1.68}/py2http/diagnosis.py +0 -0
- {py2http-0.1.67 → py2http-0.1.68}/py2http/examples/__init__.py +0 -0
- {py2http-0.1.67 → py2http-0.1.68}/py2http/examples/custom_input_mapper.py +0 -0
- {py2http-0.1.67 → py2http-0.1.68}/py2http/examples/custom_input_mappers.py +0 -0
- {py2http-0.1.67 → py2http-0.1.68}/py2http/examples/example_service.py +0 -0
- {py2http-0.1.67 → py2http-0.1.68}/py2http/examples/example_service_bottle.py +0 -0
- {py2http-0.1.67 → py2http-0.1.68}/py2http/examples/example_service_flask.py +0 -0
- {py2http-0.1.67 → py2http-0.1.68}/py2http/examples/object_dispatch.py +0 -0
- {py2http-0.1.67 → py2http-0.1.68}/py2http/middleware.py +0 -0
- {py2http-0.1.67 → py2http-0.1.68}/py2http/openapi_utils.py +0 -0
- {py2http-0.1.67 → py2http-0.1.68}/py2http/route_utils.py +0 -0
- {py2http-0.1.67 → py2http-0.1.68}/py2http/schema_tools.py +0 -0
- {py2http-0.1.67 → py2http-0.1.68}/py2http/tests/__init__.py +0 -0
- {py2http-0.1.67 → py2http-0.1.68}/py2http/tests/example_service_test.py +0 -0
- {py2http-0.1.67 → py2http-0.1.68}/py2http/tests/objects_for_testing.py +0 -0
- {py2http-0.1.67 → py2http-0.1.68}/py2http/tests/simple_run_process_test.py +0 -0
- {py2http-0.1.67 → py2http-0.1.68}/py2http/tests/test_attr_dispatch.py +0 -0
- {py2http-0.1.67 → py2http-0.1.68}/py2http/tests/test_binary_req.py +0 -0
- {py2http-0.1.67 → py2http-0.1.68}/py2http/tests/test_decorators.py +0 -0
- {py2http-0.1.67 → py2http-0.1.68}/py2http/tests/test_flask.py +0 -0
- {py2http-0.1.67 → py2http-0.1.68}/py2http/tests/test_instance_endpoints.py +0 -0
- {py2http-0.1.67 → py2http-0.1.68}/py2http/tests/test_jwt_middleware.py +0 -0
- {py2http-0.1.67 → py2http-0.1.68}/py2http/tests/test_not_set_defaults.py +0 -0
- {py2http-0.1.67 → py2http-0.1.68}/py2http/tests/test_obj_ownership.py +0 -0
- {py2http-0.1.67 → py2http-0.1.68}/py2http/tests/test_p2h2p.py +0 -0
- {py2http-0.1.67 → py2http-0.1.68}/py2http/tests/utils_for_testing.py +0 -0
- {py2http-0.1.67 → py2http-0.1.68}/py2http/types.py +0 -0
- {py2http-0.1.67 → py2http-0.1.68}/py2http.egg-info/dependency_links.txt +0 -0
- {py2http-0.1.67 → py2http-0.1.68}/py2http.egg-info/not-zip-safe +0 -0
- {py2http-0.1.67 → py2http-0.1.68}/py2http.egg-info/requires.txt +0 -0
- {py2http-0.1.67 → py2http-0.1.68}/py2http.egg-info/top_level.txt +0 -0
- {py2http-0.1.67 → py2http-0.1.68}/setup.py +0 -0
|
@@ -56,7 +56,7 @@ from py2http.schema_tools import (
|
|
|
56
56
|
mk_input_schema_from_func,
|
|
57
57
|
mk_output_schema_from_func,
|
|
58
58
|
)
|
|
59
|
-
from py2http.util import TypeAsserter
|
|
59
|
+
from py2http.util import TypeAsserter, call_with_inputs
|
|
60
60
|
from py2http.constants import JSON_CONTENT_TYPE
|
|
61
61
|
|
|
62
62
|
# Instance endpoints (``handler['endpoint']`` is an object) are one object every client
|
|
@@ -518,7 +518,7 @@ def mk_route(func, **configs):
|
|
|
518
518
|
try:
|
|
519
519
|
inputs = input_mapper(req)
|
|
520
520
|
input_args, input_kwargs = get_input_args_and_kwargs(inputs)
|
|
521
|
-
raw_result = func
|
|
521
|
+
raw_result = call_with_inputs(func, input_args, input_kwargs)
|
|
522
522
|
return output_mapper(raw_result, **inputs)
|
|
523
523
|
finally:
|
|
524
524
|
current_request.reset(token)
|
|
@@ -536,7 +536,7 @@ def mk_route(func, **configs):
|
|
|
536
536
|
if isawaitable(inputs): # Pattern: pass-on async property
|
|
537
537
|
inputs = await inputs
|
|
538
538
|
input_args, input_kwargs = get_input_args_and_kwargs(inputs)
|
|
539
|
-
raw_result = func
|
|
539
|
+
raw_result = call_with_inputs(func, input_args, input_kwargs)
|
|
540
540
|
if isawaitable(raw_result): # Pattern: pass-on async property
|
|
541
541
|
raw_result = await raw_result
|
|
542
542
|
final_result = output_mapper(raw_result, **inputs)
|
|
@@ -1237,7 +1237,7 @@ def _class_endpoint_dispatcher(cls, handler, name, is_dispatchable):
|
|
|
1237
1237
|
if _obj_id is None:
|
|
1238
1238
|
if _attr_name is not None:
|
|
1239
1239
|
raise InputError("_attr_name must be None when _obj_id is None")
|
|
1240
|
-
new_obj = cls
|
|
1240
|
+
new_obj = call_with_inputs(cls, kwargs=kwargs)
|
|
1241
1241
|
new_id = str(uuid4())
|
|
1242
1242
|
store.put(who, new_id, new_obj)
|
|
1243
1243
|
check_size(who, new_id, new_obj)
|
|
@@ -1262,5 +1262,5 @@ def _get_attr_value(obj, attr_name, is_dispatchable, /, **kwargs):
|
|
|
1262
1262
|
)
|
|
1263
1263
|
attr = getattr(obj, attr_name)
|
|
1264
1264
|
if callable(attr):
|
|
1265
|
-
return attr
|
|
1265
|
+
return call_with_inputs(attr, kwargs=kwargs)
|
|
1266
1266
|
return attr
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Positional-only parameters (``dict.__getitem__``, ``def f(x, /)``) can be called over
|
|
2
|
+
JSON, which supplies arguments by name (i2mint/py2http#13, #14)."""
|
|
3
|
+
|
|
4
|
+
import io
|
|
5
|
+
import json
|
|
6
|
+
from wsgiref.util import setup_testing_defaults
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
from i2.errors import InputError
|
|
10
|
+
|
|
11
|
+
from py2http import mk_app
|
|
12
|
+
from py2http.util import call_with_inputs
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_call_with_inputs_passes_positional_only_by_name():
|
|
16
|
+
assert call_with_inputs(lambda x, /, y: (x, y), kwargs={"y": 2, "x": 1}) == (1, 2)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_call_with_inputs_leaves_ordinary_calls_alone():
|
|
20
|
+
assert call_with_inputs(lambda x, y=0: (x, y), (1,), {"y": 2}) == (1, 2)
|
|
21
|
+
assert call_with_inputs(lambda *a, **k: (a, k), (1,), {"z": 2}) == ((1,), {"z": 2})
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_explicit_positional_args_come_first():
|
|
25
|
+
assert call_with_inputs(lambda x, y, /: (x, y), (1,), {"y": 2}) == (1, 2)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_optional_positional_only():
|
|
29
|
+
def f(a, b=5, c=6, /):
|
|
30
|
+
return a, b, c
|
|
31
|
+
|
|
32
|
+
assert call_with_inputs(f, kwargs={"a": 1}) == (1, 5, 6)
|
|
33
|
+
assert call_with_inputs(f, kwargs={"a": 1, "c": 9}) == (1, 5, 9)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_missing_required_positional_only_is_an_input_error():
|
|
37
|
+
with pytest.raises(InputError, match="'y'"):
|
|
38
|
+
call_with_inputs(lambda x, y, /: None, kwargs={"x": 1})
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_callable_without_a_signature_gets_kwargs_as_before(monkeypatch):
|
|
42
|
+
import py2http.util
|
|
43
|
+
|
|
44
|
+
def no_signature(func):
|
|
45
|
+
raise ValueError("no signature found")
|
|
46
|
+
|
|
47
|
+
monkeypatch.setattr(py2http.util, "signature", no_signature)
|
|
48
|
+
assert call_with_inputs(lambda **kw: kw, kwargs={"x": 1}) == {"x": 1}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_container_methods_without_a_readable_signature(monkeypatch):
|
|
52
|
+
"""Python 3.10 can't read ``dict.__getitem__``'s or ``dict.pop``'s signature."""
|
|
53
|
+
import py2http.util
|
|
54
|
+
|
|
55
|
+
def no_signature(func):
|
|
56
|
+
raise ValueError("no signature found")
|
|
57
|
+
|
|
58
|
+
monkeypatch.setattr(py2http.util, "signature", no_signature)
|
|
59
|
+
d = {"a": 1, "b": 2}
|
|
60
|
+
assert call_with_inputs(d.__getitem__, kwargs={"key": "a"}) == 1
|
|
61
|
+
assert call_with_inputs([5, 6].__getitem__, kwargs={"index": 1}) == 6
|
|
62
|
+
assert call_with_inputs(d.pop, kwargs={"key": "a"}) == 1
|
|
63
|
+
assert call_with_inputs(d.pop, kwargs={"key": "zz", "default": 0}) == 0
|
|
64
|
+
assert d == {"b": 2}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _post(app, path, payload):
|
|
68
|
+
body = json.dumps(payload).encode()
|
|
69
|
+
environ = {
|
|
70
|
+
"REQUEST_METHOD": "POST",
|
|
71
|
+
"PATH_INFO": path,
|
|
72
|
+
"CONTENT_TYPE": "application/json",
|
|
73
|
+
"CONTENT_LENGTH": str(len(body)),
|
|
74
|
+
"wsgi.input": io.BytesIO(body),
|
|
75
|
+
}
|
|
76
|
+
setup_testing_defaults(environ)
|
|
77
|
+
statuses = []
|
|
78
|
+
out = b"".join(app(environ, lambda status, headers, *a: statuses.append(status)))
|
|
79
|
+
return statuses[0], json.loads(out)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_dict_dunders_over_http():
|
|
83
|
+
store = {"some_key": "some_value"}
|
|
84
|
+
app = mk_app(
|
|
85
|
+
[
|
|
86
|
+
dict(
|
|
87
|
+
endpoint=store,
|
|
88
|
+
name="backend_store",
|
|
89
|
+
attr_names=["__getitem__", "__setitem__"],
|
|
90
|
+
obj_owner=lambda req: "anyone",
|
|
91
|
+
)
|
|
92
|
+
]
|
|
93
|
+
)
|
|
94
|
+
status, value = _post(
|
|
95
|
+
app, "/backend_store", {"_attr_name": "__getitem__", "key": "some_key"}
|
|
96
|
+
)
|
|
97
|
+
assert status.startswith("200") and value == "some_value"
|
|
98
|
+
payload = {"_attr_name": "__setitem__", "key": "k", "value": 1}
|
|
99
|
+
assert _post(app, "/backend_store", payload)[0].startswith("200")
|
|
100
|
+
assert store["k"] == 1
|
|
101
|
+
status, _ = _post(app, "/backend_store", {"_attr_name": "__getitem__"})
|
|
102
|
+
assert status.startswith("400") # missing 'key': a client error
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_plain_function_with_positional_only_over_http():
|
|
106
|
+
def add(x, y, /):
|
|
107
|
+
return x + y
|
|
108
|
+
|
|
109
|
+
app = mk_app([add])
|
|
110
|
+
assert _post(app, "/add", {"x": 1, "y": 2}) == ("200 OK", 3)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_optional_positional_only_stays_a_keyword_when_there_is_var_keyword():
|
|
114
|
+
# Counter.update(iterable=None, /, **kwds): ``iterable`` is a keyword here
|
|
115
|
+
from collections import Counter, UserDict
|
|
116
|
+
|
|
117
|
+
assert call_with_inputs(lambda a=0, /, **kw: (a, kw), kwargs={"a": 1}) == (
|
|
118
|
+
0,
|
|
119
|
+
{"a": 1},
|
|
120
|
+
)
|
|
121
|
+
c = Counter()
|
|
122
|
+
call_with_inputs(c.update, kwargs={"iterable": 3})
|
|
123
|
+
assert c == Counter(iterable=3)
|
|
124
|
+
d = UserDict()
|
|
125
|
+
call_with_inputs(d.update, kwargs={"other": 1})
|
|
126
|
+
assert dict(d) == {"other": 1}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_required_positional_only_is_bound_even_with_var_keyword():
|
|
130
|
+
assert call_with_inputs(lambda a, /, **kw: (a, kw), kwargs={"a": 1, "b": 2}) == (
|
|
131
|
+
1,
|
|
132
|
+
{"b": 2},
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_class_endpoint_with_positional_only_constructor():
|
|
137
|
+
from py2http.service import _get_func_to_dispatch_handler, shared_objects
|
|
138
|
+
|
|
139
|
+
class Box:
|
|
140
|
+
def __init__(self, content, /):
|
|
141
|
+
self.content = content
|
|
142
|
+
|
|
143
|
+
def get(self):
|
|
144
|
+
return self.content
|
|
145
|
+
|
|
146
|
+
func = _get_func_to_dispatch_handler(
|
|
147
|
+
dict(endpoint=Box, name="box", attr_names=["get"], obj_owner=shared_objects)
|
|
148
|
+
)
|
|
149
|
+
obj_id = func(content=7)
|
|
150
|
+
assert func(_obj_id=obj_id, _attr_name="get") == 7
|
|
@@ -12,7 +12,8 @@ Overall, this module provides a collection of useful tools for handling lazy pro
|
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
14
|
from typing import Optional, Callable, Union, Iterable, Any
|
|
15
|
-
from
|
|
15
|
+
from collections.abc import MutableMapping, Sequence
|
|
16
|
+
from inspect import Parameter, Signature, signature
|
|
16
17
|
from multiprocessing.context import Process
|
|
17
18
|
from multiprocessing import Queue, active_children
|
|
18
19
|
from time import sleep, time
|
|
@@ -22,6 +23,7 @@ from contextlib import contextmanager
|
|
|
22
23
|
from strand import run_process
|
|
23
24
|
|
|
24
25
|
from glom import Spec # NOTE: Third-party
|
|
26
|
+
from i2.errors import InputError
|
|
25
27
|
|
|
26
28
|
|
|
27
29
|
class lazyprop:
|
|
@@ -488,3 +490,80 @@ def obj_to_path(obj):
|
|
|
488
490
|
return obj.__globals__[root_name], tuple(attr_path)
|
|
489
491
|
else:
|
|
490
492
|
return obj
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
_MISSING = object()
|
|
496
|
+
_REQUIRED = object()
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _positional_only_sig(*names: str, optional: Iterable[str] = ()) -> Signature:
|
|
500
|
+
return Signature(
|
|
501
|
+
Parameter(
|
|
502
|
+
name,
|
|
503
|
+
Parameter.POSITIONAL_ONLY,
|
|
504
|
+
default=None if name in optional else Parameter.empty,
|
|
505
|
+
)
|
|
506
|
+
for name in names
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def _signature_or_none(func: Callable) -> Optional[Signature]:
|
|
511
|
+
"""The signature of ``func``; for the few built-in container methods the Python
|
|
512
|
+
versions we support (3.10) can't read one, the one they have in later versions."""
|
|
513
|
+
try:
|
|
514
|
+
return signature(func)
|
|
515
|
+
except (ValueError, TypeError):
|
|
516
|
+
pass
|
|
517
|
+
owner = getattr(func, "__self__", None)
|
|
518
|
+
if getattr(func, "__name__", None) == "__getitem__":
|
|
519
|
+
return _positional_only_sig("index" if isinstance(owner, Sequence) else "key")
|
|
520
|
+
if getattr(func, "__name__", None) == "pop" and isinstance(owner, MutableMapping):
|
|
521
|
+
return _positional_only_sig("key", "default", optional=["default"])
|
|
522
|
+
return None
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def call_with_inputs(func: Callable, args: Iterable = (), kwargs: dict = None):
|
|
526
|
+
"""Call ``func(*args, **kwargs)``, passing the entries of ``kwargs`` that name
|
|
527
|
+
positional-only parameters of ``func`` positionally, which JSON (a mapping) can
|
|
528
|
+
only supply by name (i2mint/py2http#14).
|
|
529
|
+
|
|
530
|
+
>>> call_with_inputs({"a": 1}.__getitem__, kwargs={"key": "a"})
|
|
531
|
+
1
|
|
532
|
+
>>> d = {}
|
|
533
|
+
>>> call_with_inputs(d.__setitem__, kwargs={"key": "k", "value": 2}), d
|
|
534
|
+
(None, {'k': 2})
|
|
535
|
+
>>> call_with_inputs(lambda x, /, y=0: (x, y), kwargs={"x": 1, "y": 2})
|
|
536
|
+
(1, 2)
|
|
537
|
+
|
|
538
|
+
A positional-only argument that is neither required to be given nor given takes its
|
|
539
|
+
default; a missing required one is an ``InputError`` (a client error), not a
|
|
540
|
+
``TypeError``. A callable whose signature can't be read gets ``**kwargs`` as is.
|
|
541
|
+
|
|
542
|
+
Python lets a function with ``**kwargs`` receive a keyword that is also the name of
|
|
543
|
+
a positional-only parameter (``Counter.update(iterable=3)``): such an optional
|
|
544
|
+
parameter is left in ``kwargs``; only required ones are bound by name.
|
|
545
|
+
"""
|
|
546
|
+
args, kwargs = list(args), dict(kwargs or {})
|
|
547
|
+
sig = _signature_or_none(func)
|
|
548
|
+
if sig is None:
|
|
549
|
+
return func(*args, **kwargs)
|
|
550
|
+
params = list(sig.parameters.values())
|
|
551
|
+
takes_any_keyword = any(p.kind is Parameter.VAR_KEYWORD for p in params)
|
|
552
|
+
values = []
|
|
553
|
+
for param in params[len(args) :]:
|
|
554
|
+
if param.kind is not Parameter.POSITIONAL_ONLY:
|
|
555
|
+
break
|
|
556
|
+
required = param.default is param.empty
|
|
557
|
+
if param.name in kwargs and (required or not takes_any_keyword):
|
|
558
|
+
values.append(kwargs.pop(param.name))
|
|
559
|
+
else:
|
|
560
|
+
values.append(_REQUIRED if required else _MISSING)
|
|
561
|
+
while values and values[-1] is _MISSING: # trailing optionals: leave to the callee
|
|
562
|
+
values.pop()
|
|
563
|
+
for i, value in enumerate(values):
|
|
564
|
+
param = params[len(args) + i]
|
|
565
|
+
if value is _REQUIRED:
|
|
566
|
+
raise InputError(f"Missing argument: {param.name!r}")
|
|
567
|
+
if value is _MISSING: # an optional one before a given one: its default
|
|
568
|
+
values[i] = param.default
|
|
569
|
+
return func(*args, *values, **kwargs)
|
|
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
|
|
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
|