decoy 2.1.2__py3-none-any.whl → 2.2.1__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/__init__.py +14 -20
- decoy/call_handler.py +1 -0
- decoy/context_managers.py +2 -1
- decoy/core.py +1 -0
- decoy/errors.py +20 -11
- decoy/matchers.py +60 -3
- decoy/mypy/__init__.py +1 -0
- decoy/mypy/plugin.py +1 -0
- decoy/pytest_plugin.py +1 -0
- decoy/spy.py +3 -4
- decoy/spy_core.py +2 -11
- decoy/spy_events.py +1 -0
- decoy/spy_log.py +10 -9
- decoy/stringify.py +1 -0
- decoy/stub_store.py +1 -0
- decoy/types.py +1 -0
- decoy/verifier.py +3 -2
- decoy/warning_checker.py +5 -4
- decoy/warnings.py +23 -21
- {decoy-2.1.2.dist-info → decoy-2.2.1.dist-info}/METADATA +7 -14
- decoy-2.2.1.dist-info/RECORD +25 -0
- {decoy-2.1.2.dist-info → decoy-2.2.1.dist-info}/WHEEL +1 -1
- decoy-2.1.2.dist-info/RECORD +0 -25
- {decoy-2.1.2.dist-info → decoy-2.2.1.dist-info}/entry_points.txt +0 -0
- {decoy-2.1.2.dist-info → decoy-2.2.1.dist-info/licenses}/LICENSE +0 -0
decoy/__init__.py
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
"""Decoy stubbing and spying library."""
|
|
2
|
+
|
|
2
3
|
from typing import Any, Callable, Coroutine, Generic, Optional, Union, overload
|
|
3
4
|
|
|
4
5
|
from . import errors, matchers, warnings
|
|
5
|
-
from .core import DecoyCore, StubCore, PropCore
|
|
6
|
-
from .types import ClassT, ContextValueT, FuncT, ReturnT
|
|
7
6
|
from .context_managers import (
|
|
8
|
-
ContextManager,
|
|
9
7
|
AsyncContextManager,
|
|
10
|
-
GeneratorContextManager,
|
|
11
8
|
AsyncGeneratorContextManager,
|
|
9
|
+
ContextManager,
|
|
10
|
+
GeneratorContextManager,
|
|
12
11
|
)
|
|
12
|
+
from .core import DecoyCore, PropCore, StubCore
|
|
13
|
+
from .types import ClassT, ContextValueT, FuncT, ReturnT
|
|
13
14
|
|
|
14
15
|
# ensure decoy does not pollute pytest tracebacks
|
|
15
16
|
__tracebackhide__ = True
|
|
@@ -40,16 +41,13 @@ class Decoy:
|
|
|
40
41
|
self._core = DecoyCore()
|
|
41
42
|
|
|
42
43
|
@overload
|
|
43
|
-
def mock(self, *, cls: Callable[..., ClassT]) -> ClassT:
|
|
44
|
-
...
|
|
44
|
+
def mock(self, *, cls: Callable[..., ClassT]) -> ClassT: ...
|
|
45
45
|
|
|
46
46
|
@overload
|
|
47
|
-
def mock(self, *, func: FuncT) -> FuncT:
|
|
48
|
-
...
|
|
47
|
+
def mock(self, *, func: FuncT) -> FuncT: ...
|
|
49
48
|
|
|
50
49
|
@overload
|
|
51
|
-
def mock(self, *, name: str, is_async: bool = False) -> Any:
|
|
52
|
-
...
|
|
50
|
+
def mock(self, *, name: str, is_async: bool = False) -> Any: ...
|
|
53
51
|
|
|
54
52
|
def mock(
|
|
55
53
|
self,
|
|
@@ -84,7 +82,7 @@ class Decoy:
|
|
|
84
82
|
spec = cls or func
|
|
85
83
|
|
|
86
84
|
if spec is None and name is None:
|
|
87
|
-
raise errors.MockNameRequiredError()
|
|
85
|
+
raise errors.MockNameRequiredError.create()
|
|
88
86
|
|
|
89
87
|
return self._core.mock(spec=spec, name=name, is_async=is_async)
|
|
90
88
|
|
|
@@ -255,29 +253,25 @@ class Stub(Generic[ReturnT]):
|
|
|
255
253
|
def then_enter_with(
|
|
256
254
|
self: "Stub[ContextManager[ContextValueT]]",
|
|
257
255
|
value: ContextValueT,
|
|
258
|
-
) -> None:
|
|
259
|
-
...
|
|
256
|
+
) -> None: ...
|
|
260
257
|
|
|
261
258
|
@overload
|
|
262
259
|
def then_enter_with(
|
|
263
260
|
self: "Stub[AsyncContextManager[ContextValueT]]",
|
|
264
261
|
value: ContextValueT,
|
|
265
|
-
) -> None:
|
|
266
|
-
...
|
|
262
|
+
) -> None: ...
|
|
267
263
|
|
|
268
264
|
@overload
|
|
269
265
|
def then_enter_with(
|
|
270
266
|
self: "Stub[GeneratorContextManager[ContextValueT]]",
|
|
271
267
|
value: ContextValueT,
|
|
272
|
-
) -> None:
|
|
273
|
-
...
|
|
268
|
+
) -> None: ...
|
|
274
269
|
|
|
275
270
|
@overload
|
|
276
271
|
def then_enter_with(
|
|
277
272
|
self: "Stub[AsyncGeneratorContextManager[ContextValueT]]",
|
|
278
273
|
value: ContextValueT,
|
|
279
|
-
) -> None:
|
|
280
|
-
...
|
|
274
|
+
) -> None: ...
|
|
281
275
|
|
|
282
276
|
def then_enter_with(
|
|
283
277
|
self: Union[
|
|
@@ -347,4 +341,4 @@ class Prop(Generic[ReturnT]):
|
|
|
347
341
|
self._core.delete()
|
|
348
342
|
|
|
349
343
|
|
|
350
|
-
__all__ = ["Decoy", "
|
|
344
|
+
__all__ = ["Decoy", "Prop", "Stub", "errors", "matchers", "warnings"]
|
decoy/call_handler.py
CHANGED
decoy/context_managers.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Wrappers around contextlib types and fallbacks."""
|
|
2
|
+
|
|
2
3
|
from typing import Any, AsyncContextManager, ContextManager, Generic, TypeVar
|
|
3
4
|
|
|
4
5
|
from contextlib import (
|
|
@@ -44,6 +45,6 @@ __all__ = [
|
|
|
44
45
|
"AsyncContextManager",
|
|
45
46
|
"AsyncGeneratorContextManager",
|
|
46
47
|
"ContextManager",
|
|
47
|
-
"GeneratorContextManager",
|
|
48
48
|
"ContextWrapper",
|
|
49
|
+
"GeneratorContextManager",
|
|
49
50
|
]
|
decoy/core.py
CHANGED
decoy/errors.py
CHANGED
|
@@ -4,6 +4,7 @@ See the [errors guide][] for more details.
|
|
|
4
4
|
|
|
5
5
|
[errors guide]: usage/errors-and-warnings.md#errors
|
|
6
6
|
"""
|
|
7
|
+
|
|
7
8
|
from typing import Optional, Sequence
|
|
8
9
|
|
|
9
10
|
from .spy_events import SpyEvent, VerifyRehearsal
|
|
@@ -18,8 +19,10 @@ class MockNameRequiredError(ValueError):
|
|
|
18
19
|
[MockNameRequiredError guide]: usage/errors-and-warnings.md#mocknamerequirederror
|
|
19
20
|
"""
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
@classmethod
|
|
23
|
+
def create(cls) -> "MockNameRequiredError":
|
|
24
|
+
"""Create a MockNameRequiredError."""
|
|
25
|
+
return cls("Mocks without `cls` or `func` require a `name`.")
|
|
23
26
|
|
|
24
27
|
|
|
25
28
|
class MissingRehearsalError(ValueError):
|
|
@@ -35,8 +38,10 @@ class MissingRehearsalError(ValueError):
|
|
|
35
38
|
[MissingRehearsalError guide]: usage/errors-and-warnings.md#missingrehearsalerror
|
|
36
39
|
"""
|
|
37
40
|
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
@classmethod
|
|
42
|
+
def create(cls) -> "MissingRehearsalError":
|
|
43
|
+
"""Create a MissingRehearsalError."""
|
|
44
|
+
return cls("Rehearsal not found.")
|
|
40
45
|
|
|
41
46
|
|
|
42
47
|
class MockNotAsyncError(TypeError):
|
|
@@ -67,12 +72,14 @@ class VerifyError(AssertionError):
|
|
|
67
72
|
calls: Sequence[SpyEvent]
|
|
68
73
|
times: Optional[int]
|
|
69
74
|
|
|
70
|
-
|
|
71
|
-
|
|
75
|
+
@classmethod
|
|
76
|
+
def create(
|
|
77
|
+
cls,
|
|
72
78
|
rehearsals: Sequence[VerifyRehearsal],
|
|
73
79
|
calls: Sequence[SpyEvent],
|
|
74
80
|
times: Optional[int],
|
|
75
|
-
) ->
|
|
81
|
+
) -> "VerifyError":
|
|
82
|
+
"""Create a VerifyError."""
|
|
76
83
|
if times is not None:
|
|
77
84
|
heading = f"Expected exactly {count(times, 'call')}:"
|
|
78
85
|
elif len(rehearsals) == 1:
|
|
@@ -87,7 +94,9 @@ class VerifyError(AssertionError):
|
|
|
87
94
|
include_calls=times is None or times == len(calls),
|
|
88
95
|
)
|
|
89
96
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
97
|
+
result = cls(message)
|
|
98
|
+
result.rehearsals = rehearsals
|
|
99
|
+
result.calls = calls
|
|
100
|
+
result.times = times
|
|
101
|
+
|
|
102
|
+
return result
|
decoy/matchers.py
CHANGED
|
@@ -26,20 +26,42 @@ See the [matchers guide][] for more details.
|
|
|
26
26
|
Identity comparisons (`is`) will not work with matchers. Decoy only uses
|
|
27
27
|
equality comparisons (`==`) for stubbing and verification.
|
|
28
28
|
"""
|
|
29
|
+
|
|
29
30
|
from re import compile as compile_re
|
|
30
31
|
from typing import cast, Any, List, Mapping, Optional, Pattern, Type, TypeVar
|
|
31
32
|
|
|
32
33
|
|
|
33
34
|
__all__ = [
|
|
34
35
|
"Anything",
|
|
36
|
+
"Captor",
|
|
37
|
+
"ErrorMatching",
|
|
35
38
|
"IsA",
|
|
36
39
|
"IsNot",
|
|
37
40
|
"StringMatching",
|
|
38
|
-
"ErrorMatching",
|
|
39
|
-
"Captor",
|
|
40
41
|
]
|
|
41
42
|
|
|
42
43
|
|
|
44
|
+
class _AnythingOrNone:
|
|
45
|
+
def __eq__(self, target: object) -> bool:
|
|
46
|
+
return True
|
|
47
|
+
|
|
48
|
+
def __repr__(self) -> str:
|
|
49
|
+
"""Return a string representation of the matcher."""
|
|
50
|
+
return "<AnythingOrNone>"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def AnythingOrNone() -> Any:
|
|
54
|
+
"""Match anything including None.
|
|
55
|
+
|
|
56
|
+
!!! example
|
|
57
|
+
```python
|
|
58
|
+
assert "foobar" == AnythingOrNone()
|
|
59
|
+
assert None == AnythingOrNone()
|
|
60
|
+
```
|
|
61
|
+
"""
|
|
62
|
+
return _AnythingOrNone()
|
|
63
|
+
|
|
64
|
+
|
|
43
65
|
class _Anything:
|
|
44
66
|
def __eq__(self, target: object) -> bool:
|
|
45
67
|
"""Return true if target is not None."""
|
|
@@ -228,6 +250,41 @@ def DictMatching(values: Mapping[str, Any]) -> Any:
|
|
|
228
250
|
return _DictMatching(values)
|
|
229
251
|
|
|
230
252
|
|
|
253
|
+
class _ListMatching:
|
|
254
|
+
_values: List[Any]
|
|
255
|
+
|
|
256
|
+
def __init__(self, values: List[Any]) -> None:
|
|
257
|
+
self._values = values
|
|
258
|
+
|
|
259
|
+
def __eq__(self, target: object) -> bool:
|
|
260
|
+
"""Return true if target matches all given values."""
|
|
261
|
+
if not hasattr(target, "__iter__"):
|
|
262
|
+
return False
|
|
263
|
+
|
|
264
|
+
return all(
|
|
265
|
+
any(item == target_item for target_item in target) for item in self._values
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
def __repr__(self) -> str:
|
|
269
|
+
"""Return a string representation of the matcher."""
|
|
270
|
+
return f"<ListMatching {self._values!r}>"
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def ListMatching(values: List[Any]) -> Any:
|
|
274
|
+
"""Match any list with the passed in values.
|
|
275
|
+
|
|
276
|
+
Arguments:
|
|
277
|
+
values: Values to check.
|
|
278
|
+
|
|
279
|
+
!!! example
|
|
280
|
+
```python
|
|
281
|
+
value = [1, 2, 3]
|
|
282
|
+
assert value == matchers.ListMatching([1, 2])
|
|
283
|
+
```
|
|
284
|
+
"""
|
|
285
|
+
return _ListMatching(values)
|
|
286
|
+
|
|
287
|
+
|
|
231
288
|
class _StringMatching:
|
|
232
289
|
_pattern: Pattern[str]
|
|
233
290
|
|
|
@@ -270,7 +327,7 @@ class _ErrorMatching:
|
|
|
270
327
|
|
|
271
328
|
def __eq__(self, target: object) -> bool:
|
|
272
329
|
"""Return true if target is not self._reject_value."""
|
|
273
|
-
error_match = type(target)
|
|
330
|
+
error_match = type(target) is self._error_type
|
|
274
331
|
message_match = (
|
|
275
332
|
str(target) == self._string_matcher
|
|
276
333
|
if self._string_matcher is not None
|
decoy/mypy/__init__.py
CHANGED
decoy/mypy/plugin.py
CHANGED
decoy/pytest_plugin.py
CHANGED
decoy/spy.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
Classes in this module are heavily inspired by the
|
|
4
4
|
[unittest.mock library](https://docs.python.org/3/library/unittest.mock.html).
|
|
5
5
|
"""
|
|
6
|
+
|
|
6
7
|
import inspect
|
|
7
8
|
from types import TracebackType
|
|
8
9
|
from typing import Any, ContextManager, Dict, Optional, Type, Union, cast, overload
|
|
@@ -186,14 +187,12 @@ class SpyCreator:
|
|
|
186
187
|
self._decoy_spy_call_handler = call_handler
|
|
187
188
|
|
|
188
189
|
@overload
|
|
189
|
-
def create(self, *, core: SpyCore) -> AnySpy:
|
|
190
|
-
...
|
|
190
|
+
def create(self, *, core: SpyCore) -> AnySpy: ...
|
|
191
191
|
|
|
192
192
|
@overload
|
|
193
193
|
def create(
|
|
194
194
|
self, *, spec: Optional[object], name: Optional[str], is_async: bool
|
|
195
|
-
) -> AnySpy:
|
|
196
|
-
...
|
|
195
|
+
) -> AnySpy: ...
|
|
197
196
|
|
|
198
197
|
def create(
|
|
199
198
|
self,
|
decoy/spy_core.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Core spy logic."""
|
|
2
|
+
|
|
2
3
|
import inspect
|
|
3
4
|
import functools
|
|
4
5
|
import warnings
|
|
@@ -15,7 +16,7 @@ from typing import (
|
|
|
15
16
|
)
|
|
16
17
|
|
|
17
18
|
from .spy_events import SpyInfo
|
|
18
|
-
from .warnings import IncorrectCallWarning
|
|
19
|
+
from .warnings import IncorrectCallWarning
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
class _FROM_SOURCE:
|
|
@@ -122,15 +123,12 @@ class SpyCore:
|
|
|
122
123
|
source = self._source
|
|
123
124
|
child_name = f"{self._name}.{name}"
|
|
124
125
|
child_source = None
|
|
125
|
-
child_found = False
|
|
126
126
|
|
|
127
127
|
if inspect.isclass(source):
|
|
128
128
|
# use type hints to get child spec for class attributes
|
|
129
129
|
child_hint = _get_type_hints(source).get(name)
|
|
130
130
|
# use inspect to get child spec for methods and properties
|
|
131
131
|
child_source = inspect.getattr_static(source, name, child_hint)
|
|
132
|
-
# record whether a child was found before we make modifications
|
|
133
|
-
child_found = child_source is not None
|
|
134
132
|
|
|
135
133
|
if isinstance(child_source, property):
|
|
136
134
|
child_source = _get_type_hints(child_source.fget).get("return")
|
|
@@ -151,13 +149,6 @@ class SpyCore:
|
|
|
151
149
|
|
|
152
150
|
child_source = _unwrap_optional(child_source)
|
|
153
151
|
|
|
154
|
-
if source is not None and child_found is False:
|
|
155
|
-
# stacklevel: 4 ensures warning is linked to call location
|
|
156
|
-
warnings.warn(
|
|
157
|
-
MissingSpecAttributeWarning(f"{self._name} has no attribute '{name}'"),
|
|
158
|
-
stacklevel=4,
|
|
159
|
-
)
|
|
160
|
-
|
|
161
152
|
return SpyCore(
|
|
162
153
|
source=child_source,
|
|
163
154
|
name=child_name,
|
decoy/spy_events.py
CHANGED
decoy/spy_log.py
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
"""Spy activity log."""
|
|
2
|
+
|
|
2
3
|
from typing import List, Sequence
|
|
3
4
|
|
|
4
5
|
from .errors import MissingRehearsalError
|
|
5
6
|
from .spy_events import (
|
|
6
7
|
AnySpyEvent,
|
|
8
|
+
PropAccessType,
|
|
9
|
+
PropRehearsal,
|
|
7
10
|
SpyCall,
|
|
8
11
|
SpyEvent,
|
|
9
12
|
SpyPropAccess,
|
|
10
|
-
WhenRehearsal,
|
|
11
13
|
VerifyRehearsal,
|
|
12
|
-
|
|
13
|
-
PropRehearsal,
|
|
14
|
+
WhenRehearsal,
|
|
14
15
|
)
|
|
15
16
|
|
|
16
17
|
|
|
@@ -32,10 +33,10 @@ class SpyLog:
|
|
|
32
33
|
try:
|
|
33
34
|
event = self._log[-1]
|
|
34
35
|
except IndexError as e:
|
|
35
|
-
raise MissingRehearsalError() from e
|
|
36
|
+
raise MissingRehearsalError.create() from e
|
|
36
37
|
|
|
37
38
|
if not isinstance(event, SpyEvent):
|
|
38
|
-
raise MissingRehearsalError()
|
|
39
|
+
raise MissingRehearsalError.create()
|
|
39
40
|
|
|
40
41
|
spy, payload = _apply_ignore_extra_args(event, ignore_extra_args)
|
|
41
42
|
|
|
@@ -58,12 +59,12 @@ class SpyLog:
|
|
|
58
59
|
|
|
59
60
|
while len(rehearsals) < count:
|
|
60
61
|
if index < 0:
|
|
61
|
-
raise MissingRehearsalError()
|
|
62
|
+
raise MissingRehearsalError.create()
|
|
62
63
|
|
|
63
64
|
event = self._log[index]
|
|
64
65
|
|
|
65
66
|
if not isinstance(event, (SpyEvent, PropRehearsal)):
|
|
66
|
-
raise MissingRehearsalError()
|
|
67
|
+
raise MissingRehearsalError.create()
|
|
67
68
|
|
|
68
69
|
if _is_verifiable(event):
|
|
69
70
|
rehearsal = VerifyRehearsal(
|
|
@@ -81,7 +82,7 @@ class SpyLog:
|
|
|
81
82
|
try:
|
|
82
83
|
event = self._log[-1]
|
|
83
84
|
except IndexError as e:
|
|
84
|
-
raise MissingRehearsalError() from e
|
|
85
|
+
raise MissingRehearsalError.create() from e
|
|
85
86
|
|
|
86
87
|
spy, payload = event
|
|
87
88
|
|
|
@@ -90,7 +91,7 @@ class SpyLog:
|
|
|
90
91
|
or not isinstance(payload, SpyPropAccess)
|
|
91
92
|
or payload.access_type != PropAccessType.GET
|
|
92
93
|
):
|
|
93
|
-
raise MissingRehearsalError()
|
|
94
|
+
raise MissingRehearsalError.create()
|
|
94
95
|
|
|
95
96
|
rehearsal = PropRehearsal(spy, payload)
|
|
96
97
|
self._log[-1] = rehearsal
|
decoy/stringify.py
CHANGED
decoy/stub_store.py
CHANGED
decoy/types.py
CHANGED
decoy/verifier.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"""Spy call verification."""
|
|
2
|
+
|
|
2
3
|
from typing import Optional, Sequence
|
|
3
4
|
|
|
4
|
-
from .spy_events import SpyEvent, VerifyRehearsal, match_event
|
|
5
5
|
from .errors import VerifyError
|
|
6
|
+
from .spy_events import SpyEvent, VerifyRehearsal, match_event
|
|
6
7
|
|
|
7
8
|
# ensure decoy.verifier does not pollute Pytest tracebacks
|
|
8
9
|
__tracebackhide__ = True
|
|
@@ -41,7 +42,7 @@ class Verifier:
|
|
|
41
42
|
calls_verified = match_count != 0 if times is None else match_count == times
|
|
42
43
|
|
|
43
44
|
if not calls_verified:
|
|
44
|
-
raise VerifyError(
|
|
45
|
+
raise VerifyError.create(
|
|
45
46
|
rehearsals=rehearsals,
|
|
46
47
|
calls=calls,
|
|
47
48
|
times=times,
|
decoy/warning_checker.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Warning checker."""
|
|
2
|
+
|
|
2
3
|
from collections import defaultdict
|
|
3
4
|
from itertools import groupby
|
|
4
5
|
from typing import Dict, List, NamedTuple, Sequence
|
|
@@ -8,9 +9,9 @@ from .spy_events import (
|
|
|
8
9
|
AnySpyEvent,
|
|
9
10
|
SpyCall,
|
|
10
11
|
SpyEvent,
|
|
12
|
+
SpyRehearsal,
|
|
11
13
|
VerifyRehearsal,
|
|
12
14
|
WhenRehearsal,
|
|
13
|
-
SpyRehearsal,
|
|
14
15
|
match_event,
|
|
15
16
|
)
|
|
16
17
|
from .warnings import DecoyWarning, MiscalledStubWarning, RedundantVerifyWarning
|
|
@@ -77,7 +78,7 @@ def _check_no_miscalled_stubs(all_events: Sequence[AnySpyEvent]) -> None:
|
|
|
77
78
|
|
|
78
79
|
if is_stubbed and all(len(c.matching_rehearsals) == 0 for c in calls):
|
|
79
80
|
_warn(
|
|
80
|
-
MiscalledStubWarning(
|
|
81
|
+
MiscalledStubWarning.create(
|
|
81
82
|
calls=[c.event for c in calls],
|
|
82
83
|
rehearsals=rehearsals,
|
|
83
84
|
)
|
|
@@ -89,8 +90,8 @@ def _check_no_redundant_verify(all_calls: Sequence[AnySpyEvent]) -> None:
|
|
|
89
90
|
verify_rehearsals = [c for c in all_calls if isinstance(c, VerifyRehearsal)]
|
|
90
91
|
|
|
91
92
|
for vr in verify_rehearsals:
|
|
92
|
-
if any(wr for wr in when_rehearsals if wr == vr):
|
|
93
|
-
_warn(RedundantVerifyWarning(rehearsal=vr))
|
|
93
|
+
if any(wr for wr in when_rehearsals if wr == vr): # type: ignore[comparison-overlap]
|
|
94
|
+
_warn(RedundantVerifyWarning.create(rehearsal=vr))
|
|
94
95
|
|
|
95
96
|
|
|
96
97
|
def _warn(warning: DecoyWarning) -> None:
|
decoy/warnings.py
CHANGED
|
@@ -4,11 +4,12 @@ See the [warnings guide][] for more details.
|
|
|
4
4
|
|
|
5
5
|
[warnings guide]: usage/errors-and-warnings.md#warnings
|
|
6
6
|
"""
|
|
7
|
+
|
|
7
8
|
import os
|
|
8
9
|
from typing import Sequence
|
|
9
10
|
|
|
10
11
|
from .spy_events import SpyEvent, SpyRehearsal, VerifyRehearsal
|
|
11
|
-
from .stringify import stringify_call, stringify_error_message
|
|
12
|
+
from .stringify import count, stringify_call, stringify_error_message
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
class DecoyWarning(UserWarning):
|
|
@@ -30,18 +31,20 @@ class MiscalledStubWarning(DecoyWarning):
|
|
|
30
31
|
[MiscalledStubWarning guide]: usage/errors-and-warnings.md#miscalledstubwarning
|
|
31
32
|
|
|
32
33
|
Attributes:
|
|
33
|
-
rehearsals: The
|
|
34
|
+
rehearsals: The mock's configured rehearsals.
|
|
34
35
|
calls: Actual calls to the mock.
|
|
35
36
|
"""
|
|
36
37
|
|
|
37
38
|
rehearsals: Sequence[SpyRehearsal]
|
|
38
39
|
calls: Sequence[SpyEvent]
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
@classmethod
|
|
42
|
+
def create(
|
|
43
|
+
cls,
|
|
42
44
|
rehearsals: Sequence[SpyRehearsal],
|
|
43
45
|
calls: Sequence[SpyEvent],
|
|
44
|
-
) ->
|
|
46
|
+
) -> "MiscalledStubWarning":
|
|
47
|
+
"""Create a MiscalledStubWarning."""
|
|
45
48
|
heading = os.linesep.join(
|
|
46
49
|
[
|
|
47
50
|
"Stub was called but no matching rehearsal found.",
|
|
@@ -55,9 +58,11 @@ class MiscalledStubWarning(DecoyWarning):
|
|
|
55
58
|
calls=calls,
|
|
56
59
|
)
|
|
57
60
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
result = cls(message)
|
|
62
|
+
result.rehearsals = rehearsals
|
|
63
|
+
result.calls = calls
|
|
64
|
+
|
|
65
|
+
return result
|
|
61
66
|
|
|
62
67
|
|
|
63
68
|
class RedundantVerifyWarning(DecoyWarning):
|
|
@@ -73,7 +78,11 @@ class RedundantVerifyWarning(DecoyWarning):
|
|
|
73
78
|
[RedundantVerifyWarning guide]: usage/errors-and-warnings.md#redundantverifywarning
|
|
74
79
|
"""
|
|
75
80
|
|
|
76
|
-
|
|
81
|
+
rehearsal: VerifyRehearsal
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def create(cls, rehearsal: VerifyRehearsal) -> "RedundantVerifyWarning":
|
|
85
|
+
"""Create a RedundantVerifyWarning."""
|
|
77
86
|
message = os.linesep.join(
|
|
78
87
|
[
|
|
79
88
|
"The same rehearsal was used in both a `when` and a `verify`.",
|
|
@@ -82,8 +91,11 @@ class RedundantVerifyWarning(DecoyWarning):
|
|
|
82
91
|
"See https://michael.cousins.io/decoy/usage/errors-and-warnings/#redundantverifywarning",
|
|
83
92
|
]
|
|
84
93
|
)
|
|
85
|
-
|
|
86
|
-
|
|
94
|
+
|
|
95
|
+
result = cls(message)
|
|
96
|
+
result.rehearsal = rehearsal
|
|
97
|
+
|
|
98
|
+
return result
|
|
87
99
|
|
|
88
100
|
|
|
89
101
|
class IncorrectCallWarning(DecoyWarning):
|
|
@@ -95,13 +107,3 @@ class IncorrectCallWarning(DecoyWarning):
|
|
|
95
107
|
|
|
96
108
|
[IncorrectCallWarning guide]: usage/errors-and-warnings.md#incorrectcallwarning
|
|
97
109
|
"""
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
class MissingSpecAttributeWarning(DecoyWarning):
|
|
101
|
-
"""A warning raised if a Decoy mock with a spec is used with a missing attribute.
|
|
102
|
-
|
|
103
|
-
This will become an error in the next major version of Decoy.
|
|
104
|
-
See the [MissingSpecAttributeWarning guide][] for more details.
|
|
105
|
-
|
|
106
|
-
[MissingSpecAttributeWarning guide]: usage/errors-and-warnings.md#missingspecattributewarning
|
|
107
|
-
"""
|
|
@@ -1,29 +1,22 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: decoy
|
|
3
|
-
Version: 2.1
|
|
3
|
+
Version: 2.2.1
|
|
4
4
|
Summary: Opinionated mocking library for Python
|
|
5
|
-
License: MIT
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
6
7
|
Author: Michael Cousins
|
|
7
|
-
Author-email: michael@cousins.io
|
|
8
|
-
Requires-Python: >=3.7
|
|
8
|
+
Author-email: michael@cousins.io>
|
|
9
|
+
Requires-Python: >=3.7
|
|
9
10
|
Classifier: Development Status :: 5 - Production/Stable
|
|
10
11
|
Classifier: Intended Audience :: Developers
|
|
11
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
12
12
|
Classifier: Operating System :: OS Independent
|
|
13
|
-
Classifier: Programming Language :: Python :: 3
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.7
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
21
13
|
Classifier: Topic :: Software Development :: Testing
|
|
22
14
|
Classifier: Topic :: Software Development :: Testing :: Mocking
|
|
23
15
|
Classifier: Typing :: Typed
|
|
24
16
|
Project-URL: Changelog, https://github.com/mcous/decoy/releases
|
|
25
17
|
Project-URL: Documentation, https://michael.cousins.io/decoy/
|
|
26
18
|
Project-URL: Homepage, https://michael.cousins.io/decoy/
|
|
19
|
+
Project-URL: Issues, https://github.com/mcous/decoy/issues
|
|
27
20
|
Project-URL: Repository, https://github.com/mcous/decoy
|
|
28
21
|
Description-Content-Type: text/markdown
|
|
29
22
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
decoy/__init__.py,sha256=v3LUyAxuxFrO2M4Iw0VRX0IM6axFMxPmtgIbBemmwj8,11232
|
|
2
|
+
decoy/call_handler.py,sha256=fuhWr9VAnh0FUvbfWw11fpdQ_GCUE3Hah-EXvdk3gGw,1512
|
|
3
|
+
decoy/context_managers.py,sha256=oYsH99ZPmkaCcqjCrOWInueO5ympHm_ICWmbpFoQzqA,1287
|
|
4
|
+
decoy/core.py,sha256=0YsQFnL4Kw_oDX7jRgOVLw9BGsnNdekymQCd1aq-4sQ,5830
|
|
5
|
+
decoy/errors.py,sha256=mmUAOoQQyXhCLjMcf_3bHMz3PjU7bingIU6sC3hk2B4,3119
|
|
6
|
+
decoy/matchers.py,sha256=_hIyrBZfwRqhQKpiBczN7OVIBw1n3IRfZRv2lhQqgzM,11299
|
|
7
|
+
decoy/mypy/__init__.py,sha256=GdJCf-gXpYB4SBr-Rm8zCLZQxAUuLjPyUieIWgLmdwA,86
|
|
8
|
+
decoy/mypy/plugin.py,sha256=nCfo4XpDxBRDsInDsvCe6h8t7-1OV0ZCJzIJqTsAU0I,1359
|
|
9
|
+
decoy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
decoy/pytest_plugin.py,sha256=muiWN6zwQPD-AaL4YH6usp-25P4EYjrROTx6wdLE9hM,853
|
|
11
|
+
decoy/spy.py,sha256=cxlwDFxd2dcTb6vFJJGc0B3IuFCPJs2cgVMeB0qNXW4,7581
|
|
12
|
+
decoy/spy_core.py,sha256=b3PTgCkYnijT08ak_iRUKnA95FnmspSRmT8cs_YtjtM,7965
|
|
13
|
+
decoy/spy_events.py,sha256=PFTfkgN7CVWfbVoSSUMSaCSxfRrUMd7U419iwQ1Xofg,2383
|
|
14
|
+
decoy/spy_log.py,sha256=5JcjcH7zHrn12jp7xyrs_UWY7ATcWJkQ4v9oCR_Cclg,3929
|
|
15
|
+
decoy/stringify.py,sha256=6JA2VA9eK-4AtwSSZPnMYT8ucW7EMpGdbTXokbOpgOg,2240
|
|
16
|
+
decoy/stub_store.py,sha256=tVqKOW65NjotVdDhDDXpLfwagKHypMS4WMob71RYk1c,1744
|
|
17
|
+
decoy/types.py,sha256=LXzky6T0RXvS53NoPpVgCi3KVV6KCyB6EvnBl0NaKug,404
|
|
18
|
+
decoy/verifier.py,sha256=3ZVqaUJPjaNM9Nue3FK6Zzj15qWskX2CEQXoe_yjUEs,1586
|
|
19
|
+
decoy/warning_checker.py,sha256=OOkcrr_I98Pzs3o9UUNgaihJDHwNiu1LlcEK9hi5ZFs,3577
|
|
20
|
+
decoy/warnings.py,sha256=DLdX1ABPk6QvhJWt5s98uR7rCQFQdJJjSPPXRSX0nXA,3203
|
|
21
|
+
decoy-2.2.1.dist-info/METADATA,sha256=NSyb-y2FGxtmuCo_rCHkHbxosqfmC6yzh5x9hUMWdkU,6855
|
|
22
|
+
decoy-2.2.1.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
23
|
+
decoy-2.2.1.dist-info/entry_points.txt,sha256=P2wF8zdthEM-3Yo32kxHDhZDjbW6AE489HPWqnvPLOU,38
|
|
24
|
+
decoy-2.2.1.dist-info/licenses/LICENSE,sha256=Rxi19kHgqakAsJNG1jMuORmgKx9bI8Pcu_gtzFkhflQ,1078
|
|
25
|
+
decoy-2.2.1.dist-info/RECORD,,
|
decoy-2.1.2.dist-info/RECORD
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
decoy/__init__.py,sha256=vwNdsTHo61z4UQH3U4hiy6ECb9mKK9pUfNGEzxnAwn8,11280
|
|
2
|
-
decoy/call_handler.py,sha256=vbqY_N6UWIRdwoOljEzl7y5iTDiFC-zxHfzl8ykuhxU,1511
|
|
3
|
-
decoy/context_managers.py,sha256=-Zmqtd_kZESZpLrBSxgOSofH6scgsOwcqPATFb49LWM,1286
|
|
4
|
-
decoy/core.py,sha256=4zQTVa3f8UNqwb6VQOGR1IDOSdvNOWIKuemmqPb5P1M,5829
|
|
5
|
-
decoy/errors.py,sha256=F-8T_e_xDVcuVaGYJG1Bdc0RRVGQFyBtH-A4eMW_AYQ,2888
|
|
6
|
-
decoy/matchers.py,sha256=zTYmT7gPuMa2UZDeMdaukmanWRDuwWDm94_5Mq9txFg,9963
|
|
7
|
-
decoy/mypy/__init__.py,sha256=_Va3O7r0QoniZAD3IaRWo2DHt1OF0o5vgR9gmr48U3s,85
|
|
8
|
-
decoy/mypy/plugin.py,sha256=LLJCfYQdEEayym0S6Re3BZG4gdi3drmyoIjiFXasfw0,1358
|
|
9
|
-
decoy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
-
decoy/pytest_plugin.py,sha256=bPJ9v8cwEFxtPQRWeTuP97WsWsaWHaXPPVKxiFf3VyE,852
|
|
11
|
-
decoy/spy.py,sha256=kpPXruiWcGwu8jOlIU4HWjaJUiSEzZ_AFc19eFevOhc,7596
|
|
12
|
-
decoy/spy_core.py,sha256=QrmS7q8ABo-pXabk55IXVot3TN_fMhaG5CUGNuk2mz8,8435
|
|
13
|
-
decoy/spy_events.py,sha256=paDLR_Pdn9KDemSOFRvHcDSidt-yzjX7sidXE8S67z8,2382
|
|
14
|
-
decoy/spy_log.py,sha256=cgl4inDJ-bpAiwuGbkWKWRaOGlGPTN8DKv1LiDJVbd4,3886
|
|
15
|
-
decoy/stringify.py,sha256=-xlYGfYfjHJuAATmH9dQk7kIQbiBknzZ07GOzO4p7yU,2239
|
|
16
|
-
decoy/stub_store.py,sha256=lLJGqv1CIxZlJFHhnXuoQdbp_q524eT-e8QHEDPSAew,1743
|
|
17
|
-
decoy/types.py,sha256=y2DYDekkGzjKTshW0VN3Yw6pifb5T9AyqdrpwohKV3A,403
|
|
18
|
-
decoy/verifier.py,sha256=FK_HmJnNSqwkkCAlkeZVtb2sK0sUZEwhnlP42dgXN4Y,1578
|
|
19
|
-
decoy/warning_checker.py,sha256=NVA4KwPeM7vneTku60XiDGjD9tG_ywkz6R7WvmTAlQA,3526
|
|
20
|
-
decoy/warnings.py,sha256=kPRR0ujdNGS_W_ciNvR5dGWyQuuWbooZduPW6OB32t8,3347
|
|
21
|
-
decoy-2.1.2.dist-info/LICENSE,sha256=Rxi19kHgqakAsJNG1jMuORmgKx9bI8Pcu_gtzFkhflQ,1078
|
|
22
|
-
decoy-2.1.2.dist-info/METADATA,sha256=5qGNUFcf_B1Blly9uBnBQV_S3nemThdQobxh6dZudv0,7220
|
|
23
|
-
decoy-2.1.2.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
|
|
24
|
-
decoy-2.1.2.dist-info/entry_points.txt,sha256=P2wF8zdthEM-3Yo32kxHDhZDjbW6AE489HPWqnvPLOU,38
|
|
25
|
-
decoy-2.1.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|