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 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
- """An error raised if a name is not provided for a mock.
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
- """An error raised when a Decoy method is called without rehearsal(s).
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 error raised when an asynchronous function is used with a synchronous mock.
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 VerifyError(AssertionError):
59
- """An error raised when actual calls do not match rehearsals given to `verify`.
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
- [spying with verify]: usage/verify.md
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 is_match:
184
- is_match = (
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 is_match
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
- if is_match:
226
- try:
227
- is_match = key in target and target[key] == value # type: ignore[index,operator]
228
- except TypeError:
229
- is_match = False
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 is_match
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)