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.
Files changed (87) hide show
  1. {wrapture-1.0.0.dev7/src/wrapture.egg-info → wrapture-1.0.0.dev9}/PKG-INFO +23 -11
  2. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/README.md +22 -10
  3. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/__init__.py +27 -1
  4. wrapture-1.0.0.dev9/src/wrapture/_wrappermixins.py +438 -0
  5. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/attributes.py +343 -64
  6. wrapture-1.0.0.dev9/src/wrapture/behaviours.py +947 -0
  7. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/bindings.py +1136 -137
  8. wrapture-1.0.0.dev9/src/wrapture/decorators.py +702 -0
  9. wrapture-1.0.0.dev9/src/wrapture/doubles.py +452 -0
  10. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/eventlogs.py +72 -7
  11. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/events.py +38 -3
  12. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/exceptions.py +10 -0
  13. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/export.py +1 -0
  14. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/iterators.py +53 -19
  15. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/observed.py +113 -29
  16. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/sinks.py +4 -0
  17. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/timeline.py +128 -18
  18. wrapture-1.0.0.dev9/src/wrapture/values.py +163 -0
  19. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/windows.py +24 -2
  20. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9/src/wrapture.egg-info}/PKG-INFO +23 -11
  21. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture.egg-info/SOURCES.txt +13 -0
  22. wrapture-1.0.0.dev9/tests/test_async_stubs.py +450 -0
  23. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_attributes.py +4 -15
  24. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_behaviours.py +114 -1
  25. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_bindings.py +66 -5
  26. wrapture-1.0.0.dev9/tests/test_decorators.py +647 -0
  27. wrapture-1.0.0.dev9/tests/test_doubles.py +678 -0
  28. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_eventlogs.py +166 -0
  29. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_iterators.py +50 -0
  30. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_jsonlines.py +34 -0
  31. wrapture-1.0.0.dev9/tests/test_mapping.py +383 -0
  32. wrapture-1.0.0.dev9/tests/test_module_attributes.py +389 -0
  33. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_observed.py +107 -0
  34. wrapture-1.0.0.dev9/tests/test_phases.py +920 -0
  35. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_recording.py +7 -5
  36. wrapture-1.0.0.dev9/tests/test_strict.py +228 -0
  37. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_timeline.py +205 -3
  38. wrapture-1.0.0.dev9/tests/test_values.py +535 -0
  39. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_windows.py +76 -0
  40. wrapture-1.0.0.dev9/tests/test_wrappermixins.py +214 -0
  41. wrapture-1.0.0.dev7/src/wrapture/behaviours.py +0 -428
  42. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/LICENSE +0 -0
  43. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/pyproject.toml +0 -0
  44. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/setup.cfg +0 -0
  45. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/__main__.py +0 -0
  46. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/asgi.py +0 -0
  47. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/bootstrap.py +0 -0
  48. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/capture.py +0 -0
  49. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/collectors.py +0 -0
  50. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/config.py +0 -0
  51. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/lifecycle.py +0 -0
  52. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/outputs.py +0 -0
  53. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/py.typed +0 -0
  54. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/pytest_plugin.py +0 -0
  55. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/scheduler.py +0 -0
  56. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/stacks.py +0 -0
  57. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/tools/__init__.py +0 -0
  58. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/tools/__main__.py +0 -0
  59. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/tools/convert.py +0 -0
  60. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture/wsgi.py +0 -0
  61. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture.egg-info/dependency_links.txt +0 -0
  62. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture.egg-info/entry_points.txt +0 -0
  63. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture.egg-info/not-zip-safe +0 -0
  64. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture.egg-info/requires.txt +0 -0
  65. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/src/wrapture.egg-info/top_level.txt +0 -0
  66. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_asgi.py +0 -0
  67. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_attribute_events.py +0 -0
  68. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_bootstrap.py +0 -0
  69. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_capture.py +0 -0
  70. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_collectors.py +0 -0
  71. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_config.py +0 -0
  72. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_events.py +0 -0
  73. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_export.py +0 -0
  74. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_generator_events.py +0 -0
  75. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_outputs.py +0 -0
  76. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_predicates.py +0 -0
  77. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_pytest_plugin.py +0 -0
  78. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_runner.py +0 -0
  79. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_sink_library.py +0 -0
  80. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_sinks.py +0 -0
  81. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_stacks.py +0 -0
  82. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_targets.py +0 -0
  83. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_testing_patterns.py +0 -0
  84. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_threads.py +0 -0
  85. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_tools.py +0 -0
  86. {wrapture-1.0.0.dev7 → wrapture-1.0.0.dev9}/tests/test_version.py +0 -0
  87. {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.dev7
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: early development.** The monkey patching, unit testing and
48
- > ad-hoc tracing layers are implemented, including WSGI and ASGI request
49
- > tracing. Development previews are published to PyPI; the API may still
50
- > shift before 1.0.0.
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
- ## Thirty seconds of it
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 `Mock`,
116
- which fabricates values and cannot see calls an object makes to itself,
117
- wrapture watches the real code run. This makes it possible to test code
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 replacement for `unittest.mock`.** It complements mocking where
158
- code has seams; it exists for the code that doesn't.
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: early development.** The monkey patching, unit testing and
17
- > ad-hoc tracing layers are implemented, including WSGI and ASGI request
18
- > tracing. Development previews are published to PyPI; the API may still
19
- > shift before 1.0.0.
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
- ## Thirty seconds of it
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 `Mock`,
85
- which fabricates values and cannot see calls an object makes to itself,
86
- wrapture watches the real code run. This makes it possible to test code
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 replacement for `unittest.mock`.** It complements mocking where
127
- code has seams; it exists for the code that doesn't.
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", "dev7")
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)