Skip to content

Commit 080e690

Browse files
authored
fix(warnings): remove faulty MissingSpecAttributeWarning (#263)
Closes #257
1 parent 8df3521 commit 080e690

File tree

4 files changed

+46
-116
lines changed

4 files changed

+46
-116
lines changed

decoy/spy_core.py

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
)
1717

1818
from .spy_events import SpyInfo
19-
from .warnings import IncorrectCallWarning, MissingSpecAttributeWarning
19+
from .warnings import IncorrectCallWarning
2020

2121

2222
class _FROM_SOURCE:
@@ -123,15 +123,12 @@ def create_child_core(self, name: str, is_async: bool) -> "SpyCore":
123123
source = self._source
124124
child_name = f"{self._name}.{name}"
125125
child_source = None
126-
child_found = False
127126

128127
if inspect.isclass(source):
129128
# use type hints to get child spec for class attributes
130129
child_hint = _get_type_hints(source).get(name)
131130
# use inspect to get child spec for methods and properties
132131
child_source = inspect.getattr_static(source, name, child_hint)
133-
# record whether a child was found before we make modifications
134-
child_found = child_source is not None
135132

136133
if isinstance(child_source, property):
137134
child_source = _get_type_hints(child_source.fget).get("return")
@@ -152,13 +149,6 @@ def create_child_core(self, name: str, is_async: bool) -> "SpyCore":
152149

153150
child_source = _unwrap_optional(child_source)
154151

