decoy 2.2.2__py3-none-any.whl → 2.4.0__py3-none-any.whl
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.
- decoy/call_handler.py +7 -2
- decoy/errors.py +28 -26
- decoy/matchers.py +63 -48
- decoy/next/__init__.py +21 -0
- decoy/next/_internal/__init__.py +0 -0
- decoy/next/_internal/compare.py +138 -0
- decoy/next/_internal/decoy.py +231 -0
- decoy/next/_internal/errors.py +103 -0
- decoy/next/_internal/inspect.py +229 -0
- decoy/next/_internal/matcher.py +328 -0
- decoy/next/_internal/mock.py +220 -0
- decoy/next/_internal/state.py +265 -0
- decoy/next/_internal/stringify.py +80 -0
- decoy/next/_internal/values.py +96 -0
- decoy/next/_internal/verify.py +78 -0
- decoy/next/_internal/warnings.py +35 -0
- decoy/next/_internal/when.py +136 -0
- decoy/spy.py +1 -1
- {decoy-2.2.2.dist-info → decoy-2.4.0.dist-info}/METADATA +9 -17
- decoy-2.4.0.dist-info/RECORD +38 -0
- {decoy-2.2.2.dist-info → decoy-2.4.0.dist-info}/WHEEL +1 -1
- decoy-2.4.0.dist-info/entry_points.txt +3 -0
- decoy-2.2.2.dist-info/RECORD +0 -25
- decoy-2.2.2.dist-info/entry_points.txt +0 -3
- decoy-2.2.2.dist-info/licenses/LICENSE +0 -21
decoy/call_handler.py
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import Any, NamedTuple, Optional
|
|
4
4
|
|
|
5
|
-
from .spy_log import SpyLog
|
|
6
5
|
from .context_managers import ContextWrapper
|
|
7
|
-
from .spy_events import SpyCall, SpyEvent
|
|
6
|
+
from .spy_events import PropAccessType, SpyCall, SpyEvent, SpyPropAccess
|
|
7
|
+
from .spy_log import SpyLog
|
|
8
8
|
from .stub_store import MISSING, StubStore
|
|
9
9
|
|
|
10
10
|
|
|
@@ -41,6 +41,11 @@ class CallHandler:
|
|
|
41
41
|
*call.payload.args,
|
|
42
42
|
**call.payload.kwargs,
|
|
43
43
|
)
|
|
44
|
+
elif (
|
|
45
|
+
isinstance(call.payload, SpyPropAccess)
|
|
46
|
+
and call.payload.access_type == PropAccessType.SET
|
|
47
|
+
):
|
|
48
|
+
return_value = behavior.action(call.payload.value)
|
|
44
49
|
else:
|
|
45
50
|
return_value = behavior.action()
|
|
46
51
|
|
decoy/errors.py
CHANGED
|
@@ -12,12 +12,7 @@ from .stringify import count, stringify_error_message
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class MockNameRequiredError(ValueError):
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
See the [MockNameRequiredError guide][] for more details.
|
|
18
|
-
|
|
19
|
-
[MockNameRequiredError guide]: usage/errors-and-warnings.md#mocknamerequirederror
|
|
20
|
-
"""
|
|
15
|
+
"""A name was not provided for a mock."""
|
|
21
16
|
|
|
22
17
|
@classmethod
|
|
23
18
|
def create(cls) -> "MockNameRequiredError":
|
|
@@ -25,17 +20,17 @@ class MockNameRequiredError(ValueError):
|
|
|
25
20
|
return cls("Mocks without `cls` or `func` require a `name`.")
|
|
26
21
|
|
|
27
22
|
|
|
23
|
+
class MockSpecInvalidError(TypeError):
|
|
24
|
+
"""A value passed as a mock spec is not valid."""
|
|
25
|
+
|
|
26
|
+
|
|
28
27
|
class MissingRehearsalError(ValueError):
|
|
29
|
-
"""
|
|
28
|
+
"""A Decoy method was called without rehearsal(s).
|
|
30
29
|
|
|
31
30
|
This error is raised if you use [`when`][decoy.Decoy.when],
|
|
32
31
|
[`verify`][decoy.Decoy.verify], or [`prop`][decoy.Decoy.prop] incorrectly
|
|
33
32
|
in your tests. When using async/await, this error can be triggered if you
|
|
34
33
|
forget to include `await` with your rehearsal.
|
|
35
|
-
|
|
36
|
-
See the [MissingRehearsalError guide][] for more details.
|
|
37
|
-
|
|
38
|
-
[MissingRehearsalError guide]: usage/errors-and-warnings.md#missingrehearsalerror
|
|
39
34
|
"""
|
|
40
35
|
|
|
41
36
|
@classmethod
|
|
@@ -44,29 +39,28 @@ class MissingRehearsalError(ValueError):
|
|
|
44
39
|
return cls("Rehearsal not found.")
|
|
45
40
|
|
|
46
41
|
|
|
42
|
+
class NotAMockError(TypeError):
|
|
43
|
+
"""A Decoy method was called without a mock."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ThenDoActionNotCallableError(TypeError):
|
|
47
|
+
"""A value passed to `then_do` is not callable."""
|
|
48
|
+
|
|
49
|
+
|
|
47
50
|
class MockNotAsyncError(TypeError):
|
|
48
|
-
"""An
|
|
51
|
+
"""An asynchronous function was passed to a synchronous mock.
|
|
49
52
|
|
|
50
53
|
This error is raised if you pass an `async def` function
|
|
51
|
-
to a synchronous stub's `then_do` method.
|
|
52
|
-
See the [MockNotAsyncError guide][] for more details.
|
|
53
|
-
|
|
54
|
-
[MockNotAsyncError guide]: usage/errors-and-warnings.md#mocknotasyncerror
|
|
54
|
+
to a synchronous stub's [`then_do`][decoy.Stub.then_do] method.
|
|
55
55
|
"""
|
|
56
56
|
|
|
57
57
|
|
|
58
|
-
class
|
|
59
|
-
"""
|
|
58
|
+
class SignatureMismatchError(TypeError):
|
|
59
|
+
"""Arguments did not match the signature of the mock."""
|
|
60
60
|
|
|
61
|
-
See [spying with verify][] for more details.
|
|
62
61
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
Attributes:
|
|
66
|
-
rehearsals: Rehearsals that were being verified.
|
|
67
|
-
calls: Actual calls to the mock(s).
|
|
68
|
-
times: The expected number of calls to the mock, if any.
|
|
69
|
-
"""
|
|
62
|
+
class VerifyError(AssertionError):
|
|
63
|
+
"""A [`Decoy.verify`][decoy.Decoy.verify] assertion failed."""
|
|
70
64
|
|
|
71
65
|
rehearsals: Sequence[VerifyRehearsal]
|
|
72
66
|
calls: Sequence[SpyEvent]
|
|
@@ -100,3 +94,11 @@ class VerifyError(AssertionError):
|
|
|
100
94
|
result.times = times
|
|
101
95
|
|
|
102
96
|
return result
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class VerifyOrderError(VerifyError):
|
|
100
|
+
"""A [`Decoy.verify_order`][decoy.next.Decoy.verify_order] assertion failed."""
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class NoMatcherValueCapturedError(ValueError):
|
|
104
|
+
"""An error raised if a [decoy.next.Matcher][] has not captured any matching values."""
|
decoy/matchers.py
CHANGED
|
@@ -28,16 +28,17 @@ See the [matchers guide][] for more details.
|
|
|
28
28
|
"""
|
|
29
29
|
|
|
30
30
|
from re import compile as compile_re
|
|
31
|
-
from typing import
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
31
|
+
from typing import (
|
|
32
|
+
Any,
|
|
33
|
+
Generic,
|
|
34
|
+
List,
|
|
35
|
+
Mapping,
|
|
36
|
+
Optional,
|
|
37
|
+
Pattern,
|
|
38
|
+
Type,
|
|
39
|
+
TypeVar,
|
|
40
|
+
cast,
|
|
41
|
+
)
|
|
41
42
|
|
|
42
43
|
|
|
43
44
|
class _AnythingOrNone:
|
|
@@ -177,14 +178,11 @@ class _HasAttributes:
|
|
|
177
178
|
|
|
178
179
|
def __eq__(self, target: object) -> bool:
|
|
179
180
|
"""Return true if target matches all given attributes."""
|
|
180
|
-
is_match = True
|
|
181
181
|
for attr_name, value in self._attributes.items():
|
|
182
|
-
if
|
|
183
|
-
|
|
184
|
-
hasattr(target, attr_name) and getattr(target, attr_name) == value
|
|
185
|
-
)
|
|
182
|
+
if not hasattr(target, attr_name) or getattr(target, attr_name) != value:
|
|
183
|
+
return False
|
|
186
184
|
|
|
187
|
-
return
|
|
185
|
+
return True
|
|
188
186
|
|
|
189
187
|
def __repr__(self) -> str:
|
|
190
188
|
"""Return a string representation of the matcher."""
|
|
@@ -218,16 +216,14 @@ class _DictMatching:
|
|
|
218
216
|
|
|
219
217
|
def __eq__(self, target: object) -> bool:
|
|
220
218
|
"""Return true if target matches all given keys/values."""
|
|
221
|
-
is_match = True
|
|
222
|
-
|
|
223
219
|
for key, value in self._values.items():
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
220
|
+
try:
|
|
221
|
+
if key not in target or target[key] != value: # type: ignore[index,operator]
|
|
222
|
+
return False
|
|
223
|
+
except TypeError:
|
|
224
|
+
return False
|
|
229
225
|
|
|
230
|
-
return
|
|
226
|
+
return True
|
|
231
227
|
|
|
232
228
|
def __repr__(self) -> str:
|
|
233
229
|
"""Return a string representation of the matcher."""
|
|
@@ -318,10 +314,12 @@ def StringMatching(match: str) -> str:
|
|
|
318
314
|
class _ErrorMatching:
|
|
319
315
|
_error_type: Type[BaseException]
|
|
320
316
|
_string_matcher: Optional[_StringMatching]
|
|
317
|
+
_match: Optional[str]
|
|
321
318
|
|
|
322
319
|
def __init__(self, error: Type[BaseException], match: Optional[str] = None) -> None:
|
|
323
320
|
"""Initialize with the Exception type and optional message matcher."""
|
|
324
321
|
self._error_type = error
|
|
322
|
+
self._match = match
|
|
325
323
|
self._string_matcher = _StringMatching(match) if match is not None else None
|
|
326
324
|
|
|
327
325
|
def __eq__(self, target: object) -> bool:
|
|
@@ -337,9 +335,7 @@ class _ErrorMatching:
|
|
|
337
335
|
|
|
338
336
|
def __repr__(self) -> str:
|
|
339
337
|
"""Return a string representation of the matcher."""
|
|
340
|
-
return
|
|
341
|
-
f"<ErrorMatching {self._error_type.__name__} match={self._string_matcher}>"
|
|
342
|
-
)
|
|
338
|
+
return f"<ErrorMatching {self._error_type.__name__} match={self._match!r}>"
|
|
343
339
|
|
|
344
340
|
|
|
345
341
|
ErrorT = TypeVar("ErrorT", bound=BaseException)
|
|
@@ -361,12 +357,32 @@ def ErrorMatching(error: Type[ErrorT], match: Optional[str] = None) -> ErrorT:
|
|
|
361
357
|
return cast(ErrorT, _ErrorMatching(error, match))
|
|
362
358
|
|
|
363
359
|
|
|
364
|
-
|
|
360
|
+
CapturedT = TypeVar("CapturedT")
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
class ValueCaptor(Generic[CapturedT]):
|
|
364
|
+
"""Match anything, capturing its value for further assertions.
|
|
365
|
+
|
|
366
|
+
Compare against the `matcher` property to capture a value.
|
|
367
|
+
The last captured value is available via `captor.value`,
|
|
368
|
+
while all captured values are stored in `captor.values`.
|
|
369
|
+
|
|
370
|
+
!!! example
|
|
371
|
+
```python
|
|
372
|
+
captor = ValueCaptor[str]()
|
|
373
|
+
assert "foobar" == captor.matcher
|
|
374
|
+
print(captor.value) # "foobar"
|
|
375
|
+
print(captor.values) # ["foobar"]
|
|
376
|
+
```
|
|
377
|
+
"""
|
|
378
|
+
|
|
379
|
+
_values: List[object]
|
|
380
|
+
|
|
365
381
|
def __init__(self) -> None:
|
|
366
|
-
self._values
|
|
382
|
+
self._values = []
|
|
367
383
|
|
|
368
384
|
def __eq__(self, target: object) -> bool:
|
|
369
|
-
"""
|
|
385
|
+
"""Captors are always "equal" to a given target."""
|
|
370
386
|
self._values.append(target)
|
|
371
387
|
return True
|
|
372
388
|
|
|
@@ -375,11 +391,19 @@ class _Captor:
|
|
|
375
391
|
return "<Captor>"
|
|
376
392
|
|
|
377
393
|
@property
|
|
378
|
-
def
|
|
379
|
-
"""
|
|
394
|
+
def matcher(self) -> CapturedT:
|
|
395
|
+
"""Match anything, capturing its value.
|
|
396
|
+
|
|
397
|
+
This method exists as a type-checking convenience.
|
|
398
|
+
"""
|
|
399
|
+
return cast(CapturedT, self)
|
|
400
|
+
|
|
401
|
+
@property
|
|
402
|
+
def value(self) -> object:
|
|
403
|
+
"""The latest captured value.
|
|
380
404
|
|
|
381
405
|
Raises:
|
|
382
|
-
AssertionError:
|
|
406
|
+
AssertionError: no value has been captured.
|
|
383
407
|
"""
|
|
384
408
|
if len(self._values) == 0:
|
|
385
409
|
raise AssertionError("No value captured by captor.")
|
|
@@ -387,24 +411,15 @@ class _Captor:
|
|
|
387
411
|
return self._values[-1]
|
|
388
412
|
|
|
389
413
|
@property
|
|
390
|
-
def values(self) -> List[
|
|
391
|
-
"""
|
|
414
|
+
def values(self) -> List[object]:
|
|
415
|
+
"""All captured values."""
|
|
392
416
|
return self._values
|
|
393
417
|
|
|
394
418
|
|
|
395
419
|
def Captor() -> Any:
|
|
396
|
-
"""Match anything, capturing its value.
|
|
420
|
+
"""Match anything, capturing its value for further assertions.
|
|
397
421
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
helpful if a captor needs to be triggered multiple times.
|
|
401
|
-
|
|
402
|
-
!!! example
|
|
403
|
-
```python
|
|
404
|
-
captor = Captor()
|
|
405
|
-
assert "foobar" == captor
|
|
406
|
-
print(captor.value) # "foobar"
|
|
407
|
-
print(captor.values) # ["foobar"]
|
|
408
|
-
```
|
|
422
|
+
!!! tip
|
|
423
|
+
Prefer [decoy.matchers.ValueCaptor][], which has better type annotations.
|
|
409
424
|
"""
|
|
410
|
-
return
|
|
425
|
+
return ValueCaptor()
|
decoy/next/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Decoy mocking library.
|
|
2
|
+
|
|
3
|
+
Use Decoy to create stubs and spies
|
|
4
|
+
to isolate your code under test.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from ._internal.decoy import Decoy
|
|
8
|
+
from ._internal.matcher import Matcher
|
|
9
|
+
from ._internal.mock import AsyncMock, Mock
|
|
10
|
+
from ._internal.verify import Verify
|
|
11
|
+
from ._internal.when import Stub, When
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"AsyncMock",
|
|
15
|
+
"Decoy",
|
|
16
|
+
"Matcher",
|
|
17
|
+
"Mock",
|
|
18
|
+
"Stub",
|
|
19
|
+
"Verify",
|
|
20
|
+
"When",
|
|
21
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
from .values import (
|
|
2
|
+
AttributeEvent,
|
|
3
|
+
AttributeEventType,
|
|
4
|
+
BehaviorEntry,
|
|
5
|
+
CallEvent,
|
|
6
|
+
Event,
|
|
7
|
+
EventEntry,
|
|
8
|
+
EventMatcher,
|
|
9
|
+
EventState,
|
|
10
|
+
MockInfo,
|
|
11
|
+
VerificationEntry,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def is_event_from_mock(event_entry: EventEntry, mock: MockInfo) -> bool:
|
|
16
|
+
return mock.id == event_entry.mock.id
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def is_verifiable_mock_event(event_entry: EventEntry, mock: MockInfo) -> bool:
|
|
20
|
+
return is_event_from_mock(event_entry, mock) and (
|
|
21
|
+
isinstance(event_entry.event, CallEvent)
|
|
22
|
+
or event_entry.event.type != AttributeEventType.GET
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def is_matching_behavior(
|
|
27
|
+
event_entry: EventEntry,
|
|
28
|
+
behavior_entry: BehaviorEntry,
|
|
29
|
+
) -> bool:
|
|
30
|
+
return is_event_from_mock(
|
|
31
|
+
event_entry,
|
|
32
|
+
behavior_entry.mock,
|
|
33
|
+
) and is_matching_event(
|
|
34
|
+
event_entry,
|
|
35
|
+
behavior_entry.matcher,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def is_matching_event(event_entry: EventEntry, matcher: EventMatcher) -> bool:
|
|
40
|
+
event_matches = _match_event(event_entry.event, matcher)
|
|
41
|
+
state_matches = _match_state(event_entry.state, matcher)
|
|
42
|
+
|
|
43
|
+
return event_matches and state_matches
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def is_matching_count(usage_count: int, matcher: EventMatcher) -> bool:
|
|
47
|
+
return matcher.options.times is None or usage_count < matcher.options.times
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def is_successful_verify(verification: VerificationEntry) -> bool:
|
|
51
|
+
if verification.matcher.options.times is not None:
|
|
52
|
+
return len(verification.matching_events) == verification.matcher.options.times
|
|
53
|
+
|
|
54
|
+
return len(verification.matching_events) > 0
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def is_successful_verify_order(verifications: list[VerificationEntry]) -> bool:
|
|
58
|
+
matching_events: list[tuple[EventEntry, VerificationEntry]] = []
|
|
59
|
+
verification_index = 0
|
|
60
|
+
event_index = 0
|
|
61
|
+
|
|
62
|
+
for verification in verifications:
|
|
63
|
+
for matching_event in verification.matching_events:
|
|
64
|
+
matching_events.append((matching_event, verification))
|
|
65
|
+
|
|
66
|
+
matching_events.sort(key=lambda e: e[0].order)
|
|
67
|
+
|
|
68
|
+
while event_index < len(matching_events) and verification_index < len(
|
|
69
|
+
verifications
|
|
70
|
+
):
|
|
71
|
+
_, event_verification = matching_events[event_index]
|
|
72
|
+
expected_verification = verifications[verification_index]
|
|
73
|
+
expected_times = expected_verification.matcher.options.times
|
|
74
|
+
remaining_events = len(matching_events) - event_index
|
|
75
|
+
|
|
76
|
+
if event_verification is expected_verification:
|
|
77
|
+
verification_index += 1
|
|
78
|
+
|
|
79
|
+
if expected_times is None or expected_times == 1:
|
|
80
|
+
event_index += 1
|
|
81
|
+
else:
|
|
82
|
+
for times_index in range(1, expected_times):
|
|
83
|
+
_, later_verification = matching_events[event_index + times_index]
|
|
84
|
+
if later_verification is not expected_verification:
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
event_index += expected_times
|
|
88
|
+
|
|
89
|
+
elif remaining_events >= len(verifications) and verification_index > 0:
|
|
90
|
+
verification_index = 0
|
|
91
|
+
else:
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
return True
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def is_redundant_verify(
|
|
98
|
+
verification: VerificationEntry,
|
|
99
|
+
behaviors: list[BehaviorEntry],
|
|
100
|
+
) -> bool:
|
|
101
|
+
return any(
|
|
102
|
+
behavior
|
|
103
|
+
for behavior in behaviors
|
|
104
|
+
if verification.mock.id == behavior.mock.id
|
|
105
|
+
and verification.matcher.options == behavior.matcher.options
|
|
106
|
+
and _match_event(verification.matcher.event, behavior.matcher)
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _match_event(event: Event, matcher: EventMatcher) -> bool:
|
|
111
|
+
if (
|
|
112
|
+
matcher.options.ignore_extra_args is False
|
|
113
|
+
or isinstance(event, AttributeEvent)
|
|
114
|
+
or isinstance(matcher.event, AttributeEvent)
|
|
115
|
+
):
|
|
116
|
+
return event == matcher.event
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
args_match = all(
|
|
120
|
+
value == event.args[i] for i, value in enumerate(matcher.event.args)
|
|
121
|
+
)
|
|
122
|
+
kwargs_match = all(
|
|
123
|
+
value == event.kwargs[key] for key, value in matcher.event.kwargs.items()
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
return args_match and kwargs_match
|
|
127
|
+
|
|
128
|
+
except (IndexError, KeyError):
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _match_state(event_state: EventState, matcher: EventMatcher) -> bool:
|
|
135
|
+
return (
|
|
136
|
+
matcher.options.is_entered is None
|
|
137
|
+
or event_state.is_entered == matcher.options.is_entered
|
|
138
|
+
)
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import collections.abc
|
|
2
|
+
import contextlib
|
|
3
|
+
from typing import Any, Callable, Literal, TypeVar, overload
|
|
4
|
+
|
|
5
|
+
from .errors import createNotAMockError, createVerifyOrderError
|
|
6
|
+
from .inspect import (
|
|
7
|
+
ensure_spec,
|
|
8
|
+
ensure_spec_name,
|
|
9
|
+
get_spec_module_name,
|
|
10
|
+
is_async_callable,
|
|
11
|
+
)
|
|
12
|
+
from .mock import AsyncMock, Mock, create_mock, ensure_mock
|
|
13
|
+
from .state import DecoyState
|
|
14
|
+
from .values import MatchOptions
|
|
15
|
+
from .verify import Verify
|
|
16
|
+
from .when import When
|
|
17
|
+
|
|
18
|
+
ClassT = TypeVar("ClassT")
|
|
19
|
+
FuncT = TypeVar("FuncT", bound=Callable[..., Any])
|
|
20
|
+
SpecT = TypeVar("SpecT")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Decoy:
|
|
24
|
+
"""Decoy mock factory and state container.
|
|
25
|
+
|
|
26
|
+
Use the `create` context manager to create a new Decoy instance
|
|
27
|
+
and reset it after your test.
|
|
28
|
+
If you use the [`decoy` pytest fixture][decoy.pytest_plugin.decoy],
|
|
29
|
+
this is done automatically.
|
|
30
|
+
|
|
31
|
+
See the [setup guide][] for more details.
|
|
32
|
+
|
|
33
|
+
!!! example
|
|
34
|
+
```python
|
|
35
|
+
with Decoy.create() as decoy:
|
|
36
|
+
...
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
[setup guide]: ../index.md#setup
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
@contextlib.contextmanager
|
|
44
|
+
def create(cls) -> collections.abc.Iterator["Decoy"]:
|
|
45
|
+
"""Create a Decoy instance for testing that will reset after usage.
|
|
46
|
+
|
|
47
|
+
This method is used by the [pytest plugin][decoy.pytest_plugin].
|
|
48
|
+
"""
|
|
49
|
+
decoy = cls()
|
|
50
|
+
try:
|
|
51
|
+
yield decoy
|
|
52
|
+
finally:
|
|
53
|
+
decoy.reset()
|
|
54
|
+
|
|
55
|
+
def __init__(self) -> None:
|
|
56
|
+
self._state = DecoyState()
|
|
57
|
+
|
|
58
|
+
@overload
|
|
59
|
+
def mock(self, *, cls: Callable[..., ClassT]) -> ClassT: ...
|
|
60
|
+
|
|
61
|
+
@overload
|
|
62
|
+
def mock(self, *, func: FuncT) -> FuncT: ...
|
|
63
|
+
|
|
64
|
+
@overload
|
|
65
|
+
def mock(self, *, name: str, is_async: Literal[True]) -> AsyncMock: ...
|
|
66
|
+
|
|
67
|
+
@overload
|
|
68
|
+
def mock(self, *, name: str, is_async: bool = False) -> Mock: ...
|
|
69
|
+
|
|
70
|
+
def mock(
|
|
71
|
+
self,
|
|
72
|
+
*,
|
|
73
|
+
cls: Callable[..., ClassT] | None = None,
|
|
74
|
+
func: FuncT | None = None,
|
|
75
|
+
name: str | None = None,
|
|
76
|
+
is_async: bool = False,
|
|
77
|
+
) -> ClassT | FuncT | AsyncMock | Mock:
|
|
78
|
+
"""Create a mock. See the [mock creation guide](./create.md) for more details.
|
|
79
|
+
|
|
80
|
+
Arguments:
|
|
81
|
+
cls: A class definition that the mock should imitate.
|
|
82
|
+
func: A function definition the mock should imitate.
|
|
83
|
+
name: A name to use for the mock. If you do not use
|
|
84
|
+
`cls` or `func`, you must add a `name`.
|
|
85
|
+
is_async: Force the returned mock to be asynchronous. This argument
|
|
86
|
+
only applies if you don't use `cls` nor `func`.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
A mock typecast as the object it's imitating, if any.
|
|
90
|
+
|
|
91
|
+
!!! example
|
|
92
|
+
```python
|
|
93
|
+
def test_get_something(decoy: Decoy):
|
|
94
|
+
db = decoy.mock(cls=Database)
|
|
95
|
+
# ...
|
|
96
|
+
```
|
|
97
|
+
"""
|
|
98
|
+
spec = ensure_spec(cls, func)
|
|
99
|
+
name = ensure_spec_name(spec, name)
|
|
100
|
+
parent_name = get_spec_module_name(spec)
|
|
101
|
+
is_async = is_async_callable(spec, is_async)
|
|
102
|
+
|
|
103
|
+
return create_mock(
|
|
104
|
+
spec=spec,
|
|
105
|
+
name=name,
|
|
106
|
+
parent_name=parent_name,
|
|
107
|
+
is_async=is_async,
|
|
108
|
+
state=self._state,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def when(
|
|
112
|
+
self,
|
|
113
|
+
mock: SpecT,
|
|
114
|
+
*,
|
|
115
|
+
times: int | None = None,
|
|
116
|
+
ignore_extra_args: bool = False,
|
|
117
|
+
is_entered: bool | None = None,
|
|
118
|
+
) -> When[SpecT, SpecT]:
|
|
119
|
+
"""Configure a mock as a stub. See [`when` guide](./when.md) for more details.
|
|
120
|
+
|
|
121
|
+
Arguments:
|
|
122
|
+
mock: The mock to configure.
|
|
123
|
+
times: Limit the number of times the behavior is triggered.
|
|
124
|
+
ignore_extra_args: Only partially match arguments.
|
|
125
|
+
is_entered: Limit the behavior to when the mock is entered using `with`.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
A stub interface to configure matching arguments.
|
|
129
|
+
|
|
130
|
+
Raises:
|
|
131
|
+
NotAMockError: `mock` is invalid.
|
|
132
|
+
|
|
133
|
+
!!! example
|
|
134
|
+
```python
|
|
135
|
+
db = decoy.mock(cls=Database)
|
|
136
|
+
decoy.when(db.exists).called_with("some-id").then_return(True)
|
|
137
|
+
```
|
|
138
|
+
"""
|
|
139
|
+
mock_info = ensure_mock(mock)
|
|
140
|
+
match_options = MatchOptions(times, ignore_extra_args, is_entered)
|
|
141
|
+
|
|
142
|
+
if not mock_info:
|
|
143
|
+
mock_info = self._state.peek_last_attribute_mock(mock)
|
|
144
|
+
|
|
145
|
+
if not mock_info:
|
|
146
|
+
raise createNotAMockError("when", mock)
|
|
147
|
+
|
|
148
|
+
return When(self._state, mock_info, match_options)
|
|
149
|
+
|
|
150
|
+
def verify(
|
|
151
|
+
self,
|
|
152
|
+
mock: SpecT,
|
|
153
|
+
*,
|
|
154
|
+
times: int | None = None,
|
|
155
|
+
ignore_extra_args: bool = False,
|
|
156
|
+
is_entered: bool | None = None,
|
|
157
|
+
) -> Verify[SpecT]:
|
|
158
|
+
"""Verify a mock was called. See the [`verify` guide](./verify.md) for more details.
|
|
159
|
+
|
|
160
|
+
Arguments:
|
|
161
|
+
mock: The mock to verify.
|
|
162
|
+
times: Limit the number of times the call is expected.
|
|
163
|
+
ignore_extra_args: Only partially match arguments.
|
|
164
|
+
is_entered: Verify call happens when the mock is entered using `with`.
|
|
165
|
+
|
|
166
|
+
Raises:
|
|
167
|
+
VerifyError: The verification was not satisfied.
|
|
168
|
+
NotAMockError: `mock` is invalid.
|
|
169
|
+
|
|
170
|
+
!!! example
|
|
171
|
+
```python
|
|
172
|
+
def test_create_something(decoy: Decoy):
|
|
173
|
+
gen_id = decoy.mock(func=generate_unique_id)
|
|
174
|
+
|
|
175
|
+
# ...
|
|
176
|
+
|
|
177
|
+
decoy.verify(gen_id).called_with("model-prefix_")
|
|
178
|
+
```
|
|
179
|
+
"""
|
|
180
|
+
mock_info = ensure_mock(mock)
|
|
181
|
+
match_options = MatchOptions(times, ignore_extra_args, is_entered)
|
|
182
|
+
|
|
183
|
+
if not mock_info:
|
|
184
|
+
mock_info = self._state.peek_last_attribute_mock(mock)
|
|
185
|
+
|
|
186
|
+
if not mock_info:
|
|
187
|
+
raise createNotAMockError("verify", mock)
|
|
188
|
+
|
|
189
|
+
return Verify(
|
|
190
|
+
self._state,
|
|
191
|
+
mock_info,
|
|
192
|
+
match_options,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
@contextlib.contextmanager
|
|
196
|
+
def verify_order(self) -> collections.abc.Iterator[None]:
|
|
197
|
+
"""Verify a sequence of interactions.
|
|
198
|
+
|
|
199
|
+
All verifications in the sequence must be individually satisfied
|
|
200
|
+
before the sequence is checked.
|
|
201
|
+
|
|
202
|
+
See [verification usage guide](verify.md) for more details.
|
|
203
|
+
|
|
204
|
+
Raises:
|
|
205
|
+
VerifyOrderError: The sequence was not satisfied.
|
|
206
|
+
|
|
207
|
+
!!! example
|
|
208
|
+
```python
|
|
209
|
+
def test_greet(decoy: Decoy):
|
|
210
|
+
verify_greeting = decoy.mock(name="verify_greeting")
|
|
211
|
+
greet = decoy.mock(name="greet")
|
|
212
|
+
|
|
213
|
+
# ...
|
|
214
|
+
|
|
215
|
+
with decoy.verify_order():
|
|
216
|
+
decoy.verify(verify_greeting).called_with("hello world")
|
|
217
|
+
decoy.verify(greet).called_with("hello world")
|
|
218
|
+
```
|
|
219
|
+
"""
|
|
220
|
+
with self._state.verify_order() as verify_order_result:
|
|
221
|
+
yield
|
|
222
|
+
|
|
223
|
+
if not verify_order_result.is_success:
|
|
224
|
+
raise createVerifyOrderError(
|
|
225
|
+
verify_order_result.verifications,
|
|
226
|
+
verify_order_result.all_events,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
def reset(self) -> None:
|
|
230
|
+
"""Reset the decoy instance."""
|
|
231
|
+
self._state.reset()
|