Skip to content

Commit 2466753

Browse files
committed
feat: add decoy.next preview API
1 parent 07ea0d6 commit 2466753

21 files changed

+2148
-228
lines changed

decoy/errors.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ def create(cls) -> "MockNameRequiredError":
2525
return cls("Mocks without `cls` or `func` require a `name`.")
2626

2727

28+
class MockSpecInvalidError(TypeError):
29+
"""An value passed as a mock spec is not valid."""
30+
31+
2832
class MissingRehearsalError(ValueError):
2933
"""An error raised when a Decoy method is called without rehearsal(s).
3034
@@ -44,6 +48,14 @@ def create(cls) -> "MissingRehearsalError":
4448
return cls("Rehearsal not found.")
4549

4650

51+
class NotAMockError(TypeError):
52+
"""A Decoy method was called without a mock."""
53+
54+
55+
class ThenDoActionNotCallableError(TypeError):
56+
"""A value passed to `then_do` is not callable."""
57+
58+
4759
class MockNotAsyncError(TypeError):
4860
"""An error raised when an asynchronous function is used with a synchronous mock.
4961
@@ -55,6 +67,10 @@ class MockNotAsyncError(TypeError):
5567
"""
5668

5769

70+
class SignatureMismatchError(TypeError):
71+
"""Arguments did not match the signature of the mock."""
72+
73+
5874
class VerifyError(AssertionError):
5975
"""An error raised when actual calls do not match rehearsals given to `verify`.
6076
@@ -100,3 +116,12 @@ def create(
100116
result.times = times
101117

102118
return result
119+
120+
121+
class VerifyOrderError(VerifyError):
122+
"""An error raised when the order of calls do not match expectations.
123+
124+
See [spying with verify][] for more details.
125+
126+
[spying with verify]: usage/verify.md
127+
"""

decoy/next/__init__.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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.mock import AsyncMock, Mock
9+
from ._internal.verify import Verify
10+
from ._internal.when import Stub, When
11+
12+
__all__ = [
13+
"AsyncMock",
14+
"Decoy",
15+
"Mock",
16+
"Stub",
17+
"Verify",
18+
"When",
19+
]

decoy/next/_internal/__init__.py

Whitespace-only changes.

decoy/next/_internal/compare.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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 commit comments

Comments
 (0)