decoy 2.3.0__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 +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/spy.py +1 -1
- {decoy-2.3.0.dist-info → decoy-2.4.0.dist-info}/METADATA +5 -4
- decoy-2.4.0.dist-info/RECORD +38 -0
- {decoy-2.3.0.dist-info → decoy-2.4.0.dist-info}/WHEEL +2 -2
- decoy-2.3.0.dist-info/RECORD +0 -24
- {decoy-2.3.0.dist-info → decoy-2.4.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import warnings
|
|
3
|
+
from types import TracebackType
|
|
4
|
+
from typing import Any, cast
|
|
5
|
+
|
|
6
|
+
from .inspect import (
|
|
7
|
+
bind_args,
|
|
8
|
+
get_awaitable_value,
|
|
9
|
+
get_child_spec,
|
|
10
|
+
get_method_class,
|
|
11
|
+
get_signature,
|
|
12
|
+
get_spec_class_type,
|
|
13
|
+
is_async_callable,
|
|
14
|
+
is_magic_attribute,
|
|
15
|
+
)
|
|
16
|
+
from .state import DecoyState
|
|
17
|
+
from .values import AttributeEvent, CallEvent, EventState, MockInfo
|
|
18
|
+
from .warnings import createMiscalledStubWarning
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MockInternals:
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
*,
|
|
25
|
+
state: DecoyState,
|
|
26
|
+
spec: object,
|
|
27
|
+
name: str,
|
|
28
|
+
is_async: bool,
|
|
29
|
+
full_name: str | None,
|
|
30
|
+
spec_class_type: type[object],
|
|
31
|
+
) -> None:
|
|
32
|
+
self.state = state
|
|
33
|
+
self.spec = spec
|
|
34
|
+
self.name = name
|
|
35
|
+
self.full_name = full_name or name
|
|
36
|
+
self.spec_class_type = spec_class_type
|
|
37
|
+
self.children: dict[str, Mock] = {}
|
|
38
|
+
self.attribute_values: dict[str, object] = {}
|
|
39
|
+
self.event_state = EventState(is_entered=False)
|
|
40
|
+
self.info = MockInfo(
|
|
41
|
+
id=id(self),
|
|
42
|
+
name=self.full_name,
|
|
43
|
+
is_async=is_async,
|
|
44
|
+
signature=get_signature(spec),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def call(self, args: tuple[object, ...], kwargs: dict[str, object]) -> object:
|
|
48
|
+
bound_args = bind_args(self.info.signature, args, kwargs)
|
|
49
|
+
event = CallEvent(bound_args.args, bound_args.kwargs)
|
|
50
|
+
behavior = self.state.use_call_behavior(
|
|
51
|
+
mock=self.info,
|
|
52
|
+
event=event,
|
|
53
|
+
event_state=self.event_state,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
if not behavior.is_found and behavior.expected_events:
|
|
57
|
+
warnings.warn(
|
|
58
|
+
createMiscalledStubWarning(
|
|
59
|
+
self.name,
|
|
60
|
+
behavior.expected_events,
|
|
61
|
+
event,
|
|
62
|
+
),
|
|
63
|
+
stacklevel=3,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
return behavior.return_value
|
|
67
|
+
|
|
68
|
+
def get_child(self, name: str, is_async: bool = False) -> "Mock":
|
|
69
|
+
if name in self.children:
|
|
70
|
+
return self.children[name]
|
|
71
|
+
|
|
72
|
+
spec = get_child_spec(self.spec, name)
|
|
73
|
+
child = create_mock(
|
|
74
|
+
spec=spec,
|
|
75
|
+
name=name,
|
|
76
|
+
is_async=is_async_callable(spec) if spec else is_async,
|
|
77
|
+
parent_name=self.full_name,
|
|
78
|
+
state=self.state,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
self.children[name] = child
|
|
82
|
+
|
|
83
|
+
return child
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class Mock:
|
|
87
|
+
"""A mock callable/class, created by [`Decoy.mock`][decoy.next.Decoy.mock]."""
|
|
88
|
+
|
|
89
|
+
_decoy: MockInternals
|
|
90
|
+
|
|
91
|
+
def __init__(self, internals: MockInternals) -> None:
|
|
92
|
+
super().__setattr__("_decoy", internals)
|
|
93
|
+
|
|
94
|
+
def __call__(self, *args: Any, **kwargs: Any) -> Any:
|
|
95
|
+
return self._decoy.call(args, kwargs)
|
|
96
|
+
|
|
97
|
+
@property # type: ignore[misc]
|
|
98
|
+
def __class__(self) -> type[Any]: # pyright: ignore[reportIncompatibleMethodOverride]
|
|
99
|
+
"""Imitate `isinstance` checks."""
|
|
100
|
+
return self._decoy.spec_class_type
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def __signature__(self) -> inspect.Signature | None:
|
|
104
|
+
"""Imitate `inspect.signature` checks."""
|
|
105
|
+
return self._decoy.info.signature
|
|
106
|
+
|
|
107
|
+
def __repr__(self) -> str:
|
|
108
|
+
return f"<Decoy mock `{self._decoy.full_name}`>"
|
|
109
|
+
|
|
110
|
+
def __enter__(self) -> Any:
|
|
111
|
+
enter = self._decoy.get_child("__enter__")
|
|
112
|
+
self._decoy.event_state = EventState(is_entered=True)
|
|
113
|
+
|
|
114
|
+
return enter()
|
|
115
|
+
|
|
116
|
+
def __exit__(
|
|
117
|
+
self,
|
|
118
|
+
exc_type: type[BaseException] | None,
|
|
119
|
+
exc_value: BaseException | None,
|
|
120
|
+
traceback: TracebackType | None,
|
|
121
|
+
) -> bool | None:
|
|
122
|
+
exit = self._decoy.get_child("__exit__")
|
|
123
|
+
self._decoy.event_state = EventState(is_entered=False)
|
|
124
|
+
|
|
125
|
+
return cast(bool | None, exit(exc_type, exc_value, traceback))
|
|
126
|
+
|
|
127
|
+
async def __aenter__(self) -> Any:
|
|
128
|
+
enter = self._decoy.get_child("__aenter__", is_async=True)
|
|
129
|
+
self._decoy.event_state = EventState(is_entered=True)
|
|
130
|
+
|
|
131
|
+
return await enter()
|
|
132
|
+
|
|
133
|
+
async def __aexit__(
|
|
134
|
+
self,
|
|
135
|
+
exc_type: type[BaseException] | None,
|
|
136
|
+
exc_value: BaseException | None,
|
|
137
|
+
traceback: TracebackType | None,
|
|
138
|
+
) -> bool | None:
|
|
139
|
+
exit = self._decoy.get_child("__aexit__", is_async=True)
|
|
140
|
+
self._decoy.event_state = EventState(is_entered=False)
|
|
141
|
+
|
|
142
|
+
return cast(bool | None, await exit(exc_type, exc_value, traceback))
|
|
143
|
+
|
|
144
|
+
def __getattr__(self, name: str) -> Any:
|
|
145
|
+
if is_magic_attribute(name):
|
|
146
|
+
return super().__getattribute__(name)
|
|
147
|
+
|
|
148
|
+
child = self._decoy.get_child(name)
|
|
149
|
+
|
|
150
|
+
return self._decoy.state.use_attribute_behavior(
|
|
151
|
+
mock=child._decoy.info,
|
|
152
|
+
event=AttributeEvent.get(),
|
|
153
|
+
event_state=self._decoy.event_state,
|
|
154
|
+
default_return_value=child,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
def __setattr__(self, name: str, value: object) -> None:
|
|
158
|
+
child = self._decoy.get_child(name)
|
|
159
|
+
|
|
160
|
+
self._decoy.state.use_attribute_behavior(
|
|
161
|
+
mock=child._decoy.info,
|
|
162
|
+
event=AttributeEvent.set(value),
|
|
163
|
+
event_state=self._decoy.event_state,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def __delattr__(self, name: str) -> None:
|
|
167
|
+
child = self._decoy.get_child(name)
|
|
168
|
+
|
|
169
|
+
self._decoy.state.use_attribute_behavior(
|
|
170
|
+
mock=child._decoy.info,
|
|
171
|
+
event=AttributeEvent.delete(),
|
|
172
|
+
event_state=self._decoy.event_state,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class AsyncMock(Mock):
|
|
177
|
+
"""A mock async callable/class, created by [`Decoy.mock`][decoy.next.Decoy.mock]."""
|
|
178
|
+
|
|
179
|
+
async def __call__(self, *args: Any, **kwargs: Any) -> Any:
|
|
180
|
+
call_result = self._decoy.call(args, kwargs)
|
|
181
|
+
result = await get_awaitable_value(call_result)
|
|
182
|
+
|
|
183
|
+
return result
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def create_mock(
|
|
187
|
+
spec: object,
|
|
188
|
+
name: str,
|
|
189
|
+
parent_name: str | None,
|
|
190
|
+
is_async: bool,
|
|
191
|
+
state: DecoyState,
|
|
192
|
+
) -> Mock:
|
|
193
|
+
cls = AsyncMock if is_async else Mock
|
|
194
|
+
mock_internals = MockInternals(
|
|
195
|
+
state=state,
|
|
196
|
+
spec=spec,
|
|
197
|
+
name=name,
|
|
198
|
+
is_async=is_async,
|
|
199
|
+
full_name=name if parent_name is None else f"{parent_name}.{name}",
|
|
200
|
+
spec_class_type=get_spec_class_type(spec, cls),
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
return cls(mock_internals)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def ensure_mock(maybe_mock: object) -> MockInfo | None:
|
|
207
|
+
if isinstance(maybe_mock, Mock):
|
|
208
|
+
return maybe_mock._decoy.info
|
|
209
|
+
|
|
210
|
+
for cm_method, is_async in (
|
|
211
|
+
("__enter__", False),
|
|
212
|
+
("__exit__", False),
|
|
213
|
+
("__aenter__", True),
|
|
214
|
+
("__aexit__", True),
|
|
215
|
+
):
|
|
216
|
+
maybe_cm_mock = get_method_class(cm_method, maybe_mock)
|
|
217
|
+
if isinstance(maybe_cm_mock, Mock):
|
|
218
|
+
return maybe_cm_mock._decoy.get_child(cm_method, is_async)._decoy.info
|
|
219
|
+
|
|
220
|
+
return None
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import collections
|
|
2
|
+
import collections.abc
|
|
3
|
+
import contextlib
|
|
4
|
+
import dataclasses
|
|
5
|
+
from typing import NamedTuple
|
|
6
|
+
|
|
7
|
+
from .compare import (
|
|
8
|
+
is_event_from_mock,
|
|
9
|
+
is_matching_behavior,
|
|
10
|
+
is_matching_count,
|
|
11
|
+
is_matching_event,
|
|
12
|
+
is_redundant_verify,
|
|
13
|
+
is_successful_verify,
|
|
14
|
+
is_successful_verify_order,
|
|
15
|
+
is_verifiable_mock_event,
|
|
16
|
+
)
|
|
17
|
+
from .values import (
|
|
18
|
+
MISSING,
|
|
19
|
+
AttributeEvent,
|
|
20
|
+
AttributeEventType,
|
|
21
|
+
Behavior,
|
|
22
|
+
BehaviorEntry,
|
|
23
|
+
CallEvent,
|
|
24
|
+
Event,
|
|
25
|
+
EventEntry,
|
|
26
|
+
EventMatcher,
|
|
27
|
+
EventState,
|
|
28
|
+
MockInfo,
|
|
29
|
+
VerificationEntry,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class BehaviorResult(NamedTuple):
|
|
34
|
+
is_found: bool
|
|
35
|
+
return_value: object
|
|
36
|
+
expected_events: list[Event]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class VerificationResult(NamedTuple):
|
|
40
|
+
is_success: bool
|
|
41
|
+
is_redundant: bool
|
|
42
|
+
matching_events: list[EventEntry]
|
|
43
|
+
mock_events: list[EventEntry]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclasses.dataclass
|
|
47
|
+
class OrderVerificationResult:
|
|
48
|
+
is_success: bool = False
|
|
49
|
+
verifications: list[VerificationEntry] = dataclasses.field(default_factory=list)
|
|
50
|
+
all_events: list[EventEntry] = dataclasses.field(default_factory=list)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class DecoyState:
|
|
54
|
+
def __init__(self) -> None:
|
|
55
|
+
self._order_verification: OrderVerificationResult | None = None
|
|
56
|
+
self._events: list[EventEntry] = []
|
|
57
|
+
self._behaviors: list[BehaviorEntry] = []
|
|
58
|
+
self._behavior_usage_by_index: dict[int, int] = collections.defaultdict(int)
|
|
59
|
+
self._attribute_mocks_by_id: dict[int, object] = {}
|
|
60
|
+
|
|
61
|
+
def _consume_behavior(
|
|
62
|
+
self,
|
|
63
|
+
event_entry: EventEntry,
|
|
64
|
+
) -> tuple[Behavior | None, list[Event], bool]:
|
|
65
|
+
mock_behaviors = [
|
|
66
|
+
behavior
|
|
67
|
+
for behavior in self._behaviors
|
|
68
|
+
if is_event_from_mock(event_entry, behavior.mock)
|
|
69
|
+
]
|
|
70
|
+
matched_behaviors = [
|
|
71
|
+
behavior
|
|
72
|
+
for behavior in mock_behaviors
|
|
73
|
+
if is_matching_event(event_entry, behavior.matcher)
|
|
74
|
+
]
|
|
75
|
+
expected_events = [behavior.matcher.event for behavior in mock_behaviors]
|
|
76
|
+
|
|
77
|
+
for behavior_entry in reversed(matched_behaviors):
|
|
78
|
+
usage_count = self._behavior_usage_by_index[behavior_entry.order]
|
|
79
|
+
|
|
80
|
+
if is_matching_count(usage_count, behavior_entry.matcher):
|
|
81
|
+
self._behavior_usage_by_index[behavior_entry.order] = usage_count + 1
|
|
82
|
+
return behavior_entry.behavior, expected_events, True
|
|
83
|
+
|
|
84
|
+
return None, expected_events, len(matched_behaviors) > 0
|
|
85
|
+
|
|
86
|
+
def _use_behavior(
|
|
87
|
+
self,
|
|
88
|
+
event_entry: EventEntry,
|
|
89
|
+
default_return_value: object = None,
|
|
90
|
+
) -> BehaviorResult:
|
|
91
|
+
event = event_entry.event
|
|
92
|
+
behavior, expected_events, is_found = self._consume_behavior(event_entry)
|
|
93
|
+
|
|
94
|
+
if behavior is None:
|
|
95
|
+
return_value = default_return_value
|
|
96
|
+
|
|
97
|
+
elif behavior.error:
|
|
98
|
+
raise behavior.error
|
|
99
|
+
|
|
100
|
+
elif behavior.action:
|
|
101
|
+
if isinstance(event, CallEvent):
|
|
102
|
+
args = event.args
|
|
103
|
+
kwargs = event.kwargs
|
|
104
|
+
elif isinstance(event, AttributeEvent) and event.value is not MISSING:
|
|
105
|
+
args = (event.value,)
|
|
106
|
+
kwargs = {}
|
|
107
|
+
else:
|
|
108
|
+
args = ()
|
|
109
|
+
kwargs = {}
|
|
110
|
+
|
|
111
|
+
return_value = behavior.action(*args, **kwargs)
|
|
112
|
+
|
|
113
|
+
elif behavior.context is not MISSING:
|
|
114
|
+
return_value = contextlib.nullcontext(behavior.context)
|
|
115
|
+
|
|
116
|
+
else:
|
|
117
|
+
return_value = behavior.return_value
|
|
118
|
+
|
|
119
|
+
return BehaviorResult(
|
|
120
|
+
is_found=is_found,
|
|
121
|
+
expected_events=expected_events,
|
|
122
|
+
return_value=return_value,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def _add_event(
|
|
126
|
+
self,
|
|
127
|
+
mock: MockInfo,
|
|
128
|
+
event: Event,
|
|
129
|
+
event_state: EventState,
|
|
130
|
+
) -> EventEntry:
|
|
131
|
+
event_entry = EventEntry(mock, event, event_state, len(self._events))
|
|
132
|
+
|
|
133
|
+
self._events.append(event_entry)
|
|
134
|
+
|
|
135
|
+
return event_entry
|
|
136
|
+
|
|
137
|
+
def _get_current_attribute_value(self, event_entry: EventEntry) -> object:
|
|
138
|
+
if event_entry.mock.id in self._attribute_mocks_by_id:
|
|
139
|
+
return self._attribute_mocks_by_id[event_entry.mock.id]
|
|
140
|
+
|
|
141
|
+
for behavior_entry in reversed(self._behaviors):
|
|
142
|
+
if is_matching_behavior(event_entry, behavior_entry):
|
|
143
|
+
return behavior_entry.behavior.return_value
|
|
144
|
+
|
|
145
|
+
return MISSING
|
|
146
|
+
|
|
147
|
+
def use_call_behavior(
|
|
148
|
+
self,
|
|
149
|
+
mock: MockInfo,
|
|
150
|
+
event: CallEvent,
|
|
151
|
+
event_state: EventState,
|
|
152
|
+
) -> BehaviorResult:
|
|
153
|
+
event_entry = self._add_event(mock, event, event_state)
|
|
154
|
+
behavior_result = self._use_behavior(event_entry)
|
|
155
|
+
|
|
156
|
+
return behavior_result
|
|
157
|
+
|
|
158
|
+
def use_attribute_behavior(
|
|
159
|
+
self,
|
|
160
|
+
mock: MockInfo,
|
|
161
|
+
event: AttributeEvent,
|
|
162
|
+
event_state: EventState,
|
|
163
|
+
default_return_value: object = None,
|
|
164
|
+
) -> object:
|
|
165
|
+
event_entry = self._add_event(mock, event, event_state)
|
|
166
|
+
|
|
167
|
+
if (
|
|
168
|
+
event.type == AttributeEventType.GET
|
|
169
|
+
and mock.id in self._attribute_mocks_by_id
|
|
170
|
+
):
|
|
171
|
+
return self._attribute_mocks_by_id[mock.id]
|
|
172
|
+
|
|
173
|
+
if event.type == AttributeEventType.SET:
|
|
174
|
+
self._attribute_mocks_by_id[mock.id] = event.value
|
|
175
|
+
elif event.type == AttributeEventType.DELETE:
|
|
176
|
+
self._attribute_mocks_by_id.pop(mock.id, None)
|
|
177
|
+
|
|
178
|
+
behavior_result = self._use_behavior(event_entry, default_return_value)
|
|
179
|
+
|
|
180
|
+
return behavior_result.return_value
|
|
181
|
+
|
|
182
|
+
def use_verification(
|
|
183
|
+
self,
|
|
184
|
+
mock: MockInfo,
|
|
185
|
+
matcher: EventMatcher,
|
|
186
|
+
) -> VerificationResult:
|
|
187
|
+
mock_events = [
|
|
188
|
+
event_entry
|
|
189
|
+
for event_entry in self._events
|
|
190
|
+
if is_verifiable_mock_event(event_entry, mock)
|
|
191
|
+
]
|
|
192
|
+
matching_events = [
|
|
193
|
+
event_entry
|
|
194
|
+
for event_entry in mock_events
|
|
195
|
+
if is_matching_event(event_entry, matcher)
|
|
196
|
+
]
|
|
197
|
+
verification = VerificationEntry(mock, matcher, matching_events)
|
|
198
|
+
is_success = is_successful_verify(verification)
|
|
199
|
+
is_redundant = is_redundant_verify(verification, self._behaviors)
|
|
200
|
+
|
|
201
|
+
if is_success and self._order_verification:
|
|
202
|
+
self._order_verification.verifications.append(verification)
|
|
203
|
+
|
|
204
|
+
return VerificationResult(
|
|
205
|
+
is_success=is_success,
|
|
206
|
+
is_redundant=is_redundant,
|
|
207
|
+
mock_events=mock_events,
|
|
208
|
+
matching_events=matching_events,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
def push_behaviors(
|
|
212
|
+
self,
|
|
213
|
+
mock: MockInfo,
|
|
214
|
+
matcher: EventMatcher,
|
|
215
|
+
behaviors: list[Behavior],
|
|
216
|
+
) -> None:
|
|
217
|
+
for reversed_index, behavior in enumerate(reversed(behaviors)):
|
|
218
|
+
times = (
|
|
219
|
+
1
|
|
220
|
+
if matcher.options.times is None and reversed_index != 0
|
|
221
|
+
else matcher.options.times
|
|
222
|
+
)
|
|
223
|
+
matcher = EventMatcher(
|
|
224
|
+
event=matcher.event,
|
|
225
|
+
options=matcher.options._replace(times=times),
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
self._behaviors.append(
|
|
229
|
+
BehaviorEntry(mock, matcher, behavior, order=len(self._behaviors))
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
def peek_last_attribute_mock(self, value: object) -> MockInfo | None:
|
|
233
|
+
try:
|
|
234
|
+
event_entry = self._events[-1]
|
|
235
|
+
except IndexError:
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
if (
|
|
239
|
+
isinstance(event_entry.event, AttributeEvent)
|
|
240
|
+
and event_entry.event.type == AttributeEventType.GET
|
|
241
|
+
and self._get_current_attribute_value(event_entry) is value
|
|
242
|
+
):
|
|
243
|
+
return event_entry.mock
|
|
244
|
+
|
|
245
|
+
return None
|
|
246
|
+
|
|
247
|
+
@contextlib.contextmanager
|
|
248
|
+
def verify_order(self) -> collections.abc.Iterator[OrderVerificationResult]:
|
|
249
|
+
result = OrderVerificationResult(is_success=True)
|
|
250
|
+
self._order_verification = result
|
|
251
|
+
yield result
|
|
252
|
+
self._order_verification = None
|
|
253
|
+
|
|
254
|
+
for verification in result.verifications:
|
|
255
|
+
result.all_events.extend(verification.matching_events)
|
|
256
|
+
|
|
257
|
+
result.all_events.sort(key=lambda event: event.order)
|
|
258
|
+
result.is_success = is_successful_verify_order(result.verifications)
|
|
259
|
+
|
|
260
|
+
def reset(self) -> None:
|
|
261
|
+
self._events.clear()
|
|
262
|
+
self._behaviors.clear()
|
|
263
|
+
self._behavior_usage_by_index.clear()
|
|
264
|
+
self._attribute_mocks_by_id.clear()
|
|
265
|
+
self._order_verification = None
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Message string generation."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Iterable
|
|
5
|
+
|
|
6
|
+
from .values import (
|
|
7
|
+
AttributeEvent,
|
|
8
|
+
AttributeEventType,
|
|
9
|
+
Event,
|
|
10
|
+
EventEntry,
|
|
11
|
+
VerificationEntry,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def stringify_event(
|
|
16
|
+
mock_name: str, event: Event, ignore_extra_args: bool = False
|
|
17
|
+
) -> str:
|
|
18
|
+
"""Stringify a call to something human readable."""
|
|
19
|
+
if isinstance(event, AttributeEvent):
|
|
20
|
+
if event.type == AttributeEventType.SET:
|
|
21
|
+
return f"{mock_name} = {event.value}"
|
|
22
|
+
elif event.type == AttributeEventType.DELETE:
|
|
23
|
+
return f"del {mock_name}"
|
|
24
|
+
|
|
25
|
+
# unused, attribute get events are not logged
|
|
26
|
+
return mock_name # pragma: no cover
|
|
27
|
+
|
|
28
|
+
else:
|
|
29
|
+
args_list = [repr(arg) for arg in event.args]
|
|
30
|
+
kwargs_list = [f"{key}={val!r}" for key, val in event.kwargs.items()]
|
|
31
|
+
extra_args_msg = (
|
|
32
|
+
" - ignoring unspecified arguments" if ignore_extra_args else ""
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
return f"{mock_name}({', '.join(args_list + kwargs_list)}){extra_args_msg}"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def stringify_event_list(mock_name: str, events: Iterable[Event]) -> str:
|
|
39
|
+
"""Stringify a sequence of mock events into an ordered list."""
|
|
40
|
+
return _stringify_ordered_list(
|
|
41
|
+
stringify_event(mock_name, event) for event in events
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def stringify_event_entry_list(event_entries: Iterable[EventEntry]) -> str:
|
|
46
|
+
"""Stringify a sequence of verifications into an ordered list."""
|
|
47
|
+
return _stringify_ordered_list(
|
|
48
|
+
stringify_event(entry.mock.name, entry.event) for entry in event_entries
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def stringify_verification_list(verifications: Iterable[VerificationEntry]) -> str:
|
|
53
|
+
"""Stringify a sequence of verifications into an ordered list."""
|
|
54
|
+
events: list[tuple[str, Event]] = []
|
|
55
|
+
|
|
56
|
+
for verification in verifications:
|
|
57
|
+
times = verification.matcher.options.times
|
|
58
|
+
if times is None:
|
|
59
|
+
times = 1
|
|
60
|
+
|
|
61
|
+
for _ in range(times):
|
|
62
|
+
events.append((verification.mock.name, verification.matcher.event))
|
|
63
|
+
|
|
64
|
+
return _stringify_ordered_list(
|
|
65
|
+
stringify_event(mock_name, event) for mock_name, event in events
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def count(count: int, noun: str) -> str:
|
|
70
|
+
"""Count a given noun, pluralizing if necessary."""
|
|
71
|
+
return f"{count} {noun}{'s' if count != 1 else ''}"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def join_lines(*lines: str) -> str:
|
|
75
|
+
"""Join a list of lines with newline characters."""
|
|
76
|
+
return os.linesep.join(lines).strip()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _stringify_ordered_list(lines: Iterable[str]) -> str:
|
|
80
|
+
return join_lines(*(f"{i + 1}.\t{line}" for i, line in enumerate(lines)))
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Value objects that move around the mocking system."""
|
|
2
|
+
|
|
3
|
+
import enum
|
|
4
|
+
import inspect
|
|
5
|
+
from typing import Callable, NamedTuple, final
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@final
|
|
9
|
+
class MISSING:
|
|
10
|
+
"""Value not specified sentinel.
|
|
11
|
+
|
|
12
|
+
Used when `None` could be a valid value,
|
|
13
|
+
so `Optional` would be inappropriate.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AttributeEventType(str, enum.Enum):
|
|
18
|
+
GET = "get"
|
|
19
|
+
SET = "set"
|
|
20
|
+
DELETE = "delete"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AttributeEvent(NamedTuple):
|
|
24
|
+
type: AttributeEventType
|
|
25
|
+
value: object = MISSING
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def get(cls) -> "AttributeEvent":
|
|
29
|
+
return cls(AttributeEventType.GET)
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def set(cls, value: object) -> "AttributeEvent":
|
|
33
|
+
return cls(AttributeEventType.SET, value)
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def delete(cls) -> "AttributeEvent":
|
|
37
|
+
return cls(AttributeEventType.DELETE)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class CallEvent(NamedTuple):
|
|
41
|
+
"""Event representing a call to a function or method."""
|
|
42
|
+
|
|
43
|
+
args: tuple[object, ...]
|
|
44
|
+
kwargs: dict[str, object]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
Event = CallEvent | AttributeEvent
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class EventState(NamedTuple):
|
|
51
|
+
is_entered: bool
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class MatchOptions(NamedTuple):
|
|
55
|
+
times: int | None
|
|
56
|
+
ignore_extra_args: bool
|
|
57
|
+
is_entered: bool | None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Behavior(NamedTuple):
|
|
61
|
+
return_value: object = None
|
|
62
|
+
error: Exception | None = None
|
|
63
|
+
action: Callable[..., object] | None = None
|
|
64
|
+
context: object = MISSING
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class EventMatcher(NamedTuple):
|
|
68
|
+
event: Event
|
|
69
|
+
options: MatchOptions
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class MockInfo(NamedTuple):
|
|
73
|
+
id: int
|
|
74
|
+
name: str
|
|
75
|
+
is_async: bool
|
|
76
|
+
signature: inspect.Signature | None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class EventEntry(NamedTuple):
|
|
80
|
+
mock: MockInfo
|
|
81
|
+
event: Event
|
|
82
|
+
state: EventState
|
|
83
|
+
order: int
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class BehaviorEntry(NamedTuple):
|
|
87
|
+
mock: MockInfo
|
|
88
|
+
matcher: EventMatcher
|
|
89
|
+
behavior: Behavior
|
|
90
|
+
order: int
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class VerificationEntry(NamedTuple):
|
|
94
|
+
mock: MockInfo
|
|
95
|
+
matcher: EventMatcher
|
|
96
|
+
matching_events: list[EventEntry]
|