155-
if source is not None and child_found is False:
156-
# stacklevel: 4 ensures warning is linked to call location
157-
warnings.warn(
158-
MissingSpecAttributeWarning(f"{self._name} has no attribute '{name}'"),
159-
stacklevel=4,
160-
)
161-
162152
return SpyCore(
163153
source=child_source,
164154
name=child_name,

decoy/warnings.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -96,13 +96,3 @@ class IncorrectCallWarning(DecoyWarning):
9696
9797
[IncorrectCallWarning guide]: usage/errors-and-warnings.md#incorrectcallwarning
9898
"""
99-
100-
101-
class MissingSpecAttributeWarning(DecoyWarning):
102-
"""A warning raised if a Decoy mock with a spec is used with a missing attribute.
103-
104-
This will become an error in the next major version of Decoy.
105-
See the [MissingSpecAttributeWarning guide][] for more details.
106-
107-
[MissingSpecAttributeWarning guide]: usage/errors-and-warnings.md#missingspecattributewarning
108-
"""

docs/usage/errors-and-warnings.md

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ pytestmark = pytest.mark.filterwarnings("ignore::decoy.warnings.DecoyWarning")
8080

8181
A [decoy.warnings.MiscalledStubWarning][] is a warning provided mostly for productivity convenience. If you configure a stub but your code under test calls the stub incorrectly, it can sometimes be difficult to immediately figure out what went wrong. This warning exists to alert you if:
8282

83-
- A mock has at least 1 stubbing configured with [decoy.Decoy.when][]
84-
- A call was made to the mock that didn't match any configured stubbing
83+
- A mock has at least 1 stubbing configured with [decoy.Decoy.when][]
84+
- A call was made to the mock that didn't match any configured stubbing
8585

8686
In the example below, our test subject is supposed to call a `DataGetter` dependency and pass the output data into a `DataHandler` dependency to get the result. However, in writing `Subject.get_and_handle_data`, we forgot to pass `data_id` into `DataGetter.get`.
8787

@@ -200,27 +200,3 @@ spy(val="world") # ok
200200
spy(wrong_name="ah!") # triggers an IncorrectCallWarning
201201
spy("too", "many", "args") # triggers an IncorrectCallWarning
202202
```
203-
204-
### MissingSpecAttributeWarning
205-
206-
If you provide a Decoy mock with a specification `cls` or `func` and you attempt to access an attribute of the mock that does not exist on the specification, Decoy will raise a [decoy.warnings.MissingSpecAttributeWarning][].
207-
208-
While Decoy will merely issue a warning, this call would likely cause the Python engine to error at runtime and should not be ignored. In the next major version of Decoy, this warning will become an error.
209-
210-
```python
211-
class SomeClass:
212-
def foo(self, val: str) -> str:
213-
...
214-
215-
def some_func(val: string) -> int:
216-
...
217-
218-
class_spy = decoy.mock(cls=SomeClass)
219-
func_spy = decoy.mock(func=some_func)
220-
221-
class_spy.foo("hello") # ok
222-
class_spy.bar("world") # triggers a MissingSpecAttributeWarning
223-
224-
func_spy("hello") # ok
225-
func_spy.foo("world") # triggers a MissingSpecAttributeWarning
226-
```

tests/test_spy_core.py

Lines changed: 43 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@
22

33
import pytest
44
import inspect
5-
import warnings
65
from typing import Any, Dict, NamedTuple, Optional, Tuple, Type
76

87
from decoy.spy_core import SpyCore, BoundArgs
9-
from decoy.warnings import IncorrectCallWarning, MissingSpecAttributeWarning
8+
from decoy.warnings import IncorrectCallWarning
109
from .fixtures import (
1110
SomeClass,
1211
SomeAsyncClass,
@@ -151,6 +150,14 @@ class GetSignatureSpec(NamedTuple):
151150
),
152151
expected_signature=None,
153152
),
153+
GetSignatureSpec(
154+
subject=(
155+
SpyCore(source=SomeNestedClass, name=None)
156+
.create_child_core("union_child", is_async=False)
157+
.create_child_core("foo", is_async=False)
158+
),
159+
expected_signature=None,
160+
),
154161
GetSignatureSpec(
155162
subject=(
156163
SpyCore(source=SomeNestedClass, name=None)
@@ -256,6 +263,40 @@ class GetSignatureSpec(NamedTuple):
256263
return_annotation=None,
257264
),
258265
),
266+
GetSignatureSpec(
267+
subject=(
268+
SpyCore(source=SomeNestedClass, name=None)
269+
.create_child_core("optional_child", is_async=False)
270+
.create_child_core("foo", is_async=False)
271+
),
272+
expected_signature=inspect.Signature(
273+
parameters=[
274+
inspect.Parameter(
275+
name="val",
276+
kind=inspect.Parameter.POSITIONAL_OR_KEYWORD,
277+
annotation=str,
278+
)
279+
],
280+
return_annotation=str,
281+
),
282+
),
283+
GetSignatureSpec(
284+
subject=(
285+
SpyCore(source=SomeNestedClass, name=None)
286+
.create_child_core("union_none_child", is_async=False)
287+
.create_child_core("foo", is_async=False)
288+
),
289+
expected_signature=inspect.Signature(
290+
parameters=[
291+
inspect.Parameter(
292+
name="val",
293+
kind=inspect.Parameter.POSITIONAL_OR_KEYWORD,
294+
annotation=str,
295+
)
296+
],
297+
return_annotation=str,
298+
),
299+
),
259300
],
260301
)
261302
def test_get_signature(
@@ -440,70 +481,3 @@ def test_warn_if_called_incorrectly() -> None:
440481

441482
with pytest.warns(IncorrectCallWarning, match="missing a required argument"):
442483
subject.bind_args(wrong_arg_name="1")
443-
444-
445-
def test_warn_if_spec_does_not_have_method() -> None:
446-
"""It should trigger a warning if bound_args is called incorrectly."""
447-
class_subject = SpyCore(source=SomeClass, name=None)
448-
nested_class_subject = SpyCore(source=SomeNestedClass, name=None)
449-
func_subject = SpyCore(source=some_func, name=None)
450-
specless_subject = SpyCore(source=None, name="anonymous")
451-
452-
# specless mocks and correct usage should not warn
453-
with warnings.catch_warnings():
454-
warnings.simplefilter("error")
455-
specless_subject.create_child_core("foo", False)
456-
457-
# proper class usage should not warn
458-
with warnings.catch_warnings():
459-
warnings.simplefilter("error")
460-
class_subject.create_child_core("foo", False)
461-
462-
# property access without types should not warn
463-
with warnings.catch_warnings():
464-
warnings.simplefilter("error")
465-
class_subject.create_child_core("mystery_property", False)
466-
467-
# property access should be allowed through optionals
468-
with warnings.catch_warnings():
469-
warnings.simplefilter("error")
470-
parent = nested_class_subject.create_child_core("optional_child", False)
471-
parent.create_child_core("primitive_property", False)
472-
473-
# property access should be allowed through None unions
474-
with warnings.catch_warnings():
475-
warnings.simplefilter("error")
476-
parent = nested_class_subject.create_child_core("union_none_child", False)
477-
parent.create_child_core("primitive_property", False)
478-
479-
# property access should not be checked through unions
480-
with warnings.catch_warnings():
481-
warnings.simplefilter("error")
482-
parent = nested_class_subject.create_child_core("union_child", False)
483-
parent.create_child_core("who_knows", False)
484-
485-
# incorrect class usage should warn
486-
with pytest.warns(
487-
MissingSpecAttributeWarning, match="has no attribute 'this_is_wrong'"
488-
):
489-
class_subject.create_child_core("this_is_wrong", False)
490-
491-
# incorrect function usage should warn
492-
with pytest.warns(
493-
MissingSpecAttributeWarning, match="has no attribute 'this_is_wrong'"
494-
):
495-
func_subject.create_child_core("this_is_wrong", False)
496-
497-
# incorrect nested property usage should warn
498-
with pytest.warns(
499-
MissingSpecAttributeWarning, match="has no attribute 'this_is_wrong'"
500-
):
501-
parent = nested_class_subject.create_child_core("optional_child", False)
502-
parent.create_child_core("this_is_wrong", False)
503-
504-
# incorrect nested property usage should warn
505-
with pytest.warns(
506-
MissingSpecAttributeWarning, match="has no attribute 'this_is_wrong'"
507-
):
508-
parent = nested_class_subject.create_child_core("union_none_child", False)
509-
parent.create_child_core("this_is_wrong", False)

0 commit comments

Comments
 (0)