decoy 2.1.1__py3-none-any.whl → 2.2.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/__init__.py CHANGED
@@ -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", "Stub", "Prop", "matchers", "warnings", "errors"]
344
+ __all__ = ["Decoy", "Prop", "Stub", "errors", "matchers", "warnings"]
decoy/call_handler.py CHANGED
@@ -1,4 +1,5 @@
1
1
  """Spy call handling."""
2
+
2
3
  from typing import Any, NamedTuple, Optional
3
4
 
4
5
  from .spy_log import SpyLog
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
@@ -1,4 +1,5 @@
1
1
  """Decoy implementation logic."""
2
+
2
3
  import inspect
3
4
  from typing import Any, Callable, Optional
4
5
 
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
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) == self._error_type
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
@@ -1,4 +1,5 @@
1
1
  """Decoy mypy plugin entrypoint."""
2
+
2
3
  from .plugin import plugin
3
4
 
4
5
  __all__ = ["plugin"]
decoy/mypy/plugin.py CHANGED
@@ -1,4 +1,5 @@
1
1
  """Decoy mypy plugin."""
2
+
2
3
  from typing import Callable, Optional, Type as ClassType
3
4
 
4
5
  from mypy.errorcodes import FUNC_RETURNS_VALUE
decoy/pytest_plugin.py CHANGED
@@ -4,6 +4,7 @@ The plugin will be registered with pytest when you install Decoy. It adds a
4
4
  fixture without modifying any other pytest behavior. Its usage is optional
5
5
  but highly recommended.
6
6
  """
7
+
7
8
  from typing import Iterable
8
9
 
9
10
  import pytest
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,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 Any, Dict, NamedTuple, Optional, Tuple, Type, Union, get_type_hints
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, MissingSpecAttributeWarning
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
- if child_source is None and source is not None:
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
decoy/spy_events.py CHANGED
@@ -1,4 +1,5 @@
1
1
  """Spy interaction event value objects."""
2
+
2
3
  import enum
3
4
  from typing import Any, Dict, NamedTuple, Optional, Tuple, Union
4
5
 
decoy/spy_log.py CHANGED
@@ -1,4 +1,5 @@
1
1
  """Spy activity log."""
2
+
2
3
  from typing import List, Sequence
3
4
 
4
5
  from .errors import MissingRehearsalError
decoy/stringify.py CHANGED
@@ -1,4 +1,5 @@
1
1
  """Message string generation."""
2
+
2
3
  import os
3
4
  from typing import Sequence
4
5
 
decoy/stub_store.py CHANGED
@@ -1,4 +1,5 @@
1
1
  """Stub creation and storage."""
2
+
2
3
  from typing import Any, Callable, List, NamedTuple, Optional, Union
3
4
 
4
5
  from .spy_events import SpyEvent, WhenRehearsal, match_event
decoy/types.py CHANGED
@@ -1,4 +1,5 @@
1
1
  """Common type definitions."""
2
+
2
3
  from typing import Any, Callable, TypeVar
3
4
 
4
5
  FuncT = TypeVar("FuncT", bound=Callable[..., Any])
decoy/verifier.py CHANGED
@@ -1,4 +1,5 @@
1
1
  """Spy call verification."""
2
+
2
3
  from typing import Optional, Sequence
3
4
 
4
5
  from .spy_events import SpyEvent, VerifyRehearsal, match_event
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
decoy/warnings.py CHANGED
@@ -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,8 +1,7 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: decoy
3
- Version: 2.1.1
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
 
@@ -0,0 +1,25 @@
1
+ decoy/__init__.py,sha256=wGgiPaajcP3DgnFQZi_6aRiCvrx-rZNNH7hgcu6Zx0Q,11225
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=SLKXIfklhrfekV-NytDBSqP_FgNIFuiw0EQ3x_GbOv4,2889
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=qU-azmm5YmX5mGd4YcAeFAuIS75PESqPNnOUnuq1GoE,3887
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=fKOTBUHv05FXzwcJuCQIkJTlcELmDp5caJpG2R9Kqo0,1579
19
+ decoy/warning_checker.py,sha256=eaawnwDdrUUFG6u6LSRi3suuKS5wirtZ_lMctWGldCE,3527
20
+ decoy/warnings.py,sha256=sHhuh9wWys8AwcOTag0js8JAzR_BP_a0jATrXAd2M4Y,2969
21
+ decoy-2.2.0.dist-info/LICENSE,sha256=Rxi19kHgqakAsJNG1jMuORmgKx9bI8Pcu_gtzFkhflQ,1078
22
+ decoy-2.2.0.dist-info/METADATA,sha256=L5mBN4FJ8dFSn6dT76t698lIMcNBxY1LyHiHiLid5Kg,7220
23
+ decoy-2.2.0.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
24
+ decoy-2.2.0.dist-info/entry_points.txt,sha256=P2wF8zdthEM-3Yo32kxHDhZDjbW6AE489HPWqnvPLOU,38
25
+ decoy-2.2.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.8.1
2
+ Generator: poetry-core 2.1.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -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=71O8DajiD1W1sIlyxQiG0D3SwSDShpMMgh5sZ4M6QS8,7519
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.1.dist-info/LICENSE,sha256=Rxi19kHgqakAsJNG1jMuORmgKx9bI8Pcu_gtzFkhflQ,1078
22
- decoy-2.1.1.dist-info/METADATA,sha256=Sp1N8SINWUxIVYtSbczbrZIsPRksBjTjhqsHWSX33Y4,7157
23
- decoy-2.1.1.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
24
- decoy-2.1.1.dist-info/entry_points.txt,sha256=P2wF8zdthEM-3Yo32kxHDhZDjbW6AE489HPWqnvPLOU,38
25
- decoy-2.1.1.dist-info/RECORD,,
File without changes