decoy 2.1.0__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
decoy/warnings.py CHANGED
@@ -79,7 +79,7 @@ class RedundantVerifyWarning(DecoyWarning):
79
79
  "The same rehearsal was used in both a `when` and a `verify`.",
80
80
  "This is redundant and probably a misuse of the mock.",
81
81
  f"\t{stringify_call(rehearsal)}",
82
- "See https://mike.cousins.io/decoy/usage/errors-and-warnings/#redundantverifywarning",
82
+ "See https://michael.cousins.io/decoy/usage/errors-and-warnings/#redundantverifywarning",
83
83
  ]
84
84
  )
85
85
  super().__init__(message)
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2020-2023, Mike Cousins
3
+ Copyright (c) 2020-2023, Michael Cousins
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,11 +1,10 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: decoy
3
- Version: 2.1.0
3
+ Version: 2.1.2
4
4
  Summary: Opinionated mocking library for Python
5
- Home-page: https://mike.cousins.io/decoy/
6
5
  License: MIT
7
- Author: Mike Cousins
8
- Author-email: mike@cousins.io
6
+ Author: Michael Cousins
7
+ Author-email: michael@cousins.io
9
8
  Requires-Python: >=3.7,<4.0
10
9
  Classifier: Development Status :: 5 - Production/Stable
11
10
  Classifier: Intended Audience :: Developers
@@ -17,16 +16,19 @@ Classifier: Programming Language :: Python :: 3.8
17
16
  Classifier: Programming Language :: Python :: 3.9
18
17
  Classifier: Programming Language :: Python :: 3.10
19
18
  Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
20
21
  Classifier: Topic :: Software Development :: Testing
21
22
  Classifier: Topic :: Software Development :: Testing :: Mocking
22
23
  Classifier: Typing :: Typed
23
24
  Project-URL: Changelog, https://github.com/mcous/decoy/releases
24
- Project-URL: Documentation, https://mike.cousins.io/decoy/
25
+ Project-URL: Documentation, https://michael.cousins.io/decoy/
26
+ Project-URL: Homepage, https://michael.cousins.io/decoy/
25
27
  Project-URL: Repository, https://github.com/mcous/decoy
26
28
  Description-Content-Type: text/markdown
27
29
 
28
30
  <div align="center">
29
- <img alt="Decoy logo" src="https://mike.cousins.io/decoy/img/decoy.png" width="256px">
31
+ <img alt="Decoy logo" src="https://michael.cousins.io/decoy/img/decoy.png" width="256px">
30
32
  <h1 class="decoy-title">Decoy</h1>
31
33
  <p>Opinionated mocking library for Python</p>
32
34
  <p>
@@ -38,7 +40,7 @@ Description-Content-Type: text/markdown
38
40
  <a title="Supported Python Versions" href="https://pypi.org/project/decoy/"><img src="https://img.shields.io/pypi/pyversions/decoy?style=flat-square"></a>
39
41
  </p>
40
42
  <p>
41
- <a href="https://mike.cousins.io/decoy/" class="decoy-hidden">Usage guide and documentation</a>
43
+ <a href="https://michael.cousins.io/decoy/" class="decoy-hidden">Usage guide and documentation</a>
42
44
  </p>
43
45
  </div>
44
46
 
@@ -86,8 +88,8 @@ plugins = decoy.mypy
86
88
 
87
89
  Decoy works well with [pytest][], but if you use another testing library or framework, you can still use Decoy! You just need to do two things:
88
90
 
89
- 1. Create a new instance of [`Decoy()`](https://mike.cousins.io/decoy/api/#decoy.Decoy) before each test
90
- 2. Call [`decoy.reset()`](https://mike.cousins.io/decoy/api/#decoy.Decoy.reset) after each test
91
+ 1. Create a new instance of [`Decoy()`](https://michael.cousins.io/decoy/api/#decoy.Decoy) before each test
92
+ 2. Call [`decoy.reset()`](https://michael.cousins.io/decoy/api/#decoy.Decoy.reset) after each test
91
93
 
92
94
  For example, using the built-in [unittest][] framework, you would use the `setUp` fixture method to do `self.decoy = Decoy()` and the `tearDown` method to call `self.decoy.reset()`. For a working example, see [`tests/test_unittest.py`](https://github.com/mcous/decoy/blob/main/tests/test_unittest.py).
93
95
 
@@ -171,9 +173,9 @@ See [spying with verify][] for more details.
171
173
  [unittest]: https://docs.python.org/3/library/unittest.html
172
174
  [typing]: https://docs.python.org/3/library/typing.html
173
175
  [mypy]: https://mypy.readthedocs.io/
174
- [api reference]: https://mike.cousins.io/decoy/api/
175
- [usage guide]: https://mike.cousins.io/decoy/usage/create/
176
- [creating mocks]: https://mike.cousins.io/decoy/usage/create/
177
- [stubbing with when]: https://mike.cousins.io/decoy/usage/when/
178
- [spying with verify]: https://mike.cousins.io/decoy/usage/verify/
176
+ [api reference]: https://michael.cousins.io/decoy/api/
177
+ [usage guide]: https://michael.cousins.io/decoy/usage/create/
178
+ [creating mocks]: https://michael.cousins.io/decoy/usage/create/
179
+ [stubbing with when]: https://michael.cousins.io/decoy/usage/when/
180
+ [spying with verify]: https://michael.cousins.io/decoy/usage/verify/
179
181
 
@@ -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
@@ -17,9 +17,9 @@ decoy/stub_store.py,sha256=lLJGqv1CIxZlJFHhnXuoQdbp_q524eT-e8QHEDPSAew,1743
17
17
  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
- decoy/warnings.py,sha256=PsIWtGbDDTSMjQTmNlNcXaStcvli_VhepsAeZoISV2k,3344
21
- decoy-2.1.0.dist-info/LICENSE,sha256=ht-RotWwc1sy3UJIAeJehNeFO0aW_0QsCaCL0-NTgsA,1075
22
- decoy-2.1.0.dist-info/METADATA,sha256=aFMQdZx2vlnD360is1P_K9Q8kDo4UszIRHgBES-hURg,7067
23
- decoy-2.1.0.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
24
- decoy-2.1.0.dist-info/entry_points.txt,sha256=P2wF8zdthEM-3Yo32kxHDhZDjbW6AE489HPWqnvPLOU,38
25
- decoy-2.1.0.dist-info/RECORD,,
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.6.1
2
+ Generator: poetry-core 2.1.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any