wrapture 1.0.0.dev7__tar.gz → 1.0.0.dev9__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.
- {wrapture-1.0.0.dev7/src/wrapture.egg-info → wrapture-1.0.0.dev9}/PKG-INFO +23 -11
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/README.md +22 -10
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/__init__.py +27 -1
- wrapture-1.0.0.dev9/src/wrapture/_wrappermixins.py +438 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/attributes.py +343 -64
- wrapture-1.0.0.dev9/src/wrapture/behaviours.py +947 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/bindings.py +1136 -137
- wrapture-1.0.0.dev9/src/wrapture/decorators.py +702 -0
- wrapture-1.0.0.dev9/src/wrapture/doubles.py +452 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/eventlogs.py +72 -7
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/events.py +38 -3
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/exceptions.py +10 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/export.py +1 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/iterators.py +53 -19
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/observed.py +113 -29
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/sinks.py +4 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/timeline.py +128 -18
- wrapture-1.0.0.dev9/src/wrapture/values.py +163 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/windows.py +24 -2
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9/src/wrapture.egg-info}/PKG-INFO +23 -11
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture.egg-info/SOURCES.txt +13 -0
- wrapture-1.0.0.dev9/tests/test_async_stubs.py +450 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_attributes.py +4 -15
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_behaviours.py +114 -1
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_bindings.py +66 -5
- wrapture-1.0.0.dev9/tests/test_decorators.py +647 -0
- wrapture-1.0.0.dev9/tests/test_doubles.py +678 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_eventlogs.py +166 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_iterators.py +50 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_jsonlines.py +34 -0
- wrapture-1.0.0.dev9/tests/test_mapping.py +383 -0
- wrapture-1.0.0.dev9/tests/test_module_attributes.py +389 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_observed.py +107 -0
- wrapture-1.0.0.dev9/tests/test_phases.py +920 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_recording.py +7 -5
- wrapture-1.0.0.dev9/tests/test_strict.py +228 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_timeline.py +205 -3
- wrapture-1.0.0.dev9/tests/test_values.py +535 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_windows.py +76 -0
- wrapture-1.0.0.dev9/tests/test_wrappermixins.py +214 -0
- wrapture-1.0.0.dev7/src/wrapture/behaviours.py +0 -428
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/LICENSE +0 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/pyproject.toml +0 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/setup.cfg +0 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/__main__.py +0 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/asgi.py +0 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/bootstrap.py +0 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/capture.py +0 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/collectors.py +0 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/config.py +0 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/lifecycle.py +0 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/outputs.py +0 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/py.typed +0 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/pytest_plugin.py +0 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/scheduler.py +0 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/stacks.py +0 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/tools/__init__.py +0 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/tools/__main__.py +0 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/tools/convert.py +0 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/wsgi.py +0 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture.egg-info/dependency_links.txt +0 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture.egg-info/entry_points.txt +0 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture.egg-info/not-zip-safe +0 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture.egg-info/requires.txt +0 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture.egg-info/top_level.txt +0 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_asgi.py +0 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_attribute_events.py +0 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_bootstrap.py +0 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_capture.py +0 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_collectors.py +0 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_config.py +0 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_events.py +0 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_export.py +0 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_generator_events.py +0 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_outputs.py +0 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_predicates.py +0 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_pytest_plugin.py +0 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_runner.py +0 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_sink_library.py +0 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_sinks.py +0 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_stacks.py +0 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_targets.py +0 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_testing_patterns.py +0 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_threads.py +0 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_tools.py +0 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_version.py +0 -0
- {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_wsgi.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wrapture
|
|
3
|
-
Version: 1.0.0.
|
|
3
|
+
Version: 1.0.0.dev9
|
|
4
4
|
Summary: Monkey patch, test, and trace Python by attaching bindings to call sites, without modifying the code being observed. Built on wrapt.
|
|
5
5
|
Author-email: Graham Dumpleton <Graham.Dumpleton@gmail.com>
|
|
6
6
|
License-Expression: BSD-2-Clause
|
|
@@ -44,10 +44,10 @@ It is a sibling project to [wrapt](https://github.com/GrahamDumpleton/wrapt)
|
|
|
44
44
|
and [autowrapt](https://github.com/GrahamDumpleton/autowrapt), building on the
|
|
45
45
|
safe monkey-patching machinery wrapt provides.
|
|
46
46
|
|
|
47
|
-
> **Status:
|
|
48
|
-
>
|
|
49
|
-
>
|
|
50
|
-
>
|
|
47
|
+
> **Status: stabilising ahead of 1.0.0.** The feature set is complete for
|
|
48
|
+
> a first release and development previews are published to PyPI. The API
|
|
49
|
+
> is being exercised and tidied rather than extended, so small changes are
|
|
50
|
+
> still possible before 1.0.0.
|
|
51
51
|
|
|
52
52
|
## Installation
|
|
53
53
|
|
|
@@ -75,7 +75,7 @@ worked examples, starting with
|
|
|
75
75
|
[testing code that calls external services](https://wrapture.readthedocs.io/en/latest/example-external-services.html),
|
|
76
76
|
each take one question you might arrive with and answer it end to end.
|
|
77
77
|
|
|
78
|
-
##
|
|
78
|
+
## At a glance
|
|
79
79
|
|
|
80
80
|
None of the classes below import wrapture or know they are observed:
|
|
81
81
|
|
|
@@ -112,9 +112,11 @@ One mechanism, three uses, in increasing order of machinery:
|
|
|
112
112
|
|
|
113
113
|
2. **Unit testing.** Observe and assert on how calls actually flowed through a
|
|
114
114
|
*real* call graph (nesting, ordering, arguments and return values) and
|
|
115
|
-
optionally intervene (stub, transform, fail-inject). Unlike a
|
|
116
|
-
which fabricates values and cannot see calls an object makes to itself,
|
|
117
|
-
wrapture watches the real code run
|
|
115
|
+
optionally intervene (stub, transform, fail-inject). Unlike a
|
|
116
|
+
`unittest.mock` `Mock`, which fabricates values and cannot see calls an object makes to itself,
|
|
117
|
+
wrapture watches the real code run, and when a test must supply a
|
|
118
|
+
stand-in it provides strict, recorded ones: `stub()` for a callable,
|
|
119
|
+
spec-required `mock()` for a collaborator. This makes it possible to test code
|
|
118
120
|
with no injectable seams at all, and to assert on what *didn't* happen on
|
|
119
121
|
an error path: inject a gateway timeout, then verify the ledger was not
|
|
120
122
|
written, the receipt was not sent, and the compensating refund was issued.
|
|
@@ -154,13 +156,23 @@ testing assertion library, a tracing tool, or both at once.
|
|
|
154
156
|
|
|
155
157
|
## What it is not
|
|
156
158
|
|
|
157
|
-
- **Not a
|
|
158
|
-
|
|
159
|
+
- **Not a fabrication tool.** There is no spec-less `Mock()` here by
|
|
160
|
+
design; stand-ins are strict and built from named specs, and
|
|
161
|
+
`unittest.mock` remains the tool for invented objects.
|
|
159
162
|
- **Not a production APM.** It is a toolkit that APM-like things could be
|
|
160
163
|
built on.
|
|
161
164
|
- **Not an OpenTelemetry competitor.** It should emit to OTel, not replace
|
|
162
165
|
it.
|
|
163
166
|
|
|
167
|
+
## How it was built
|
|
168
|
+
|
|
169
|
+
wrapture's code and documentation were written by an AI assistant under
|
|
170
|
+
the direction of Graham Dumpleton, the author of wrapt, through a long
|
|
171
|
+
process of specification, layered implementation, and validation against
|
|
172
|
+
real-world test suites.
|
|
173
|
+
[How wrapture was built](https://wrapture.readthedocs.io/en/latest/how-wrapture-was-built.html)
|
|
174
|
+
explains the process and the thinking.
|
|
175
|
+
|
|
164
176
|
## Requirements
|
|
165
177
|
|
|
166
178
|
- Python 3.12+
|
|
@@ -13,10 +13,10 @@ It is a sibling project to [wrapt](https://github.com/GrahamDumpleton/wrapt)
|
|
|
13
13
|
and [autowrapt](https://github.com/GrahamDumpleton/autowrapt), building on the
|
|
14
14
|
safe monkey-patching machinery wrapt provides.
|
|
15
15
|
|
|
16
|
-
> **Status:
|
|
17
|
-
>
|
|
18
|
-
>
|
|
19
|
-
>
|
|
16
|
+
> **Status: stabilising ahead of 1.0.0.** The feature set is complete for
|
|
17
|
+
> a first release and development previews are published to PyPI. The API
|
|
18
|
+
> is being exercised and tidied rather than extended, so small changes are
|
|
19
|
+
> still possible before 1.0.0.
|
|
20
20
|
|
|
21
21
|
## Installation
|
|
22
22
|
|
|
@@ -44,7 +44,7 @@ worked examples, starting with
|
|
|
44
44
|
[testing code that calls external services](https://wrapture.readthedocs.io/en/latest/example-external-services.html),
|
|
45
45
|
each take one question you might arrive with and answer it end to end.
|
|
46
46
|
|
|
47
|
-
##
|
|
47
|
+
## At a glance
|
|
48
48
|
|
|
49
49
|
None of the classes below import wrapture or know they are observed:
|
|
50
50
|
|
|
@@ -81,9 +81,11 @@ One mechanism, three uses, in increasing order of machinery:
|
|
|
81
81
|
|
|
82
82
|
2. **Unit testing.** Observe and assert on how calls actually flowed through a
|
|
83
83
|
*real* call graph (nesting, ordering, arguments and return values) and
|
|
84
|
-
optionally intervene (stub, transform, fail-inject). Unlike a
|
|
85
|
-
which fabricates values and cannot see calls an object makes to itself,
|
|
86
|
-
wrapture watches the real code run
|
|
84
|
+
optionally intervene (stub, transform, fail-inject). Unlike a
|
|
85
|
+
`unittest.mock` `Mock`, which fabricates values and cannot see calls an object makes to itself,
|
|
86
|
+
wrapture watches the real code run, and when a test must supply a
|
|
87
|
+
stand-in it provides strict, recorded ones: `stub()` for a callable,
|
|
88
|
+
spec-required `mock()` for a collaborator. This makes it possible to test code
|
|
87
89
|
with no injectable seams at all, and to assert on what *didn't* happen on
|
|
88
90
|
an error path: inject a gateway timeout, then verify the ledger was not
|
|
89
91
|
written, the receipt was not sent, and the compensating refund was issued.
|
|
@@ -123,13 +125,23 @@ testing assertion library, a tracing tool, or both at once.
|
|
|
123
125
|
|
|
124
126
|
## What it is not
|
|
125
127
|
|
|
126
|
-
- **Not a
|
|
127
|
-
|
|
128
|
+
- **Not a fabrication tool.** There is no spec-less `Mock()` here by
|
|
129
|
+
design; stand-ins are strict and built from named specs, and
|
|
130
|
+
`unittest.mock` remains the tool for invented objects.
|
|
128
131
|
- **Not a production APM.** It is a toolkit that APM-like things could be
|
|
129
132
|
built on.
|
|
130
133
|
- **Not an OpenTelemetry competitor.** It should emit to OTel, not replace
|
|
131
134
|
it.
|
|
132
135
|
|
|
136
|
+
## How it was built
|
|
137
|
+
|
|
138
|
+
wrapture's code and documentation were written by an AI assistant under
|
|
139
|
+
the direction of Graham Dumpleton, the author of wrapt, through a long
|
|
140
|
+
process of specification, layered implementation, and validation against
|
|
141
|
+
real-world test suites.
|
|
142
|
+
[How wrapture was built](https://wrapture.readthedocs.io/en/latest/how-wrapture-was-built.html)
|
|
143
|
+
explains the process and the thinking.
|
|
144
|
+
|
|
133
145
|
## Requirements
|
|
134
146
|
|
|
135
147
|
- Python 3.12+
|
|
@@ -17,7 +17,7 @@ def _format_version(parts: tuple[str, ...]) -> str:
|
|
|
17
17
|
)
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
__version_info__ = ("1", "0", "0", "
|
|
20
|
+
__version_info__ = ("1", "0", "0", "dev9")
|
|
21
21
|
__version__ = _format_version(__version_info__)
|
|
22
22
|
|
|
23
23
|
from wrapt import MISSING, register_post_import_hook, when_imported
|
|
@@ -27,9 +27,13 @@ from .asgi import (
|
|
|
27
27
|
)
|
|
28
28
|
from .behaviours import (
|
|
29
29
|
CallBehaviour,
|
|
30
|
+
CallPhase,
|
|
30
31
|
DeleteBehaviour,
|
|
32
|
+
DeletePhase,
|
|
31
33
|
GetBehaviour,
|
|
34
|
+
GetPhase,
|
|
32
35
|
SetBehaviour,
|
|
36
|
+
SetPhase,
|
|
33
37
|
)
|
|
34
38
|
from .bindings import (
|
|
35
39
|
Binding,
|
|
@@ -54,6 +58,16 @@ from .config import (
|
|
|
54
58
|
find_config,
|
|
55
59
|
load_config,
|
|
56
60
|
)
|
|
61
|
+
from .decorators import (
|
|
62
|
+
BoundSpec,
|
|
63
|
+
bound,
|
|
64
|
+
taped,
|
|
65
|
+
)
|
|
66
|
+
from .doubles import (
|
|
67
|
+
StubCallable,
|
|
68
|
+
mock,
|
|
69
|
+
stub,
|
|
70
|
+
)
|
|
57
71
|
from .eventlogs import (
|
|
58
72
|
EventLog,
|
|
59
73
|
)
|
|
@@ -69,6 +83,7 @@ from .exceptions import (
|
|
|
69
83
|
NeverAppliedError,
|
|
70
84
|
NotImplementedYetError,
|
|
71
85
|
RecordingGapWarning,
|
|
86
|
+
SequenceExhaustedError,
|
|
72
87
|
SinkErrorWarning,
|
|
73
88
|
WrongModeError,
|
|
74
89
|
)
|
|
@@ -138,18 +153,21 @@ __all__ = [
|
|
|
138
153
|
"AlreadyAppliedError",
|
|
139
154
|
"AppliedConfig",
|
|
140
155
|
"Binding",
|
|
156
|
+
"BoundSpec",
|
|
141
157
|
"Collector",
|
|
142
158
|
"Report",
|
|
143
159
|
"Run",
|
|
144
160
|
"Window",
|
|
145
161
|
"BindingGroup",
|
|
146
162
|
"CallBehaviour",
|
|
163
|
+
"CallPhase",
|
|
147
164
|
"Config",
|
|
148
165
|
"ConfigError",
|
|
149
166
|
"ConfigWarning",
|
|
150
167
|
"Counter",
|
|
151
168
|
"DeferredTargetError",
|
|
152
169
|
"DeleteBehaviour",
|
|
170
|
+
"DeletePhase",
|
|
153
171
|
"Depth",
|
|
154
172
|
"Event",
|
|
155
173
|
"EventLog",
|
|
@@ -157,6 +175,7 @@ __all__ = [
|
|
|
157
175
|
"Fanout",
|
|
158
176
|
"Filter",
|
|
159
177
|
"GetBehaviour",
|
|
178
|
+
"GetPhase",
|
|
160
179
|
"IteratorAbandonBehaviour",
|
|
161
180
|
"IteratorErrorBehaviour",
|
|
162
181
|
"IteratorFinishBehaviour",
|
|
@@ -171,11 +190,14 @@ __all__ = [
|
|
|
171
190
|
"Printer",
|
|
172
191
|
"RecordingGapWarning",
|
|
173
192
|
"Sample",
|
|
193
|
+
"SequenceExhaustedError",
|
|
174
194
|
"SetBehaviour",
|
|
195
|
+
"SetPhase",
|
|
175
196
|
"SetupEntry",
|
|
176
197
|
"Sink",
|
|
177
198
|
"SinkErrorWarning",
|
|
178
199
|
"StackFrame",
|
|
200
|
+
"StubCallable",
|
|
179
201
|
"Tape",
|
|
180
202
|
"Timeline",
|
|
181
203
|
"WSGIMiddleware",
|
|
@@ -184,6 +206,7 @@ __all__ = [
|
|
|
184
206
|
"annotate",
|
|
185
207
|
"binding",
|
|
186
208
|
"bindings",
|
|
209
|
+
"bound",
|
|
187
210
|
"canonical",
|
|
188
211
|
"chrome_trace",
|
|
189
212
|
"clear_stacks",
|
|
@@ -195,12 +218,15 @@ __all__ = [
|
|
|
195
218
|
"load_config",
|
|
196
219
|
"load_events",
|
|
197
220
|
"mermaid",
|
|
221
|
+
"mock",
|
|
198
222
|
"observed",
|
|
199
223
|
"propagate",
|
|
200
224
|
"redact",
|
|
201
225
|
"remove_sink",
|
|
202
226
|
"shutdown",
|
|
203
227
|
"stack_frames",
|
|
228
|
+
"stub",
|
|
229
|
+
"taped",
|
|
204
230
|
"timeline",
|
|
205
231
|
"window",
|
|
206
232
|
]
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
"""Composable introspection-override mixins for wrapt function wrappers.
|
|
2
|
+
|
|
3
|
+
A wrapt FunctionWrapper delegates all introspection to the callable it
|
|
4
|
+
wraps: `inspect.signature()`, the calling-convention probes
|
|
5
|
+
(`iscoroutinefunction()` and friends), `__annotations__` and the rest
|
|
6
|
+
all report what the wrapped callable says. These mixins let a
|
|
7
|
+
FunctionWrapper subclass override either axis for the wrapper itself,
|
|
8
|
+
without touching the wrapped callable:
|
|
9
|
+
|
|
10
|
+
- SignatureOverrideMixin / BoundSignatureOverrideMixin: report a given
|
|
11
|
+
`inspect.Signature` (or the signature of a prototype callable) from
|
|
12
|
+
`__signature__`, deriving `__annotations__`, `__defaults__`,
|
|
13
|
+
`__kwdefaults__` and the argument-related `__code__` attributes to
|
|
14
|
+
match, including the bound-method case where `self` is stripped.
|
|
15
|
+
- ConventionOverrideMixin / BoundConventionOverrideMixin: report a
|
|
16
|
+
named calling convention ("sync", "generator", "coroutine" or
|
|
17
|
+
"async_generator") by adjusting the flags the wrapper's `__code__`
|
|
18
|
+
exposes, so the `inspect` probes answer the override.
|
|
19
|
+
|
|
20
|
+
Each option defaults to None, meaning delegate exactly as before, so
|
|
21
|
+
listing the mixins costs nothing until an override is passed. Usage:
|
|
22
|
+
|
|
23
|
+
class BoundStub(BoundConventionOverrideMixin,
|
|
24
|
+
BoundSignatureOverrideMixin,
|
|
25
|
+
BoundFunctionWrapper):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
class Stub(ConventionOverrideMixin,
|
|
29
|
+
SignatureOverrideMixin,
|
|
30
|
+
FunctionWrapper):
|
|
31
|
+
__bound_function_wrapper__ = BoundStub
|
|
32
|
+
|
|
33
|
+
stub = Stub(template, wrapper_fn,
|
|
34
|
+
signature=prototype, convention="coroutine")
|
|
35
|
+
|
|
36
|
+
The mixins consume their keyword in a cooperative __init__ and pass
|
|
37
|
+
everything else up, so the host class chains `super().__init__` as
|
|
38
|
+
usual. The convention mixin goes outside the signature mixin (earlier
|
|
39
|
+
in the bases) as above: each layers its view over the next one's
|
|
40
|
+
`__code__`, so that order applies convention flags to signature-derived
|
|
41
|
+
argument attributes. Bound wrappers read the state from their parent
|
|
42
|
+
wrapper, so `__get__` needs no extra plumbing.
|
|
43
|
+
|
|
44
|
+
The module is self-contained over wrapt's public wrapper classes, so
|
|
45
|
+
the implementation can move into wrapt itself unchanged.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
from __future__ import annotations
|
|
49
|
+
|
|
50
|
+
import inspect
|
|
51
|
+
from inspect import (
|
|
52
|
+
CO_ASYNC_GENERATOR,
|
|
53
|
+
CO_COROUTINE,
|
|
54
|
+
CO_GENERATOR,
|
|
55
|
+
CO_ITERABLE_COROUTINE,
|
|
56
|
+
CO_VARARGS,
|
|
57
|
+
CO_VARKEYWORDS,
|
|
58
|
+
Parameter,
|
|
59
|
+
Signature,
|
|
60
|
+
)
|
|
61
|
+
from typing import Any
|
|
62
|
+
|
|
63
|
+
from wrapt import CallableObjectProxy, ObjectProxy
|
|
64
|
+
|
|
65
|
+
CONVENTIONS = ("sync", "generator", "coroutine", "async_generator")
|
|
66
|
+
|
|
67
|
+
# The code-object flag bits every convention override starts by
|
|
68
|
+
# clearing; the convention then asserts the bits it stands for.
|
|
69
|
+
|
|
70
|
+
_CONVENTION_BITS = (
|
|
71
|
+
CO_GENERATOR | CO_COROUTINE | CO_ITERABLE_COROUTINE | CO_ASYNC_GENERATOR
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
_CONVENTION_FLAGS = {
|
|
75
|
+
"sync": 0,
|
|
76
|
+
"generator": CO_GENERATOR,
|
|
77
|
+
"coroutine": CO_COROUTINE,
|
|
78
|
+
"async_generator": CO_ASYNC_GENERATOR,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _resolve_signature(signature: Any) -> Signature | None:
|
|
83
|
+
"""Accept a Signature, a prototype callable, or None."""
|
|
84
|
+
|
|
85
|
+
if signature is None or isinstance(signature, Signature):
|
|
86
|
+
return signature
|
|
87
|
+
|
|
88
|
+
return inspect.signature(signature)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _check_convention(convention: str | None) -> str | None:
|
|
92
|
+
if convention is not None and convention not in CONVENTIONS:
|
|
93
|
+
raise ValueError(
|
|
94
|
+
f"convention must be one of {', '.join(CONVENTIONS)} or None,"
|
|
95
|
+
f" got {convention!r}"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
return convention
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
# code-object proxies: each overrides one axis and falls through the rest
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class _SignatureCode(ObjectProxy[Any]):
|
|
107
|
+
"""A code-object proxy deriving the argument-related attributes from
|
|
108
|
+
a Signature; everything else falls through to the real code object.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def __init__(self, wrapped: Any, signature: Signature) -> None:
|
|
112
|
+
super().__init__(wrapped)
|
|
113
|
+
|
|
114
|
+
self._self_signature = signature
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def co_argcount(self) -> int:
|
|
118
|
+
kinds = (Parameter.POSITIONAL_ONLY, Parameter.POSITIONAL_OR_KEYWORD)
|
|
119
|
+
return sum(
|
|
120
|
+
parameter.kind in kinds
|
|
121
|
+
for parameter in self._self_signature.parameters.values()
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def co_posonlyargcount(self) -> int:
|
|
126
|
+
return sum(
|
|
127
|
+
parameter.kind is Parameter.POSITIONAL_ONLY
|
|
128
|
+
for parameter in self._self_signature.parameters.values()
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def co_kwonlyargcount(self) -> int:
|
|
133
|
+
return sum(
|
|
134
|
+
parameter.kind is Parameter.KEYWORD_ONLY
|
|
135
|
+
for parameter in self._self_signature.parameters.values()
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def co_varnames(self) -> tuple[str, ...]:
|
|
140
|
+
positional: list[str] = []
|
|
141
|
+
keyword_only: list[str] = []
|
|
142
|
+
var_positional: str | None = None
|
|
143
|
+
var_keyword: str | None = None
|
|
144
|
+
|
|
145
|
+
for parameter in self._self_signature.parameters.values():
|
|
146
|
+
if parameter.kind in (
|
|
147
|
+
Parameter.POSITIONAL_ONLY,
|
|
148
|
+
Parameter.POSITIONAL_OR_KEYWORD,
|
|
149
|
+
):
|
|
150
|
+
positional.append(parameter.name)
|
|
151
|
+
elif parameter.kind is Parameter.KEYWORD_ONLY:
|
|
152
|
+
keyword_only.append(parameter.name)
|
|
153
|
+
elif parameter.kind is Parameter.VAR_POSITIONAL:
|
|
154
|
+
var_positional = parameter.name
|
|
155
|
+
elif parameter.kind is Parameter.VAR_KEYWORD:
|
|
156
|
+
var_keyword = parameter.name
|
|
157
|
+
|
|
158
|
+
names = positional + keyword_only
|
|
159
|
+
if var_positional is not None:
|
|
160
|
+
names.append(var_positional)
|
|
161
|
+
if var_keyword is not None:
|
|
162
|
+
names.append(var_keyword)
|
|
163
|
+
|
|
164
|
+
return tuple(names)
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def co_flags(self) -> int:
|
|
168
|
+
kinds = {
|
|
169
|
+
parameter.kind for parameter in self._self_signature.parameters.values()
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
flags = int(self.__wrapped__.co_flags) & ~(CO_VARARGS | CO_VARKEYWORDS)
|
|
173
|
+
if Parameter.VAR_POSITIONAL in kinds:
|
|
174
|
+
flags |= CO_VARARGS
|
|
175
|
+
if Parameter.VAR_KEYWORD in kinds:
|
|
176
|
+
flags |= CO_VARKEYWORDS
|
|
177
|
+
|
|
178
|
+
return flags
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class _ConventionCode(ObjectProxy[Any]):
|
|
182
|
+
"""A code-object proxy asserting a calling convention in co_flags;
|
|
183
|
+
everything else falls through to the real code object."""
|
|
184
|
+
|
|
185
|
+
def __init__(self, wrapped: Any, convention: str) -> None:
|
|
186
|
+
super().__init__(wrapped)
|
|
187
|
+
|
|
188
|
+
self._self_convention = convention
|
|
189
|
+
|
|
190
|
+
@property
|
|
191
|
+
def co_flags(self) -> int:
|
|
192
|
+
flags = int(self.__wrapped__.co_flags) & ~_CONVENTION_BITS
|
|
193
|
+
return flags | _CONVENTION_FLAGS[self._self_convention]
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# ---------------------------------------------------------------------------
|
|
197
|
+
# function surrogates: what a bound wrapper hands out as __func__
|
|
198
|
+
# ---------------------------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class _SignatureFunctionSurrogate(CallableObjectProxy[Any]):
|
|
202
|
+
"""A function proxy exposing an overridden signature.
|
|
203
|
+
|
|
204
|
+
Handed out as a bound wrapper's __func__ so that inspect.signature,
|
|
205
|
+
which treats the bound wrapper as a method and consults __func__,
|
|
206
|
+
sees the override and strips self/cls as it would for a real
|
|
207
|
+
method.
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
def __init__(self, wrapped: Any, signature: Signature) -> None:
|
|
211
|
+
super().__init__(wrapped)
|
|
212
|
+
|
|
213
|
+
self._self_signature = signature
|
|
214
|
+
|
|
215
|
+
@property
|
|
216
|
+
def __signature__(self) -> Signature:
|
|
217
|
+
return self._self_signature
|
|
218
|
+
|
|
219
|
+
@property
|
|
220
|
+
def __code__(self) -> Any:
|
|
221
|
+
return _SignatureCode(self.__wrapped__.__code__, self._self_signature)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class _ConventionFunctionSurrogate(CallableObjectProxy[Any]):
|
|
225
|
+
"""A function proxy exposing an overridden calling convention."""
|
|
226
|
+
|
|
227
|
+
def __init__(self, wrapped: Any, convention: str) -> None:
|
|
228
|
+
super().__init__(wrapped)
|
|
229
|
+
|
|
230
|
+
self._self_convention = convention
|
|
231
|
+
|
|
232
|
+
@property
|
|
233
|
+
def __code__(self) -> Any:
|
|
234
|
+
return _ConventionCode(self.__wrapped__.__code__, self._self_convention)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
# ---------------------------------------------------------------------------
|
|
238
|
+
# the mixins
|
|
239
|
+
# ---------------------------------------------------------------------------
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _annotations_of(signature: Signature) -> dict[str, Any]:
|
|
243
|
+
annotations = {
|
|
244
|
+
parameter.name: parameter.annotation
|
|
245
|
+
for parameter in signature.parameters.values()
|
|
246
|
+
if parameter.annotation is not Parameter.empty
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if signature.return_annotation is not Signature.empty:
|
|
250
|
+
annotations["return"] = signature.return_annotation
|
|
251
|
+
|
|
252
|
+
return annotations
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _defaults_of(signature: Signature) -> tuple[Any, ...] | None:
|
|
256
|
+
defaults = tuple(
|
|
257
|
+
parameter.default
|
|
258
|
+
for parameter in signature.parameters.values()
|
|
259
|
+
if parameter.kind
|
|
260
|
+
in (Parameter.POSITIONAL_ONLY, Parameter.POSITIONAL_OR_KEYWORD)
|
|
261
|
+
and parameter.default is not Parameter.empty
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
return defaults or None
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _kwdefaults_of(signature: Signature) -> dict[str, Any] | None:
|
|
268
|
+
kwdefaults = {
|
|
269
|
+
parameter.name: parameter.default
|
|
270
|
+
for parameter in signature.parameters.values()
|
|
271
|
+
if parameter.kind is Parameter.KEYWORD_ONLY
|
|
272
|
+
and parameter.default is not Parameter.empty
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return kwdefaults or None
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class SignatureOverrideMixin:
|
|
279
|
+
"""Report a chosen signature from a FunctionWrapper subclass.
|
|
280
|
+
|
|
281
|
+
Consumes the keyword-only `signature` at construction: an
|
|
282
|
+
inspect.Signature, a prototype callable whose signature is taken,
|
|
283
|
+
or None to delegate to the wrapped callable exactly as without the
|
|
284
|
+
mixin. With an override in place, `__signature__`,
|
|
285
|
+
`__annotations__`, `__defaults__`, `__kwdefaults__` and the
|
|
286
|
+
argument-related `__code__` attributes all answer from it.
|
|
287
|
+
"""
|
|
288
|
+
|
|
289
|
+
def __init__(self, *args: Any, signature: Any = None, **kwargs: Any) -> None:
|
|
290
|
+
super().__init__(*args, **kwargs)
|
|
291
|
+
|
|
292
|
+
self._self_signature = _resolve_signature(signature)
|
|
293
|
+
|
|
294
|
+
@property
|
|
295
|
+
def __signature__(self) -> Signature:
|
|
296
|
+
signature = self._self_signature
|
|
297
|
+
if signature is None:
|
|
298
|
+
raise AttributeError("__signature__")
|
|
299
|
+
|
|
300
|
+
return signature
|
|
301
|
+
|
|
302
|
+
@property
|
|
303
|
+
def __annotations__(self) -> dict[str, Any]: # type: ignore[override]
|
|
304
|
+
signature = self._self_signature
|
|
305
|
+
if signature is None:
|
|
306
|
+
raise AttributeError("__annotations__")
|
|
307
|
+
|
|
308
|
+
return _annotations_of(signature)
|
|
309
|
+
|
|
310
|
+
@property
|
|
311
|
+
def __defaults__(self) -> tuple[Any, ...] | None:
|
|
312
|
+
signature = self._self_signature
|
|
313
|
+
if signature is None:
|
|
314
|
+
raise AttributeError("__defaults__")
|
|
315
|
+
|
|
316
|
+
return _defaults_of(signature)
|
|
317
|
+
|
|
318
|
+
@property
|
|
319
|
+
def __kwdefaults__(self) -> dict[str, Any] | None:
|
|
320
|
+
signature = self._self_signature
|
|
321
|
+
if signature is None:
|
|
322
|
+
raise AttributeError("__kwdefaults__")
|
|
323
|
+
|
|
324
|
+
return _kwdefaults_of(signature)
|
|
325
|
+
|
|
326
|
+
@property
|
|
327
|
+
def __code__(self) -> Any:
|
|
328
|
+
# Layer over whatever the next class in the MRO reports, so
|
|
329
|
+
# several overrides compose; the base classes define no
|
|
330
|
+
# __code__ descriptor, so the fallback is the wrapped
|
|
331
|
+
# callable's own code object.
|
|
332
|
+
|
|
333
|
+
try:
|
|
334
|
+
code = super().__code__ # type: ignore[misc]
|
|
335
|
+
except AttributeError:
|
|
336
|
+
code = self.__wrapped__.__code__ # type: ignore[attr-defined]
|
|
337
|
+
|
|
338
|
+
signature = self._self_signature
|
|
339
|
+
if signature is None:
|
|
340
|
+
return code
|
|
341
|
+
|
|
342
|
+
return _SignatureCode(code, signature)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
class BoundSignatureOverrideMixin:
|
|
346
|
+
"""The bound-wrapper side of SignatureOverrideMixin.
|
|
347
|
+
|
|
348
|
+
Reads the override from the parent wrapper, and hands out a
|
|
349
|
+
__func__ surrogate carrying it so inspect.signature on the bound
|
|
350
|
+
wrapper strips self/cls exactly as for a real method.
|
|
351
|
+
"""
|
|
352
|
+
|
|
353
|
+
@property
|
|
354
|
+
def __signature__(self) -> Signature:
|
|
355
|
+
signature: Signature | None = self._self_parent._self_signature # type: ignore[attr-defined]
|
|
356
|
+
if signature is None:
|
|
357
|
+
raise AttributeError("__signature__")
|
|
358
|
+
|
|
359
|
+
return signature
|
|
360
|
+
|
|
361
|
+
@property
|
|
362
|
+
def __func__(self) -> Any:
|
|
363
|
+
signature = self._self_parent._self_signature # type: ignore[attr-defined]
|
|
364
|
+
if signature is None:
|
|
365
|
+
raise AttributeError("__func__")
|
|
366
|
+
|
|
367
|
+
wrapped = self.__wrapped__ # type: ignore[attr-defined]
|
|
368
|
+
function = getattr(wrapped, "__func__", wrapped)
|
|
369
|
+
|
|
370
|
+
return _SignatureFunctionSurrogate(function, signature)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
class ConventionOverrideMixin:
|
|
374
|
+
"""Report a chosen calling convention from a FunctionWrapper
|
|
375
|
+
subclass.
|
|
376
|
+
|
|
377
|
+
Consumes the keyword-only `convention` at construction: "sync",
|
|
378
|
+
"generator", "coroutine", "async_generator", or None to delegate to
|
|
379
|
+
the wrapped callable exactly as without the mixin. With an override
|
|
380
|
+
in place the wrapper's `__code__` asserts the matching flags, so
|
|
381
|
+
`inspect.iscoroutinefunction()`, `isasyncgenfunction()` and
|
|
382
|
+
`isgeneratorfunction()` answer the override; the sync-direction
|
|
383
|
+
conventions also set `_self_is_not_coroutine`, the authoritative
|
|
384
|
+
marker wrapt's own convention checks respect through wrapper
|
|
385
|
+
chains.
|
|
386
|
+
"""
|
|
387
|
+
|
|
388
|
+
def __init__(
|
|
389
|
+
self, *args: Any, convention: str | None = None, **kwargs: Any
|
|
390
|
+
) -> None:
|
|
391
|
+
super().__init__(*args, **kwargs)
|
|
392
|
+
|
|
393
|
+
self._self_convention = _check_convention(convention)
|
|
394
|
+
if convention in ("sync", "generator"):
|
|
395
|
+
self._self_is_not_coroutine = True
|
|
396
|
+
|
|
397
|
+
@property
|
|
398
|
+
def __code__(self) -> Any:
|
|
399
|
+
try:
|
|
400
|
+
code = super().__code__ # type: ignore[misc]
|
|
401
|
+
except AttributeError:
|
|
402
|
+
code = self.__wrapped__.__code__ # type: ignore[attr-defined]
|
|
403
|
+
|
|
404
|
+
convention = self._self_convention
|
|
405
|
+
if convention is None:
|
|
406
|
+
return code
|
|
407
|
+
|
|
408
|
+
return _ConventionCode(code, convention)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
class BoundConventionOverrideMixin:
|
|
412
|
+
"""The bound-wrapper side of ConventionOverrideMixin.
|
|
413
|
+
|
|
414
|
+
Reads the override from the parent wrapper, and hands out a
|
|
415
|
+
__func__ surrogate carrying it, layered over any surrogate the next
|
|
416
|
+
mixin in the MRO provides so several overrides compose. With no
|
|
417
|
+
override of its own it passes the next layer's surrogate through,
|
|
418
|
+
and raises AttributeError only when no layer has one, which lets
|
|
419
|
+
attribute delegation take over as if the property were absent.
|
|
420
|
+
"""
|
|
421
|
+
|
|
422
|
+
@property
|
|
423
|
+
def __func__(self) -> Any:
|
|
424
|
+
convention = self._self_parent._self_convention # type: ignore[attr-defined]
|
|
425
|
+
|
|
426
|
+
try:
|
|
427
|
+
function = super().__func__ # type: ignore[misc]
|
|
428
|
+
except AttributeError:
|
|
429
|
+
if convention is None:
|
|
430
|
+
raise
|
|
431
|
+
|
|
432
|
+
wrapped = self.__wrapped__ # type: ignore[attr-defined]
|
|
433
|
+
function = getattr(wrapped, "__func__", wrapped)
|
|
434
|
+
|
|
435
|
+
if convention is None:
|
|
436
|
+
return function
|
|
437
|
+
|
|
438
|
+
return _ConventionFunctionSurrogate(function, convention)
|