decoy 2.1.1__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.1 → decoy-2.2.0}/PKG-INFO +4 -3
- {decoy-2.1.1 → decoy-2.2.0}/decoy/__init__.py +9 -15
- {decoy-2.1.1 → decoy-2.2.0}/decoy/call_handler.py +1 -0
- {decoy-2.1.1 → decoy-2.2.0}/decoy/context_managers.py +2 -1
- {decoy-2.1.1 → decoy-2.2.0}/decoy/core.py +1 -0
- {decoy-2.1.1 → decoy-2.2.0}/decoy/errors.py +1 -0
- {decoy-2.1.1 → decoy-2.2.0}/decoy/matchers.py +60 -3
- {decoy-2.1.1 → decoy-2.2.0}/decoy/mypy/__init__.py +1 -0
- {decoy-2.1.1 → decoy-2.2.0}/decoy/mypy/plugin.py +1 -0
- {decoy-2.1.1 → decoy-2.2.0}/decoy/pytest_plugin.py +1 -0
- {decoy-2.1.1 → decoy-2.2.0}/decoy/spy.py +3 -4
- {decoy-2.1.1 → decoy-2.2.0}/decoy/spy_core.py +35 -8
- {decoy-2.1.1 → decoy-2.2.0}/decoy/spy_events.py +1 -0
- {decoy-2.1.1 → decoy-2.2.0}/decoy/spy_log.py +1 -0
- {decoy-2.1.1 → decoy-2.2.0}/decoy/stringify.py +1 -0
- {decoy-2.1.1 → decoy-2.2.0}/decoy/stub_store.py +1 -0
- {decoy-2.1.1 → decoy-2.2.0}/decoy/types.py +1 -0
- {decoy-2.1.1 → decoy-2.2.0}/decoy/verifier.py +1 -0
- {decoy-2.1.1 → decoy-2.2.0}/decoy/warning_checker.py +1 -0
- {decoy-2.1.1 → decoy-2.2.0}/decoy/warnings.py +1 -10
- {decoy-2.1.1 → decoy-2.2.0}/pyproject.toml +7 -5
- {decoy-2.1.1 → decoy-2.2.0}/LICENSE +0 -0
- {decoy-2.1.1 → decoy-2.2.0}/README.md +0 -0
- {decoy-2.1.1 → decoy-2.2.0}/decoy/py.typed +0 -0
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: decoy
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Opinionated mocking library for Python
|
|
5
|
-
Home-page: https://michael.cousins.io/decoy/
|
|
6
5
|
License: MIT
|
|
7
6
|
Author: Michael Cousins
|
|
8
7
|
Author-email: michael@cousins.io
|
|
@@ -18,11 +17,13 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.10
|
|
19
18
|
Classifier: Programming Language :: Python :: 3.11
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
21
|
Classifier: Topic :: Software Development :: Testing
|
|
22
22
|
Classifier: Topic :: Software Development :: Testing :: Mocking
|
|
23
23
|
Classifier: Typing :: Typed
|
|
24
24
|
Project-URL: Changelog, https://github.com/mcous/decoy/releases
|
|
25
25
|
Project-URL: Documentation, https://michael.cousins.io/decoy/
|
|
26
|
+
Project-URL: Homepage, https://michael.cousins.io/decoy/
|
|
26
27
|
Project-URL: Repository, https://github.com/mcous/decoy
|
|
27
28
|
Description-Content-Type: text/markdown
|
|
28
29
|
|
|
@@ -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,11 +1,22 @@
|
|
|
1
1
|
"""Core spy logic."""
|
|
2
|
+
|
|
2
3
|
import inspect
|
|
3
4
|
import functools
|
|
4
5
|
import warnings
|
|
5
|
-
from typing import
|
|
6
|
+
from typing import (
|
|
7
|
+
Any,
|
|
8
|
+
Dict,
|
|
9
|
+
NamedTuple,
|
|
10
|
+
Optional,
|
|
11
|
+
Tuple,
|
|
12
|
+
Type,
|
|
13
|
+
Union,
|
|
14
|
+
Sequence,
|
|
15
|
+
get_type_hints,
|
|
16
|
+
)
|
|
6
17
|
|
|
7
18
|
from .spy_events import SpyInfo
|
|
8
|
-
from .warnings import IncorrectCallWarning
|
|
19
|
+
from .warnings import IncorrectCallWarning
|
|
9
20
|
|
|
10
21
|
|
|
11
22
|
class _FROM_SOURCE:
|
|
@@ -136,12 +147,7 @@ class SpyCore:
|
|
|
136
147
|
# signature reporting by wrapping it in a partial
|
|
137
148
|
child_source = functools.partial(child_source, None)
|
|
138
149
|
|
|
139
|
-
|
|
140
|
-
# stacklevel: 4 ensures warning is linked to call location
|
|
141
|
-
warnings.warn(
|
|
142
|
-
MissingSpecAttributeWarning(f"{self._name} has no attribute '{name}'"),
|
|
143
|
-
stacklevel=4,
|
|
144
|
-
)
|
|
150
|
+
child_source = _unwrap_optional(child_source)
|
|
145
151
|
|
|
146
152
|
return SpyCore(
|
|
147
153
|
source=child_source,
|
|
@@ -215,3 +221,24 @@ def _get_type_hints(obj: Any) -> Dict[str, Any]:
|
|
|
215
221
|
return get_type_hints(obj)
|
|
216
222
|
except Exception:
|
|
217
223
|
return {}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _unwrap_optional(source: Any) -> Any:
|
|
227
|
+
"""Return the source's base type if it's a optional.
|
|
228
|
+
|
|
229
|
+
If the type is a union of more than just T | None,
|
|
230
|
+
bail out and return None to avoid potentially false warnings.
|
|
231
|
+
"""
|
|
232
|
+
origin = getattr(source, "__origin__", None)
|
|
233
|
+
args: Sequence[Any] = getattr(source, "__args__", ())
|
|
234
|
+
|
|
235
|
+
# TODO(mc, 2025-03-19): support larger unions? might be a lot of work for little payoff
|
|
236
|
+
if origin is Union:
|
|
237
|
+
if len(args) == 2 and args[0] is type(None):
|
|
238
|
+
return args[1]
|
|
239
|
+
if len(args) == 2 and args[1] is type(None):
|
|
240
|
+
return args[0]
|
|
241
|
+
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
return source
|
|
@@ -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"
|
|
@@ -36,8 +36,8 @@ mypy = [
|
|
|
36
36
|
pytest = "7.4.0"
|
|
37
37
|
pytest-asyncio = "0.21.1"
|
|
38
38
|
pytest-mypy-plugins = "2.0.0"
|
|
39
|
-
pytest-xdist = "3.
|
|
40
|
-
ruff = "0.
|
|
39
|
+
pytest-xdist = "3.5.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
|