decoy 2.1.2__tar.gz → 2.2.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.1.2 → decoy-2.2.0}/PKG-INFO +1 -1
- {decoy-2.1.2 → decoy-2.2.0}/decoy/__init__.py +9 -15
- {decoy-2.1.2 → decoy-2.2.0}/decoy/call_handler.py +1 -0
- {decoy-2.1.2 → decoy-2.2.0}/decoy/context_managers.py +2 -1
- {decoy-2.1.2 → decoy-2.2.0}/decoy/core.py +1 -0
- {decoy-2.1.2 → decoy-2.2.0}/decoy/errors.py +1 -0
- {decoy-2.1.2 → decoy-2.2.0}/decoy/matchers.py +60 -3
- {decoy-2.1.2 → decoy-2.2.0}/decoy/mypy/__init__.py +1 -0
- {decoy-2.1.2 → decoy-2.2.0}/decoy/mypy/plugin.py +1 -0
- {decoy-2.1.2 → decoy-2.2.0}/decoy/pytest_plugin.py +1 -0
- {decoy-2.1.2 → decoy-2.2.0}/decoy/spy.py +3 -4
- {decoy-2.1.2 → decoy-2.2.0}/decoy/spy_core.py +2 -11
- {decoy-2.1.2 → decoy-2.2.0}/decoy/spy_events.py +1 -0
- {decoy-2.1.2 → decoy-2.2.0}/decoy/spy_log.py +1 -0
- {decoy-2.1.2 → decoy-2.2.0}/decoy/stringify.py +1 -0
- {decoy-2.1.2 → decoy-2.2.0}/decoy/stub_store.py +1 -0
- {decoy-2.1.2 → decoy-2.2.0}/decoy/types.py +1 -0
- {decoy-2.1.2 → decoy-2.2.0}/decoy/verifier.py +1 -0
- {decoy-2.1.2 → decoy-2.2.0}/decoy/warning_checker.py +1 -0
- {decoy-2.1.2 → decoy-2.2.0}/decoy/warnings.py +1 -10
- {decoy-2.1.2 → decoy-2.2.0}/pyproject.toml +6 -4
- {decoy-2.1.2 → decoy-2.2.0}/LICENSE +0 -0
- {decoy-2.1.2 → decoy-2.2.0}/README.md +0 -0
- {decoy-2.1.2 → decoy-2.2.0}/decoy/py.typed +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
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
|
|
@@ -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,
|
|
@@ -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"]
|
|
@@ -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
|
]
|
|
@@ -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
|
|
@@ -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,
|
|
@@ -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,
|
|
@@ -4,6 +4,7 @@ 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
|
|
|
@@ -95,13 +96,3 @@ class IncorrectCallWarning(DecoyWarning):
|
|
|
95
96
|
|
|
96
97
|
[IncorrectCallWarning guide]: usage/errors-and-warnings.md#incorrectcallwarning
|
|
97
98
|
"""
|
|
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,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "decoy"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.2.0"
|
|
4
4
|
description = "Opinionated mocking library for Python"
|
|
5
5
|
authors = ["Michael Cousins <michael@cousins.io>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -37,7 +37,7 @@ pytest = "7.4.0"
|
|
|
37
37
|
pytest-asyncio = "0.21.1"
|
|
38
38
|
pytest-mypy-plugins = "2.0.0"
|
|
39
39
|
pytest-xdist = "3.5.0"
|
|
40
|
-
ruff = "0.
|
|
40
|
+
ruff = "0.11.0"
|
|
41
41
|
|
|
42
42
|
[tool.poetry.group.docs.dependencies]
|
|
43
43
|
mkdocs = { version = "1.5.3", python = ">=3.8" }
|
|
@@ -88,10 +88,12 @@ exclude_lines = ["@overload", "if TYPE_CHECKING:"]
|
|
|
88
88
|
[tool.ruff]
|
|
89
89
|
target-version = "py37"
|
|
90
90
|
extend-exclude = [".cache"]
|
|
91
|
+
|
|
92
|
+
[tool.ruff.lint]
|
|
91
93
|
select = ["ANN", "B", "D", "E", "F", "RUF", "W"]
|
|
92
|
-
ignore = ["
|
|
94
|
+
ignore = ["ANN401", "D107", "E501"]
|
|
93
95
|
|
|
94
|
-
[tool.ruff.pydocstyle]
|
|
96
|
+
[tool.ruff.lint.pydocstyle]
|
|
95
97
|
convention = "google"
|
|
96
98
|
|
|
97
99
|
[build-system]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|