Skip to content

Commit 12b0fbe

Browse files
committed
Revert matcher changes and fix simple issues
1 parent 34e946e commit 12b0fbe

File tree

5 files changed

+66
-108
lines changed

5 files changed

+66
-108
lines changed

CONTRIBUTING.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ poetry run poe coverage
4141

4242
In an exciting twist, since version 1.6.0, Decoy's tests rely on Decoy itself to test (and more importantly, design) the relationships between Decoy's internal APIs. This means:
4343

44-
- Decoy's unit test suite serves as an end-to-end test of Decoy by virtue of existing (wow, very meta, actually kind of cool).
45-
- Changes that break a small part of Decoy may result in a large number of test failures, because if Decoy breaks it can't be used to test itself.
44+
- Decoy's unit test suite serves as an end-to-end test of Decoy by virtue of existing (wow, very meta, actually kind of cool).
45+
- Changes that break a small part of Decoy may result in a large number of test failures, because if Decoy breaks it can't be used to test itself.
4646

4747
If you find yourself in a situation where Decoy's test suite has blown up, **concentrate on getting the test suites that don't use Decoy to pass**. From there, lean on the type-checker to guide you to any components that aren't properly hooked up. Decoy also has a end-to-end smoke test suite (`tests/test_decoy.py`) that can be helpful in getting things back to green.
4848

decoy/matchers.py

