decoy 2.3.0__py3-none-any.whl → 2.5.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 +12 -17
- 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/pytest_plugin.py +30 -5
- decoy/spy.py +1 -1
- {decoy-2.3.0.dist-info → decoy-2.5.0.dist-info}/METADATA +5 -4
- decoy-2.5.0.dist-info/RECORD +38 -0
- {decoy-2.3.0.dist-info → decoy-2.5.0.dist-info}/WHEEL +2 -2
- decoy-2.3.0.dist-info/RECORD +0 -24
- {decoy-2.3.0.dist-info → decoy-2.5.0.dist-info}/entry_points.txt +0 -0
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
|
@@ -178,14 +178,11 @@ class _HasAttributes:
|
|
|
178
178
|
|
|
179
179
|
def __eq__(self, target: object) -> bool:
|
|
180
180
|
"""Return true if target matches all given attributes."""
|
|
181
|
-
is_match = True
|
|
182
181
|
for attr_name, value in self._attributes.items():
|
|
183
|
-
if
|
|
184
|
-
|
|
185
|
-
hasattr(target, attr_name) and getattr(target, attr_name) == value
|
|
186
|
-
)
|
|
182
|
+
if not hasattr(target, attr_name) or getattr(target, attr_name) != value:
|
|
183
|
+
return False
|
|
187
184
|
|
|
188
|
-
return
|
|
185
|
+
return True
|
|
189
186
|
|
|
190
187
|
def __repr__(self) -> str:
|
|
191
188
|
"""Return a string representation of the matcher."""
|
|
@@ -219,16 +216,14 @@ class _DictMatching:
|
|
|
219
216
|
|
|
220
217
|
def __eq__(self, target: object) -> bool:
|
|
221
218
|
"""Return true if target matches all given keys/values."""
|
|
222
|
-
is_match = True
|
|
223
|
-
|
|
224
219
|
for key, value in self._values.items():
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
|
230
225
|
|
|
231
|
-
return
|
|
226
|
+
return True
|
|
232
227
|
|
|
233
228
|
def __repr__(self) -> str:
|
|
234
229
|
"""Return a string representation of the matcher."""
|
|
@@ -319,10 +314,12 @@ def StringMatching(match: str) -> str:
|
|
|
319
314
|
class _ErrorMatching:
|
|
320
315
|
_error_type: Type[BaseException]
|
|
321
316
|
_string_matcher: Optional[_StringMatching]
|
|
317
|
+
_match: Optional[str]
|
|
322
318
|
|
|
323
319
|
def __init__(self, error: Type[BaseException], match: Optional[str] = None) -> None:
|
|
324
320
|
"""Initialize with the Exception type and optional message matcher."""
|
|
325
321
|
self._error_type = error
|
|
322
|
+
self._match = match
|
|
326
323
|
self._string_matcher = _StringMatching(match) if match is not None else None
|
|
327
324
|
|
|
328
325
|
def __eq__(self, target: object) -> bool:
|
|
@@ -338,9 +335,7 @@ class _ErrorMatching:
|
|
|
338
335
|
|
|
339
336
|
def __repr__(self) -> str:
|
|
340
337
|
"""Return a string representation of the matcher."""
|
|
341
|
-
return
|
|
342
|
-
f"<ErrorMatching {self._error_type.__name__} match={self._string_matcher}>"
|
|
343
|
-
)
|
|
338
|
+
return f"<ErrorMatching {self._error_type.__name__} match={self._match!r}>"
|
|
344
339
|
|
|
345
340
|
|
|
346
341
|
ErrorT = TypeVar("ErrorT", bound=BaseException)
|
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()
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
|
|
3
|
+
from ... import errors
|
|
4
|
+
from .stringify import (
|
|
5
|
+
count,
|
|
6
|
+
join_lines,
|
|
7
|
+
stringify_event,
|
|
8
|
+
stringify_event_entry_list,
|
|
9
|
+
stringify_event_list,
|
|
10
|
+
stringify_verification_list,
|
|
11
|
+
)
|
|
12
|
+
from .values import Event, EventEntry, MatchOptions, VerificationEntry
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def createMockNameRequiredError() -> errors.MockNameRequiredError:
|
|
16
|
+
"""Create a MockNameRequiredError."""
|
|
17
|
+
return errors.MockNameRequiredError(
|
|
18
|
+
"Mocks without `cls` or `func` require a `name`."
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def createMockSpecInvalidError(
|
|
23
|
+
argument_name: Literal["func", "cls"],
|
|
24
|
+
) -> errors.MockSpecInvalidError:
|
|
25
|
+
"""Create a MockSpecInvalidError."""
|
|
26
|
+
expected_type = "function" if argument_name == "func" else "class"
|
|
27
|
+
|
|
28
|
+
return errors.MockSpecInvalidError(
|
|
29
|
+
f"{argument_name} value must be a {expected_type}"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def createNotAMockError(method_name: str, actual_value: object) -> errors.NotAMockError:
|
|
34
|
+
"""Create a NotAMockError."""
|
|
35
|
+
return errors.NotAMockError(
|
|
36
|
+
f"`Decoy.{method_name}` must be called with a mock, but got: {actual_value}"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def createThenDoActionNotCallableError() -> errors.ThenDoActionNotCallableError:
|
|
41
|
+
"""Create a ThenDoActionNotCallableError."""
|
|
42
|
+
return errors.ThenDoActionNotCallableError(
|
|
43
|
+
"Value passed to `then_do` must be callable."
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def createMockNotAsyncError() -> errors.MockNotAsyncError:
|
|
48
|
+
"""Create a MockNotAsyncError."""
|
|
49
|
+
return errors.MockNotAsyncError(
|
|
50
|
+
"Synchronous mock cannot use an asynchronous callable in `then_do`."
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def createSignatureMismatchError(
|
|
55
|
+
source: TypeError | ValueError,
|
|
56
|
+
) -> errors.SignatureMismatchError:
|
|
57
|
+
"""Create a SignatureMismatchError."""
|
|
58
|
+
return errors.SignatureMismatchError(source)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def createVerifyError(
|
|
62
|
+
mock_name: str,
|
|
63
|
+
match_options: MatchOptions,
|
|
64
|
+
expected: Event,
|
|
65
|
+
all_events: list[EventEntry],
|
|
66
|
+
) -> errors.VerifyError:
|
|
67
|
+
"""Create a VerifyError."""
|
|
68
|
+
if match_options.times is not None:
|
|
69
|
+
heading = f"Expected exactly {count(match_options.times, 'call')}:"
|
|
70
|
+
else:
|
|
71
|
+
heading = "Expected at least 1 call:"
|
|
72
|
+
|
|
73
|
+
message = join_lines(
|
|
74
|
+
heading,
|
|
75
|
+
f"1.\t{stringify_event(mock_name, expected)}",
|
|
76
|
+
(
|
|
77
|
+
f"Found {count(len(all_events), 'call')}{'.' if len(all_events) == 0 else ':'}"
|
|
78
|
+
),
|
|
79
|
+
stringify_event_list(mock_name, [entry.event for entry in all_events]),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return errors.VerifyError(message)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def createVerifyOrderError(
|
|
86
|
+
verifications: list[VerificationEntry],
|
|
87
|
+
all_events: list[EventEntry],
|
|
88
|
+
) -> errors.VerifyOrderError:
|
|
89
|
+
"""Create a VerifyOrderError."""
|
|
90
|
+
message = join_lines(
|
|
91
|
+
"Expected call sequence:",
|
|
92
|
+
stringify_verification_list(verifications),
|
|
93
|
+
f"Found {len(all_events)} calls:",
|
|
94
|
+
stringify_event_entry_list(all_events),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return errors.VerifyOrderError(message)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def createNoMatcherValueCapturedError(
|
|
101
|
+
message: str,
|
|
102
|
+
) -> errors.NoMatcherValueCapturedError:
|
|
103
|
+
return errors.NoMatcherValueCapturedError(message)
|