Skip to content

Commit cb38433

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

File tree

6 files changed

+67
-111
lines changed

6 files changed

+67
-111
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 & 51 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,
102-
match_type: type[object],
103-
attributes: Mapping[str, object] | None = None,
97+
match_type: type,
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
@@ -123,9 +118,7 @@ def __repr__(self) -> str:
123118
return f"<IsA {self._match_type.__name__} {self._attributes!r}>"
124119

125120

126-
def IsA(
127-
match_type: type[MatchT], attributes: Mapping[str, object] | None = None
128-
) -> MatchT:
121+
def IsA(match_type: type, attributes: Optional[Mapping[str, Any]] = None) -> Any:
129122
"""Match anything that satisfies the passed in type.
130123
131124
Arguments:
@@ -146,7 +139,7 @@ class HelloWorld:
146139
assert HelloWorld() == IsA(HelloWorld, {"hello": "world"})
147140
```
148141
"""
149-
return cast(MatchT, _IsA(match_type, attributes))
142+
return _IsA(match_type, attributes)
150143

151144

152145
class _IsNot:
@@ -165,7 +158,7 @@ def __repr__(self) -> str:
165158
return f"<IsNot {self._reject_value!r}>"
166159

167160

168-
def IsNot(value: MatchT) -> MatchT:
161+
def IsNot(value: object) -> Any:
169162
"""Match anything that isn't the passed in value.
170163
171164
Arguments:
@@ -178,13 +171,13 @@ def IsNot(value: MatchT) -> MatchT:
178171
assert 1 != IsNot(1)
179172
```
180173
"""
181-
return cast(MatchT, _IsNot(value))
174+
return _IsNot(value)
182175

183176

184177
class _HasAttributes:
185-
_attributes: Mapping[str, object]
178+
_attributes: Mapping[str, Any]
186179

187-
def __init__(self, attributes: Mapping[str, object]) -> None:
180+
def __init__(self, attributes: Mapping[str, Any]) -> None:
188181
self._attributes = attributes
189182

190183
def __eq__(self, target: object) -> bool:
@@ -203,7 +196,7 @@ def __repr__(self) -> str:
203196
return f"<HasAttributes {self._attributes!r}>"
204197

205198

206-
def HasAttributes(attributes: Mapping[str, object]) -> MatchT: # type: ignore[type-var]
199+
def HasAttributes(attributes: Mapping[str, Any]) -> Any:
207200
"""Match anything with the passed in attributes.
208201
209202
Arguments:
@@ -219,25 +212,23 @@ class HelloWorld:
219212
assert HelloWorld() == matchers.HasAttributes({"hello": "world"})
220213
```
221214
"""
222-
return cast(MatchT, _HasAttributes(attributes))
215+
return _HasAttributes(attributes)
223216

224217

225218
class _DictMatching:
226-
_values: Mapping[str, object]
219+
_values: Mapping[str, Any]
227220

228-
def __init__(self, values: Mapping[str, object]) -> None:
221+
def __init__(self, values: Mapping[str, Any]) -> None:
229222
self._values = values
230223

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

237228
for key, value in self._values.items():
238229
if is_match:
239230
try:
240-
is_match = key in target and target[key] == value
231+
is_match = key in target and target[key] == value # type: ignore[index,operator]
241232
except TypeError:
242233
is_match = False
243234

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

250241

251-
def DictMatching(values: Mapping[str, MatchT]) -> Mapping[str, MatchT]:
242+
def DictMatching(values: Mapping[str, Any]) -> Any:
252243
"""Match any dictionary with the passed in keys / values.
253244
254245
Arguments:
@@ -260,18 +251,18 @@ def DictMatching(values: Mapping[str, MatchT]) -> Mapping[str, MatchT]:
260251
assert value == matchers.DictMatching({"hello": "world"})
261252
```
262253
"""
263-
return cast(Mapping[str, MatchT], _DictMatching(values))
254+
return _DictMatching(values)
264255

265256