Lines changed: 35 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,8 @@ def test_logger_called(decoy: Decoy):
2828
"""
2929

3030
from abc import ABC, abstractmethod
31-
from re import compile as compile_re, Pattern
32-
from typing import cast, TypeVar, Generic, Any, override, overload
33-
from collections.abc import Iterable, Mapping
34-
from warnings import deprecated
31+
from re import compile as compile_re
32+
from typing import cast, overload, Any, Generic, List, Mapping, Optional, Pattern, Type, TypeVar
3533

3634

3735
__all__ = [
@@ -47,9 +45,6 @@ def test_logger_called(decoy: Decoy):
4745
]
4846

4947

50-
MatchT = TypeVar("MatchT", default=Any)
51-
52-
5348
class _AnythingOrNone:
5449
def __eq__(self, target: object) -> bool:
5550
return True
@@ -59,7 +54,7 @@ def __repr__(self) -> str:
5954
return "<AnythingOrNone>"
6055

6156

62-
def AnythingOrNone() -> MatchT: # type: ignore[type-var]
57+
def AnythingOrNone() -> Any:
6358
"""Match anything including None.
6459
6560
!!! example
@@ -68,7 +63,7 @@ def AnythingOrNone() -> MatchT: # type: ignore[type-var]
6863
assert None == AnythingOrNone()
6964
```
7065
"""
71-
return cast(MatchT, _AnythingOrNone())
66+
return _AnythingOrNone()
7267

7368

7469
class _Anything:
@@ -81,7 +76,7 @@ def __repr__(self) -> str:
8176
return "<Anything>"
8277

8378

84-
def Anything() -> MatchT: # type: ignore[type-var]
79+
def Anything() -> Any:
8580
"""Match anything except None.
8681
8782
!!! example
@@ -90,17 +85,17 @@ def Anything() -> MatchT: # type: ignore[type-var]
9085
assert None != Anything()
9186
```
9287
"""
93-
return cast(MatchT, _Anything())
88+
return _Anything()
9489

9590

9691
class _IsA:
97-
_match_type: type[object]
98-
_attributes: Mapping[str, object] | None
92+
_match_type: type
93+
_attributes: Optional[Mapping[str, Any]]
9994

10095
def __init__(
10196
self,
10297
match_type: type[object],
103-
attributes: Mapping[str, object] | None = None,
98+
attributes: Optional[Mapping[str, Any]] = None,
10499
) -> None:
105100
"""Initialize the matcher with a type and optional attributes."""
106101
self._match_type = match_type
@@ -124,8 +119,8 @@ def __repr__(self) -> str:
124119

125120

126121
def IsA(
127-
match_type: type[MatchT], attributes: Mapping[str, object] | None = None
128-
) -> MatchT:
122+
match_type: type, attributes: Optional[Mapping[str, Any]] = None
123+
) -> Any:
129124
"""Match anything that satisfies the passed in type.
130125
131126
Arguments:
@@ -146,7 +141,7 @@ class HelloWorld:
146141
assert HelloWorld() == IsA(HelloWorld, {"hello": "world"})
147142
```
148143
"""
149-
return cast(MatchT, _IsA(match_type, attributes))
144+
return _IsA(match_type, attributes)
150145

151146

152147
class _IsNot:
@@ -165,7 +160,7 @@ def __repr__(self) -> str:
165160
return f"<IsNot {self._reject_value!r}>"
166161

167162

168-
def IsNot(value: MatchT) -> MatchT:
163+
def IsNot(value: object) -> Any:
169164
"""Match anything that isn't the passed in value.
170165
171166
Arguments:
@@ -178,13 +173,13 @@ def IsNot(value: MatchT) -> MatchT:
178173
assert 1 != IsNot(1)
179174
```
180175
"""
181-
return cast(MatchT, _IsNot(value))
176+
return _IsNot(value)
182177

183178

184179
class _HasAttributes:
185-
_attributes: Mapping[str, object]
180+
_attributes: Mapping[str, Any]
186181

187-
def __init__(self, attributes: Mapping[str, object]) -> None:
182+
def __init__(self, attributes: Mapping[str, Any]) -> None:
188183
self._attributes = attributes
189184

190185
def __eq__(self, target: object) -> bool:
@@ -203,7 +198,7 @@ def __repr__(self) -> str:
203198
return f"<HasAttributes {self._attributes!r}>"
204199

205200

206-
def HasAttributes(attributes: Mapping[str, object]) -> MatchT: # type: ignore[type-var]
201+
def HasAttributes(attributes: Mapping[str, Any]) -> Any:
207202
"""Match anything with the passed in attributes.
208203
209204
Arguments:
@@ -219,25 +214,23 @@ class HelloWorld:
219214
assert HelloWorld() == matchers.HasAttributes({"hello": "world"})
220215
```
221216
"""
222-
return cast(MatchT, _HasAttributes(attributes))
217+
return _HasAttributes(attributes)
223218

224219

225220
class _DictMatching:
226-
_values: Mapping[str, object]
221+
_values: Mapping[str, Any]
227222

228-
def __init__(self, values: Mapping[str, object]) -> None:
223+
def __init__(self, values: Mapping[str, Any]) -> None:
229224
self._values = values
230225

231226
def __eq__(self, target: object) -> bool:
232227
"""Return true if target matches all given keys/values."""
233-
if not isinstance(target, Mapping):
234-
return False
235228
is_match = True
236229

237230
for key, value in self._values.items():
238231
if is_match:
239232
try:
240-
is_match = key in target and target[key] == value
233+
is_match = key in target and target[key] == value # type: ignore[index,operator]
241234
except TypeError:
242235
is_match = False
243236

@@ -248,7 +241,7 @@ def __repr__(self) -> str:
248241
return f"<DictMatching {self._values!r}>"
249242

250243

251-
def DictMatching(values: Mapping[str, MatchT]) -> Mapping[str, MatchT]:
244+
def DictMatching(values: Mapping[str, Any]) -> Any:
252245
"""Match any dictionary with the passed in keys / values.
253246
254247
Arguments:
@@ -260,18 +253,18 @@ def DictMatching(values: Mapping[str, MatchT]) -> Mapping[str, MatchT]:
260253
assert value == matchers.DictMatching({"hello": "world"})
261254
```
262255
"""
263-
return cast(Mapping[str, MatchT], _DictMatching(values))
256+
return _DictMatching(values)
264257

265258

266259
class _ListMatching:
267-
_values: Iterable[object]
260+
_values: List[Any]
268261

269-
def __init__(self, values: Iterable[object]) -> None:
262+
def __init__(self, values: List[Any]) -> None:
270263
self._values = values
271264

272265
def __eq__(self, target: object) -> bool:
273266
"""Return true if target matches all given values."""
274-
if not isinstance(target, Iterable):
267+
if not hasattr(target, "__iter__"):
275268
return False
276269

277270
return all(
@@ -283,7 +276,7 @@ def __repr__(self) -> str:
283276
return f"<ListMatching {self._values!r}>"
284277

285278

286-
def ListMatching(values: list[MatchT]) -> list[MatchT]:
279+
def ListMatching(values: List[Any]) -> Any:
287280
"""Match any list with the passed in values.
288281
289282
Arguments:
@@ -295,7 +288,7 @@ def ListMatching(values: list[MatchT]) -> list[MatchT]:
295288
assert value == matchers.ListMatching([1, 2])
296289
```
297290
"""
298-
return cast(list[MatchT], _ListMatching(values))
291+
return _ListMatching(values)
299292

300293

301294
class _StringMatching:
@@ -330,10 +323,10 @@ def StringMatching(match: str) -> str:
330323

331324

332325
class _ErrorMatching:
333-
_error_type: type[BaseException]
334-
_string_matcher: _StringMatching | None
326+
_error_type: Type[BaseException]
327+
_string_matcher: Optional[_StringMatching]
335328

336-
def __init__(self, error: type[BaseException], match: str | None = None) -> None:
329+
def __init__(self, error: Type[BaseException], match: Optional[str] = None) -> None:
337330
"""Initialize with the Exception type and optional message matcher."""
338331
self._error_type = error
339332
self._string_matcher = _StringMatching(match) if match is not None else None
@@ -359,7 +352,7 @@ def __repr__(self) -> str:
359352
ErrorT = TypeVar("ErrorT", bound=BaseException)
360353

361354

362-
def ErrorMatching(error: type[ErrorT], match: str | None = None) -> ErrorT:
355+
def ErrorMatching(error: Type[ErrorT], match: Optional[str] = None) -> ErrorT:
363356
"""Match any error matching an Exception type and optional message matcher.
364357
365358
Arguments:
@@ -426,24 +419,20 @@ def __init__(self, match_type: type[CapturedT]) -> None:
426419
self._values = []
427420
self._match_type = match_type
428421

429-
@override
430422
def __eq__(self, target: object) -> bool:
431423
if isinstance(target, self._match_type):
432424
self._values.append(target)
433425
return True
434426
return False
435427

436-
@override
437428
def __repr__(self) -> str:
438429
"""Return a string representation of the matcher."""
439430
return "<Captor>"
440431

441-
@override
442432
def capture(self) -> CapturedT:
443433
return cast(CapturedT, self)
444434

445435
@property
446-
@override
447436
def value(self) -> CapturedT:
448437
if len(self._values) == 0:
449438
raise AssertionError("No value captured by captor.")
@@ -454,19 +443,16 @@ def values(self) -> list[CapturedT]:
454443
return self._values
455444

456445

446+
MatchT = TypeVar("MatchT")
447+
448+
457449
@overload
458450
def Captor() -> Any: ...
459451
@overload
460452
def Captor(match_type: type[MatchT]) -> MatchT: ...
461-
@deprecated(
462-
"Use ArgumentCaptor() instead, and then call capture() to pass the matcher as an argument."
463-
)
464453
def Captor(match_type: type[object] = object) -> object:
465454
"""Match anything, capturing its value for further assertions.
466455
467-
!!! warning Deprecated
468-
This matcher is deprecated. Use [decoy.matchers.ArgumentCaptor][] instead.
469-
470456
The last captured value will be set to `captor.value`. All captured
471457
values will be placed in the `captor.values` list, which can be
472458
helpful if a captor needs to be triggered multiple times.

docs/usage/matchers.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,14 +82,12 @@ def test_event_listener(decoy: Decoy):
8282
assert subject.has_heard_event is True
8383
```
8484

