Repository navigation
Expand file tree
/
Copy pathtest_wrappermixins.py
More file actions
214 lines (140 loc) · 6.58 KB
/
Copy pathtest_wrappermixins.py
File metadata and controls
214 lines (140 loc) · 6.58 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
"""Tests for the signature and convention override mixins.
The mixins compose over wrapt's public FunctionWrapper machinery: each
consumes its keyword cooperatively, defaults to delegating exactly as
an unmixed wrapper would, and overrides what introspection reports
without touching the wrapped callable. A subprocess case runs the core
assertions on the pure-Python wrapt implementation as well, since
working identically over both builds is part of the contract.
"""
from __future__ import annotations
import inspect
from typing import Any
import pytest
from wrapt import BoundFunctionWrapper, FunctionWrapper
from wrapture._wrappermixins import (
BoundConventionOverrideMixin,
BoundSignatureOverrideMixin,
ConventionOverrideMixin,
SignatureOverrideMixin,
)
class BoundStub(
BoundConventionOverrideMixin,
BoundSignatureOverrideMixin,
BoundFunctionWrapper[Any, Any],
):
pass
class Stub(
ConventionOverrideMixin,
SignatureOverrideMixin,
FunctionWrapper[Any, Any],
):
__bound_function_wrapper__ = BoundStub
def passthrough(
wrapped: Any, instance: Any, args: tuple[Any, ...], kwargs: dict[str, Any]
) -> Any:
return wrapped(*args, **kwargs)
def template(*args: Any, **kwargs: Any) -> Any:
return ("called", args, kwargs)
def prototype(amount: int, currency: str = "USD", *, retries: int = 3) -> None: ...
# ---------------------------------------------------------------------------
# defaults: no override means delegate exactly as before
# ---------------------------------------------------------------------------
def test_without_overrides_everything_delegates() -> None:
def real(a: int, b: int = 2) -> int:
return a + b
wrapper = Stub(real, passthrough)
assert inspect.signature(wrapper) == inspect.signature(real)
assert wrapper.__defaults__ == (2,)
assert not inspect.iscoroutinefunction(wrapper)
assert wrapper(1) == 3
def test_conventions_delegate_when_not_overridden() -> None:
async def fetch(url: str) -> str:
return url
wrapper = Stub(fetch, passthrough)
assert inspect.iscoroutinefunction(wrapper)
assert not inspect.isasyncgenfunction(wrapper)
# ---------------------------------------------------------------------------
# signature override
# ---------------------------------------------------------------------------
def test_signature_override_from_a_prototype() -> None:
wrapper = Stub(template, passthrough, signature=prototype)
signature = inspect.signature(wrapper)
assert list(signature.parameters) == ["amount", "currency", "retries"]
assert wrapper.__defaults__ == ("USD",)
assert wrapper.__kwdefaults__ == {"retries": 3}
# Under PEP 563 (the future import above) annotations are strings.
assert wrapper.__annotations__["amount"] == "int"
code = wrapper.__code__
assert code.co_argcount == 2
assert code.co_kwonlyargcount == 1
assert code.co_varnames[:3] == ("amount", "currency", "retries")
def test_signature_override_from_a_prebuilt_signature() -> None:
built = inspect.signature(prototype)
wrapper = Stub(template, passthrough, signature=built)
assert inspect.signature(wrapper) is built
def test_signature_override_does_not_change_calling() -> None:
wrapper = Stub(template, passthrough, signature=prototype)
assert wrapper(1, 2, x=3) == ("called", (1, 2), {"x": 3})
# ---------------------------------------------------------------------------
# convention override
# ---------------------------------------------------------------------------
def test_each_convention_reports_its_probes() -> None:
cases = {
"sync": (False, False, False),
"generator": (False, False, True),
"coroutine": (True, False, False),
"async_generator": (False, True, False),
}
for convention, (coro, agen, gen) in cases.items():
wrapper = Stub(template, passthrough, convention=convention)
assert inspect.iscoroutinefunction(wrapper) is coro, convention
assert inspect.isasyncgenfunction(wrapper) is agen, convention
assert inspect.isgeneratorfunction(wrapper) is gen, convention
def test_convention_sync_overrides_an_async_target() -> None:
async def fetch(url: str) -> str:
return url
wrapper = Stub(fetch, passthrough, convention="sync")
assert not inspect.iscoroutinefunction(wrapper)
assert wrapper._self_is_not_coroutine is True
def test_an_unknown_convention_is_refused() -> None:
with pytest.raises(ValueError, match="convention must be one of"):
Stub(template, passthrough, convention="agenda")
# ---------------------------------------------------------------------------
# both overrides together
# ---------------------------------------------------------------------------
def test_overrides_compose_in_the_blessed_order() -> None:
wrapper = Stub(template, passthrough, signature=prototype, convention="coroutine")
assert inspect.iscoroutinefunction(wrapper)
signature = inspect.signature(wrapper)
assert list(signature.parameters) == ["amount", "currency", "retries"]
code = wrapper.__code__
assert code.co_argcount == 2
# ---------------------------------------------------------------------------
# bound wrappers: placed on a class
# ---------------------------------------------------------------------------
def method_prototype(self: Any, amount: int, currency: str = "USD") -> None: ...
class Holder:
# Annotated Any so mypy does not push the descriptor protocol's
# generics onto call sites; the runtime behaviour is the point here.
plain: Any = Stub(template, passthrough)
shaped: Any = Stub(template, passthrough, signature=method_prototype)
marked: Any = Stub(template, passthrough, convention="coroutine")
both: Any = Stub(
template, passthrough, signature=method_prototype, convention="coroutine"
)
def test_bound_signature_override_strips_self() -> None:
holder = Holder()
signature = inspect.signature(holder.shaped)
assert list(signature.parameters) == ["amount", "currency"]
def test_bound_convention_override_reports_through_binding() -> None:
holder = Holder()
assert inspect.iscoroutinefunction(holder.marked)
assert not inspect.iscoroutinefunction(holder.plain)
def test_bound_overrides_compose() -> None:
holder = Holder()
assert inspect.iscoroutinefunction(holder.both)
assert list(inspect.signature(holder.both).parameters) == ["amount", "currency"]
def test_bound_wrapper_without_overrides_delegates() -> None:
holder = Holder()
assert holder.plain(1, x=2)[0] == "called"
assert isinstance(holder.plain, BoundStub)