266257
class _ListMatching:
267-
_values: Iterable[object]
258+
_values: List[Any]
268259

269-
def __init__(self, values: Iterable[object]) -> None:
260+
def __init__(self, values: List[Any]) -> None:
270261
self._values = values
271262

272263
def __eq__(self, target: object) -> bool:
273264
"""Return true if target matches all given values."""
274-
if not isinstance(target, Iterable):
265+
if not hasattr(target, "__iter__"):
275266
return False
276267

277268
return all(
@@ -283,7 +274,7 @@ def __repr__(self) -> str:
283274
return f"<ListMatching {self._values!r}>"
284275

285276

286-
def ListMatching(values: list[MatchT]) -> list[MatchT]:
277+
def ListMatching(values: List[Any]) -> Any:
287278
"""Match any list with the passed in values.
288279
289280
Arguments:
@@ -295,7 +286,7 @@ def ListMatching(values: list[MatchT]) -> list[MatchT]:
295286
assert value == matchers.ListMatching([1, 2])
296287
```
297288
"""
298-
return cast(list[MatchT], _ListMatching(values))
289+
return _ListMatching(values)
299290

300291

301292
class _StringMatching:
@@ -330,10 +321,10 @@ def StringMatching(match: str) -> str:
330321

331322

332323
class _ErrorMatching:
333-
_error_type: type[BaseException]
334-
_string_matcher: _StringMatching | None
324+
_error_type: Type[BaseException]
325+
_string_matcher: Optional[_StringMatching]
335326

336-
def __init__(self, error: type[BaseException], match: str | None = None) -> None:
327+
def __init__(self, error: Type[BaseException], match: Optional[str] = None) -> None:
337328
"""Initialize with the Exception type and optional message matcher."""
338329
self._error_type = error
339330
self._string_matcher = _StringMatching(match) if match is not None else None
@@ -359,7 +350,7 @@ def __repr__(self) -> str:
359350
ErrorT = TypeVar("ErrorT", bound=BaseException)
360351

361352

362-
def ErrorMatching(error: type[ErrorT], match: str | None = None) -> ErrorT:
353+
def ErrorMatching(error: Type[ErrorT], match: Optional[str] = None) -> ErrorT:
363354
"""Match any error matching an Exception type and optional message matcher.
364355
365356
Arguments:
@@ -426,24 +417,20 @@ def __init__(self, match_type: type[CapturedT]) -> None:
426417
self._values = []
427418
self._match_type = match_type
428419

429-
@override
430420
def __eq__(self, target: object) -> bool:
431421
if isinstance(target, self._match_type):
432422
self._values.append(target)
433423
return True
434424
return False
435425

436-
@override
437426
def __repr__(self) -> str:
438427
"""Return a string representation of the matcher."""
439428
return "<Captor>"
440429

441-
@override
442430
def capture(self) -> CapturedT:
443431
return cast(CapturedT, self)
444432

445433
@property
446-
@override
447434
def value(self) -> CapturedT:
448435
if len(self._values) == 0:
449436
raise AssertionError("No value captured by captor.")
@@ -454,19 +441,16 @@ def values(self) -> list[CapturedT]:
454441
return self._values
455442

456443

444+
MatchT = TypeVar("MatchT")
445+
446+
457447
@overload
458448
def Captor() -> Any: ...
459449
@overload
460450
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-
)
464451
def Captor(match_type: type[object] = object) -> object:
465452
"""Match anything, capturing its value for further assertions.
466453
467-
!!! warning Deprecated
468-
This matcher is deprecated. Use [decoy.matchers.ArgumentCaptor][] instead.
469-
470454
The last captured value will be set to `captor.value`. All captured
471455
values will be placed in the `captor.values` list, which can be
472456
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:

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "decoy"
3-
version = "2.3.0"
3+
version = "2.2.0"
44
description = "Opinionated mocking library for Python"
55
authors = ["Michael Cousins <[email protected]>"]
66
license = "MIT"

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)