85-
!!! tip
86-
87-
If you want to only capture values of a specific type, or you would like to have stricter type checking in your tests, consider passing a type to [decoy.matchers.argument_captor][] (e.g. `argument_captor(match_type=str)`).
88-
8985
This is a pretty verbose way of writing a test, so in general, you may want to approach using `matchers.argument_captor` as a form of potential code smell / test pain. There are often better ways to structure your code for these sorts of interactions that don't involve private functions.
9086

9187
For further reading on when (or rather, when not) to use argument captors, check out [testdouble's documentation on its argument captor matcher](https://github.com/testdouble/testdouble.js/blob/main/docs/6-verifying-invocations.md#tdmatcherscaptor).
9288

89+
If you would like to have stricter type checking in your tests, consider passing a type to [decoy.matchers.argument_captor][] (e.g. `argument_captor(match_type=str)`). By doing so, captured values will be cast to the specified type.
90+
9391
## Writing custom matchers
9492

9593
You can write your own matcher class and use it wherever you would use a built-in matcher. All you need to do is define a class with an `__eq__` method:

tests/test_matchers.py

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@
33
import pytest
44
from collections import namedtuple
55
from decoy import Decoy, matchers
6-
from typing import NamedTuple
6+
from typing import Any, List, NamedTuple
77
from .fixtures import SomeClass
8-
import warnings
98

109

1110
class _HelloClass(NamedTuple):
@@ -102,8 +101,9 @@ def test_dict_matching_matcher() -> None:
102101
)
103102

