decoy 2.2.2__tar.gz → 2.4.0__tar.gz
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-2.2.2 → decoy-2.4.0}/PKG-INFO +9 -17
- {decoy-2.2.2 → decoy-2.4.0}/README.md +1 -8
- {decoy-2.2.2 → decoy-2.4.0}/decoy/call_handler.py +7 -2
- {decoy-2.2.2 → decoy-2.4.0}/decoy/errors.py +28 -26
- {decoy-2.2.2 → decoy-2.4.0}/decoy/matchers.py +63 -48
- decoy-2.4.0/decoy/next/__init__.py +21 -0
- decoy-2.4.0/decoy/next/_internal/compare.py +138 -0
- decoy-2.4.0/decoy/next/_internal/decoy.py +231 -0
- decoy-2.4.0/decoy/next/_internal/errors.py +103 -0
- decoy-2.4.0/decoy/next/_internal/inspect.py +229 -0
- decoy-2.4.0/decoy/next/_internal/matcher.py +328 -0
- decoy-2.4.0/decoy/next/_internal/mock.py +220 -0
- decoy-2.4.0/decoy/next/_internal/state.py +265 -0
- decoy-2.4.0/decoy/next/_internal/stringify.py +80 -0
- decoy-2.4.0/decoy/next/_internal/values.py +96 -0
- decoy-2.4.0/decoy/next/_internal/verify.py +78 -0
- decoy-2.4.0/decoy/next/_internal/warnings.py +35 -0
- decoy-2.4.0/decoy/next/_internal/when.py +136 -0
- decoy-2.4.0/decoy/py.typed +0 -0
- {decoy-2.2.2 → decoy-2.4.0}/decoy/spy.py +1 -1
- decoy-2.4.0/pyproject.toml +120 -0
- decoy-2.2.2/LICENSE +0 -21
- decoy-2.2.2/pyproject.toml +0 -115
- {decoy-2.2.2 → decoy-2.4.0}/decoy/__init__.py +0 -0
- {decoy-2.2.2 → decoy-2.4.0}/decoy/context_managers.py +0 -0
- {decoy-2.2.2 → decoy-2.4.0}/decoy/core.py +0 -0
- {decoy-2.2.2 → decoy-2.4.0}/decoy/mypy/__init__.py +0 -0
- {decoy-2.2.2 → decoy-2.4.0}/decoy/mypy/plugin.py +0 -0
- /decoy-2.2.2/decoy/py.typed → /decoy-2.4.0/decoy/next/_internal/__init__.py +0 -0
- {decoy-2.2.2 → decoy-2.4.0}/decoy/pytest_plugin.py +0 -0
- {decoy-2.2.2 → decoy-2.4.0}/decoy/spy_core.py +0 -0
- {decoy-2.2.2 → decoy-2.4.0}/decoy/spy_events.py +0 -0
- {decoy-2.2.2 → decoy-2.4.0}/decoy/spy_log.py +0 -0
- {decoy-2.2.2 → decoy-2.4.0}/decoy/stringify.py +0 -0
- {decoy-2.2.2 → decoy-2.4.0}/decoy/stub_store.py +0 -0
- {decoy-2.2.2 → decoy-2.4.0}/decoy/types.py +0 -0
- {decoy-2.2.2 → decoy-2.4.0}/decoy/verifier.py +0 -0
- {decoy-2.2.2 → decoy-2.4.0}/decoy/warning_checker.py +0 -0
- {decoy-2.2.2 → decoy-2.4.0}/decoy/warnings.py +0 -0
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: decoy
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: Opinionated mocking library for Python
|
|
5
|
-
License-Expression: MIT
|
|
6
|
-
License-File: LICENSE
|
|
7
5
|
Author: Michael Cousins
|
|
8
|
-
Author-email: michael@cousins.io
|
|
9
|
-
|
|
6
|
+
Author-email: Michael Cousins <michael@cousins.io>>
|
|
7
|
+
License-Expression: MIT
|
|
10
8
|
Classifier: Development Status :: 5 - Production/Stable
|
|
11
9
|
Classifier: Intended Audience :: Developers
|
|
12
10
|
Classifier: Operating System :: OS Independent
|
|
13
11
|
Classifier: Topic :: Software Development :: Testing
|
|
14
12
|
Classifier: Topic :: Software Development :: Testing :: Mocking
|
|
15
13
|
Classifier: Typing :: Typed
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
Requires-Dist: typing-extensions>=4.10.0 ; python_full_version >= '3.10' and python_full_version < '3.13'
|
|
15
|
+
Requires-Python: >=3.7
|
|
18
16
|
Project-URL: Homepage, https://michael.cousins.io/decoy/
|
|
19
|
-
Project-URL:
|
|
17
|
+
Project-URL: Documentation, https://michael.cousins.io/decoy/
|
|
20
18
|
Project-URL: Repository, https://github.com/mcous/decoy
|
|
19
|
+
Project-URL: Issues, https://github.com/mcous/decoy/issues
|
|
20
|
+
Project-URL: Changelog, https://github.com/mcous/decoy/releases
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
|
|
23
23
|
<div align="center">
|
|
@@ -30,7 +30,7 @@ Description-Content-Type: text/markdown
|
|
|
30
30
|
<a title="Code Coverage" href="https://app.codecov.io/gh/mcous/decoy/"><img src="https://img.shields.io/codecov/c/github/mcous/decoy?style=flat-square"></a>
|
|
31
31
|
<a title="License" href="https://github.com/mcous/decoy/blob/main/LICENSE"><img src="https://img.shields.io/github/license/mcous/decoy?style=flat-square"></a>
|
|
32
32
|
<a title="PyPI Version"href="https://pypi.org/project/decoy/"><img src="https://img.shields.io/pypi/v/decoy?style=flat-square"></a>
|
|
33
|
-
<a title="Supported Python Versions" href="https://pypi.org/project/decoy/"><img src="https://img.shields.io/
|
|
33
|
+
<a title="Supported Python Versions" href="https://pypi.org/project/decoy/"><img src="https://img.shields.io/python/required-version-toml?style=flat-square&tomlFilePath=https%3A%2F%2Fraw.githubusercontent.com%2Fmcous%2Fdecoy%2Fmain%2Fpyproject.toml"></a>
|
|
34
34
|
</p>
|
|
35
35
|
<p>
|
|
36
36
|
<a href="https://michael.cousins.io/decoy/" class="decoy-hidden">Usage guide and documentation</a>
|
|
@@ -44,14 +44,7 @@ Decoy mocks are **async/await** and **type-checking** friendly. Decoy is heavily
|
|
|
44
44
|
## Install
|
|
45
45
|
|
|
46
46
|
```bash
|
|
47
|
-
# pip
|
|
48
47
|
pip install decoy
|
|
49
|
-
|
|
50
|
-
# poetry
|
|
51
|
-
poetry add --dev decoy
|
|
52
|
-
|
|
53
|
-
# pipenv
|
|
54
|
-
pipenv install --dev decoy
|
|
55
48
|
```
|
|
56
49
|
|
|
57
50
|
## Setup
|
|
@@ -171,4 +164,3 @@ See [spying with verify][] for more details.
|
|
|
171
164
|
[creating mocks]: https://michael.cousins.io/decoy/usage/create/
|
|
172
165
|
[stubbing with when]: https://michael.cousins.io/decoy/usage/when/
|
|
173
166
|
[spying with verify]: https://michael.cousins.io/decoy/usage/verify/
|
|
174
|
-
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<a title="Code Coverage" href="https://app.codecov.io/gh/mcous/decoy/"><img src="https://img.shields.io/codecov/c/github/mcous/decoy?style=flat-square"></a>
|
|
9
9
|
<a title="License" href="https://github.com/mcous/decoy/blob/main/LICENSE"><img src="https://img.shields.io/github/license/mcous/decoy?style=flat-square"></a>
|
|
10
10
|
<a title="PyPI Version"href="https://pypi.org/project/decoy/"><img src="https://img.shields.io/pypi/v/decoy?style=flat-square"></a>
|
|
11
|
-
<a title="Supported Python Versions" href="https://pypi.org/project/decoy/"><img src="https://img.shields.io/
|
|
11
|
+
<a title="Supported Python Versions" href="https://pypi.org/project/decoy/"><img src="https://img.shields.io/python/required-version-toml?style=flat-square&tomlFilePath=https%3A%2F%2Fraw.githubusercontent.com%2Fmcous%2Fdecoy%2Fmain%2Fpyproject.toml"></a>
|
|
12
12
|
</p>
|
|
13
13
|
<p>
|
|
14
14
|
<a href="https://michael.cousins.io/decoy/" class="decoy-hidden">Usage guide and documentation</a>
|
|
@@ -22,14 +22,7 @@ Decoy mocks are **async/await** and **type-checking** friendly. Decoy is heavily
|
|
|
22
22
|
## Install
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
|
-
# pip
|
|
26
25
|
pip install decoy
|
|
27
|
-
|
|
28
|
-
# poetry
|
|
29
|
-
poetry add --dev decoy
|
|
30
|
-
|
|
31
|
-
# pipenv
|
|
32
|
-
pipenv install --dev decoy
|
|
33
26
|
```
|
|
34
27
|
|
|
35
28
|
## Setup
|
|
@@ -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
|
|
|
@@ -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."""
|
|
@@ -28,16 +28,17 @@ See the [matchers guide][] for more details.
|
|
|
28
28
|
"""
|
|
29
29
|
|
|
30
30
|
from re import compile as compile_re
|
|
31
|
-
from typing import
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
31
|
+
from typing import (
|
|
32
|
+
Any,
|
|
33
|
+
Generic,
|
|
34
|
+
List,
|
|
35
|
+
Mapping,
|
|
36
|
+
Optional,
|
|
37
|
+
Pattern,
|
|
38
|
+
Type,
|
|
39
|
+
TypeVar,
|
|
40
|
+
cast,
|
|
41
|
+
)
|
|
41
42
|
|
|
42
43
|
|
|
43
44
|
class _AnythingOrNone:
|
|
@@ -177,14 +178,11 @@ class _HasAttributes:
|
|
|
177
178
|
|
|
178
179
|
def __eq__(self, target: object) -> bool:
|
|
179
180
|
"""Return true if target matches all given attributes."""
|
|
180
|
-
is_match = True
|
|
181
181
|
for attr_name, value in self._attributes.items():
|
|
182
|
-
if
|
|
183
|
-
|
|
184
|
-
hasattr(target, attr_name) and getattr(target, attr_name) == value
|
|
185
|
-
)
|
|
182
|
+
if not hasattr(target, attr_name) or getattr(target, attr_name) != value:
|
|
183
|
+
return False
|
|
186
184
|
|
|
187
|
-
return
|
|
185
|
+
return True
|
|
188
186
|
|
|
189
187
|
def __repr__(self) -> str:
|
|
190
188
|
"""Return a string representation of the matcher."""
|
|
@@ -218,16 +216,14 @@ class _DictMatching:
|
|
|
218
216
|
|
|
219
217
|
def __eq__(self, target: object) -> bool:
|
|
220
218
|
"""Return true if target matches all given keys/values."""
|
|
221
|
-
is_match = True
|
|
222
|
-
|
|
223
219
|
for key, value in self._values.items():
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
|
229
225
|
|
|
230
|
-
return
|
|
226
|
+
return True
|
|
231
227
|
|
|
232
228
|
def __repr__(self) -> str:
|
|
233
229
|
"""Return a string representation of the matcher."""
|
|
@@ -318,10 +314,12 @@ def StringMatching(match: str) -> str:
|
|
|
318
314
|
class _ErrorMatching:
|
|
319
315
|
_error_type: Type[BaseException]
|
|
320
316
|
_string_matcher: Optional[_StringMatching]
|
|
317
|
+
_match: Optional[str]
|
|
321
318
|
|
|
322
319
|
def __init__(self, error: Type[BaseException], match: Optional[str] = None) -> None:
|
|
323
320
|
"""Initialize with the Exception type and optional message matcher."""
|
|
324
321
|
self._error_type = error
|
|
322
|
+
self._match = match
|
|
325
323
|
self._string_matcher = _StringMatching(match) if match is not None else None
|
|
326
324
|
|
|
327
325
|
def __eq__(self, target: object) -> bool:
|
|
@@ -337,9 +335,7 @@ class _ErrorMatching:
|
|
|
337
335
|
|
|
338
336
|
def __repr__(self) -> str:
|
|
339
337
|
"""Return a string representation of the matcher."""
|
|
340
|
-
return
|
|
341
|
-
f"<ErrorMatching {self._error_type.__name__} match={self._string_matcher}>"
|
|
342
|
-
)
|
|
338
|
+
return f"<ErrorMatching {self._error_type.__name__} match={self._match!r}>"
|
|
343
339
|
|
|
344
340
|
|
|
345
341
|
ErrorT = TypeVar("ErrorT", bound=BaseException)
|
|
@@ -361,12 +357,32 @@ def ErrorMatching(error: Type[ErrorT], match: Optional[str] = None) -> ErrorT:
|
|
|
361
357
|
return cast(ErrorT, _ErrorMatching(error, match))
|
|
362
358
|
|
|
363
359
|
|
|
364
|
-
|
|
360
|
+
CapturedT = TypeVar("CapturedT")
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
class ValueCaptor(Generic[CapturedT]):
|
|
364
|
+
"""Match anything, capturing its value for further assertions.
|
|
365
|
+
|
|
366
|
+
Compare against the `matcher` property to capture a value.
|
|
367
|
+
The last captured value is available via `captor.value`,
|
|
368
|
+
while all captured values are stored in `captor.values`.
|
|
369
|
+
|
|
370
|
+
!!! example
|
|
371
|
+
```python
|
|
372
|
+
captor = ValueCaptor[str]()
|
|
373
|
+
assert "foobar" == captor.matcher
|
|
374
|
+
print(captor.value) # "foobar"
|
|
375
|
+
print(captor.values) # ["foobar"]
|
|
376
|
+
```
|
|
377
|
+
"""
|
|
378
|
+
|
|
379
|
+
_values: List[object]
|
|
380
|
+
|
|
365
381
|
def __init__(self) -> None:
|
|
366
|
-
self._values
|
|
382
|
+
self._values = []
|
|
367
383
|
|
|
368
384
|
def __eq__(self, target: object) -> bool:
|
|
369
|
-
"""
|
|
385
|
+
"""Captors are always "equal" to a given target."""
|
|
370
386
|
self._values.append(target)
|
|
371
387
|
return True
|
|
372
388
|
|
|
@@ -375,11 +391,19 @@ class _Captor:
|
|
|
375
391
|
return "<Captor>"
|
|
376
392
|
|
|
377
393
|
@property
|
|
378
|
-
def
|
|
379
|
-
"""
|
|
394
|
+
def matcher(self) -> CapturedT:
|
|
395
|
+
"""Match anything, capturing its value.
|
|
396
|
+
|
|
397
|
+
This method exists as a type-checking convenience.
|
|
398
|
+
"""
|
|
399
|
+
return cast(CapturedT, self)
|
|
400
|
+
|
|
401
|
+
@property
|
|
402
|
+
def value(self) -> object:
|
|
403
|
+
"""The latest captured value.
|
|
380
404
|
|
|
381
405
|
Raises:
|
|
382
|
-
AssertionError:
|
|
406
|
+
AssertionError: no value has been captured.
|
|
383
407
|
"""
|
|
384
408
|
if len(self._values) == 0:
|
|
385
409
|
raise AssertionError("No value captured by captor.")
|
|
@@ -387,24 +411,15 @@ class _Captor:
|
|
|
387
411
|
return self._values[-1]
|
|
388
412
|
|
|
389
413
|
@property
|
|
390
|
-
def values(self) -> List[
|
|
391
|
-
"""
|
|
414
|
+
def values(self) -> List[object]:
|
|
415
|
+
"""All captured values."""
|
|
392
416
|
return self._values
|
|
393
417
|
|
|
394
418
|
|
|
395
419
|
def Captor() -> Any:
|
|
396
|
-
"""Match anything, capturing its value.
|
|
420
|
+
"""Match anything, capturing its value for further assertions.
|
|
397
421
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
helpful if a captor needs to be triggered multiple times.
|
|
401
|
-
|
|
402
|
-
!!! example
|
|
403
|
-
```python
|
|
404
|
-
captor = Captor()
|
|
405
|
-
assert "foobar" == captor
|
|
406
|
-
print(captor.value) # "foobar"
|
|
407
|
-
print(captor.values) # ["foobar"]
|
|
408
|
-
```
|
|
422
|
+
!!! tip
|
|
423
|
+
Prefer [decoy.matchers.ValueCaptor][], which has better type annotations.
|
|
409
424
|
"""
|
|
410
|
-
return
|
|
425
|
+
return ValueCaptor()
|
|
@@ -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
|
+
]
|
|
@@ -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
|
+
)
|