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.
@@ -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]