decoy 2.1.1__py3-none-any.whl → 2.1.2__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/spy_core.py CHANGED
@@ -2,7 +2,17 @@
2
2
  import inspect
3
3
  import functools
4
4
  import warnings
5
- from typing import Any, Dict, NamedTuple, Optional, Tuple, Type, Union, get_type_hints
5
+ from typing import (
6
+ Any,
7
+ Dict,
8
+ NamedTuple,
9
+ Optional,
10
+ Tuple,
11
+ Type,
12
+ Union,
13
+ Sequence,
14
+ get_type_hints,
15
+ )
6
16
 
7
17
  from .spy_events import SpyInfo
8
18
  from .warnings import IncorrectCallWarning, MissingSpecAttributeWarning
@@ -112,12 +122,15 @@ class SpyCore:
112
122
  source = self._source
113
123
  child_name = f"{self._name}.{name}"
114
124
  child_source = None
125
+ child_found = False
115
126
 
116
127
  if inspect.isclass(source):
117
128
  # use type hints to get child spec for class attributes
118
129
  child_hint = _get_type_hints(source).get(name)
119
130
  # use inspect to get child spec for methods and properties
120
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
121
134
 
122
135
  if isinstance(child_source, property):
123
136
  child_source = _get_type_hints(child_source.fget).get("return")
@@ -136,7 +149,9 @@ class SpyCore:
136
149
  # signature reporting by wrapping it in a partial
137
150
  child_source = functools.partial(child_source, None)
138
151
 
139
- if child_source is None and source is not None:
152
+ child_source = _unwrap_optional(child_source)
153
+
154
+ if source is not None and child_found is False:
140
155
  # stacklevel: 4 ensures warning is linked to call location
141
156
  warnings.warn(
142
157
  MissingSpecAttributeWarning(f"{self._name} has no attribute '{name}'"),
@@ -215,3 +230,24 @@ def _get_type_hints(obj: Any) -> Dict[str, Any]:
215
230
  return get_type_hints(obj)
216
231
  except Exception:
217
232
  return {}
233
+
234
+
235
+ def _unwrap_optional(source: Any) -> Any:
236
+ """Return the source's base type if it's a optional.
237
+
238
+ If the type is a union of more than just T | None,
239
+ bail out and return None to avoid potentially false warnings.
240
+ """
241
+ origin = getattr(source, "__origin__", None)
242
+ args: Sequence[Any] = getattr(source, "__args__", ())
243
+
244
+ # TODO(mc, 2025-03-19): support larger unions? might be a lot of work for little payoff
245
+ if origin is Union:
246
+ if len(args) == 2 and args[0] is type(None):
247
+ return args[1]
248
+ if len(args) == 2 and args[1] is type(None):
249
+ return args[0]
250
+
251
+ return None
252
+
253
+ return source
@@ -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.1.2
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
 
@@ -9,7 +9,7 @@ decoy/mypy/plugin.py,sha256=LLJCfYQdEEayym0S6Re3BZG4gdi3drmyoIjiFXasfw0,1358
9
9
  decoy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  decoy/pytest_plugin.py,sha256=bPJ9v8cwEFxtPQRWeTuP97WsWsaWHaXPPVKxiFf3VyE,852
11
11
  decoy/spy.py,sha256=kpPXruiWcGwu8jOlIU4HWjaJUiSEzZ_AFc19eFevOhc,7596
12
- decoy/spy_core.py,sha256=71O8DajiD1W1sIlyxQiG0D3SwSDShpMMgh5sZ4M6QS8,7519
12
+ decoy/spy_core.py,sha256=QrmS7q8ABo-pXabk55IXVot3TN_fMhaG5CUGNuk2mz8,8435
13
13
  decoy/spy_events.py,sha256=paDLR_Pdn9KDemSOFRvHcDSidt-yzjX7sidXE8S67z8,2382
14
14
  decoy/spy_log.py,sha256=cgl4inDJ-bpAiwuGbkWKWRaOGlGPTN8DKv1LiDJVbd4,3886
15
15
  decoy/stringify.py,sha256=-xlYGfYfjHJuAATmH9dQk7kIQbiBknzZ07GOzO4p7yU,2239
@@ -18,8 +18,8 @@ decoy/types.py,sha256=y2DYDekkGzjKTshW0VN3Yw6pifb5T9AyqdrpwohKV3A,403
18
18
  decoy/verifier.py,sha256=FK_HmJnNSqwkkCAlkeZVtb2sK0sUZEwhnlP42dgXN4Y,1578
19
19
  decoy/warning_checker.py,sha256=NVA4KwPeM7vneTku60XiDGjD9tG_ywkz6R7WvmTAlQA,3526
20
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,,
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,,
@@ -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
File without changes