@@ -25,7 +25,7 @@ def test_logger_called(decoy: Decoy):
25
25
equality comparisons for stubbing and verification.
26
26
"""
27
27
from re import compile as compile_re
28
- from typing import cast , Any , List , Optional , Pattern , Type
28
+ from typing import cast , Any , List , Mapping , Optional , Pattern , Type
29
29
30
30
31
31
__all__ = [
@@ -62,34 +62,54 @@ def Anything() -> Any:
62
62
63
63
class _IsA :
64
64
_match_type : type
65
-
66
- def __init__ (self , match_type : type ) -> None :
67
- """Initialize the matcher with a type."""
65
+ _attributes : Optional [Mapping [str , Any ]]
66
+
67
+ def __init__ (
68
+ self ,
69
+ match_type : type ,
70
+ attributes : Optional [Mapping [str , Any ]] = None ,
71
+ ) -> None :
72
+ """Initialize the matcher with a type and optional attributes."""
68
73
self ._match_type = match_type
74
+ self ._attributes = attributes
69
75
70
76
def __eq__ (self , target : object ) -> bool :
71
- """Return true if target is a self._match_type."""
72
- return type (target ) == self ._match_type
77
+ """Return true if target is the correct type and matches attributes."""
78
+ matches_type = type (target ) == self ._match_type
79
+ matches_attrs = target == HasAttributes (self ._attributes or {})
80
+
81
+ return matches_type and matches_attrs
73
82
74
83
def __repr__ (self ) -> str :
75
84
"""Return a string representation of the matcher."""
76
- return "<IsA {self._match_type.__name__}>"
85
+ if self ._attributes is None :
86
+ return f"<IsA { self ._match_type .__name__ } >"
87
+ else :
88
+ return f"<IsA { self ._match_type .__name__ } { repr (self ._attributes )} >"
77
89
78
90
79
- def IsA (match_type : type ) -> Any :
91
+ def IsA (match_type : type , attributes : Optional [ Mapping [ str , Any ]] = None ) -> Any :
80
92
"""Match anything that satisfies the passed in type.
81
93
82
94
Arguments:
83
95
match_type: Type to match.
96
+ attributes: Optional set of attributes to match
84
97
85
98
Example:
86
99
```python
87
100
assert "foobar" == IsA(str)
88
101
assert datetime.now() == IsA(datetime)
89
102
assert 42 == IsA(int)
103
+
104
+ @dataclass
105
+ class HelloWorld:
106
+ hello: str = "world"
107
+ goodby: str = "so long"
108
+
109
+ assert HelloWorld() == IsA(HelloWorld, {"hello": "world"})
90
110
```
91
111
"""
92
- return _IsA (match_type )
112
+ return _IsA (match_type , attributes )
93
113
94
114
95
115
class _IsNot :
@@ -124,6 +144,86 @@ def IsNot(value: object) -> Any:
124
144
return _IsNot (value )
125
145
126
146
147
+ class _HasAttributes :
148
+ _attributes : Mapping [str , Any ]
149
+
150
+ def __init__ (self , attributes : Mapping [str , Any ]) -> None :
151
+ self ._attributes = attributes
152
+
153
+ def __eq__ (self , target : object ) -> bool :
154
+ """Return true if target matches all given attributes."""
155
+ is_match = True
156
+ for attr_name , value in self ._attributes .items ():
157
+ if is_match :
158
+ is_match = (
159
+ hasattr (target , attr_name ) and getattr (target , attr_name ) == value
160
+ )
161
+
162
+ return is_match
163
+
164
+ def __repr__ (self ) -> str :
165
+ """Return a string representation of the matcher."""
166
+ return f"<HasAttributes { repr (self ._attributes )} >"
167
+
168
+
169
+ def HasAttributes (attributes : Mapping [str , Any ]) -> Any :
170
+ """Match anything with the passed in attributes.
171
+
172
+ Arguments:
173
+ attributes: Attribute values to check.
174
+
175
+ Example:
176
+ ```python
177
+ @dataclass
178
+ class HelloWorld:
179
+ hello: str = "world"
180
+ goodby: str = "so long"
181
+
182
+ assert HelloWorld() == matchers.HasAttributes({"hello": "world"})
183
+ ```
184
+ """
185
+ return _HasAttributes (attributes )
186
+
187
+
188
+ class _DictMatching :
189
+ _values : Mapping [str , Any ]
190
+
191
+ def __init__ (self , values : Mapping [str , Any ]) -> None :
192
+ self ._values = values
193
+
194
+ def __eq__ (self , target : object ) -> bool :
195
+ """Return true if target matches all given keys/values."""
196
+ is_match = True
197
+
198
+ for key , value in self ._values .items ():
199
+ if is_match :
200
+ try :
201
+ is_match = key in target and target [key ] == value # type: ignore[index,operator] # noqa: E501
202
+ except TypeError :
203
+ is_match = False
204
+
205
+ return is_match
206
+
207
+ def __repr__ (self ) -> str :
208
+ """Return a string representation of the matcher."""
209
+ return f"<DictMatching { repr (self ._values )} >"
210
+
211
+
212
+ def DictMatching (values : Mapping [str , Any ]) -> Any :
213
+ """Match any dictionary with the passed in keys / values.
214
+
215
+ Arguments:
216
+ values: Keys and values to check.
217
+
218
+ Example:
219
+ ```python
220
+ value = {"hello": "world", "goodbye": "so long"}
221
+ assert value == matchers.DictMatching({"hello": "world"})
222
+ ```
223
+ """
224
+ return _DictMatching (values )
225
+
226
+
127
227
class _StringMatching :
128
228
_pattern : Pattern [str ]
129
229
0 commit comments