104103
assert {"hello": "world"} != matchers.DictMatching({"goodbye": "so long"})
105-
assert 1 != matchers.DictMatching({"hello": "world"}) # type: ignore[comparison-overlap]
106-
assert [] != matchers.DictMatching({"hello": "world"}) # type: ignore[comparison-overlap]
104+
assert 1 != matchers.DictMatching({"hello": "world"})
105+
assert False != matchers.DictMatching({"hello": "world"}) # noqa: E712
106+
assert [] != matchers.DictMatching({"hello": "world"})
107107

108108

109109
def test_list_matching_matcher() -> None:
@@ -124,7 +124,7 @@ def test_list_matching_matcher() -> None:
124124
[{"yoo": "mann"}]
125125
)
126126

127-
assert 1 != matchers.ListMatching([1]) # type: ignore[comparison-overlap]
127+
assert 1 != matchers.ListMatching([1])
128128

129129
assert str(matchers.ListMatching([1])) == "<ListMatching [1]>"
130130

@@ -145,18 +145,8 @@ def test_error_matching_matcher() -> None:
145145

146146
def test_captor_matcher() -> None:
147147
"""It should have a captor matcher that captures the compared value."""
148-
with warnings.catch_warnings():
149-
warnings.filterwarnings("ignore", category=DeprecationWarning)
150-
captor = matchers.Captor()
151-
comparisons: list[object] = [
152-
1,
153-
False,
154-
None,
155-
{},
156-
[],
157-
("hello", "world"),
158-
SomeClass(),
159-
]
148+
captor = matchers.Captor()
149+
comparisons: List[Any] = [1, False, None, {}, [], ("hello", "world"), SomeClass()]
160150

161151
for i, compare in enumerate(comparisons):
162152
assert compare == captor
@@ -207,9 +197,7 @@ def test_argument_captor_matcher_with_match_type() -> None:
207197

208198
def test_captor_matcher_raises_if_no_value() -> None:
209199
"""The captor matcher should raise an assertion error if no value."""
210-
with warnings.catch_warnings():
211-
warnings.filterwarnings("ignore", category=DeprecationWarning)
212-
captor = matchers.Captor()
200+
captor = matchers.Captor()
213201

214202
with pytest.raises(AssertionError, match="No value captured"):
215203
captor.value # noqa: B018

0 commit comments

Comments
 (0)