assertpy 2.35.0__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.
Files changed (63) hide show
  1. assertpy/__init__.py +87 -0
  2. assertpy/_clustering.py +511 -0
  3. assertpy/_dangling.py +505 -0
  4. assertpy/_engine/__init__.py +1 -0
  5. assertpy/_engine/_builder_check_typing.py +1365 -0
  6. assertpy/_engine/_capable_typing.py +772 -0
  7. assertpy/_engine/_check_typing.py +753 -0
  8. assertpy/_engine/_compare.py +983 -0
  9. assertpy/_engine/_compat.py +38 -0
  10. assertpy/_engine/_contract.py +1926 -0
  11. assertpy/_engine/_diff.py +1058 -0
  12. assertpy/_engine/_equality.py +843 -0
  13. assertpy/_engine/_introspection.py +729 -0
  14. assertpy/_engine/_membership.py +522 -0
  15. assertpy/_engine/_mixin_base.py +258 -0
  16. assertpy/_engine/_negated_typing.py +2176 -0
  17. assertpy/_engine/_operations.py +75 -0
  18. assertpy/_engine/_ordering.py +884 -0
  19. assertpy/_engine/_pairing.py +70 -0
  20. assertpy/_engine/_path.py +158 -0
  21. assertpy/_engine/_poll_typing.py +6039 -0
  22. assertpy/_engine/_require.py +312 -0
  23. assertpy/_engine/_size.py +31 -0
  24. assertpy/_engine/_text.py +39 -0
  25. assertpy/_engine/_typing.py +1226 -0
  26. assertpy/_hints.py +1213 -0
  27. assertpy/_inline.py +484 -0
  28. assertpy/_matcher_impls.py +1876 -0
  29. assertpy/_passes.py +249 -0
  30. assertpy/_renamed.py +64 -0
  31. assertpy/_satisfies.py +835 -0
  32. assertpy/_snapshot_codec.py +290 -0
  33. assertpy/assertpy.py +2640 -0
  34. assertpy/async_assertions.py +864 -0
  35. assertpy/base.py +862 -0
  36. assertpy/behave_matchers.py +96 -0
  37. assertpy/bytes_mixin.py +158 -0
  38. assertpy/collection.py +528 -0
  39. assertpy/contains.py +1068 -0
  40. assertpy/dataframe.py +200 -0
  41. assertpy/date.py +327 -0
  42. assertpy/dict.py +249 -0
  43. assertpy/dynamic.py +222 -0
  44. assertpy/errors.py +782 -0
  45. assertpy/exception.py +838 -0
  46. assertpy/extracting.py +266 -0
  47. assertpy/file.py +324 -0
  48. assertpy/helpers.py +747 -0
  49. assertpy/http_mixin.py +218 -0
  50. assertpy/json_mixin.py +1265 -0
  51. assertpy/matchers.py +639 -0
  52. assertpy/numeric.py +696 -0
  53. assertpy/outcome.py +163 -0
  54. assertpy/py.typed +0 -0
  55. assertpy/pytest_plugin.py +1650 -0
  56. assertpy/snapshot.py +1031 -0
  57. assertpy/string.py +803 -0
  58. assertpy/warning.py +137 -0
  59. assertpy-2.35.0.dist-info/METADATA +324 -0
  60. assertpy-2.35.0.dist-info/RECORD +63 -0
  61. assertpy-2.35.0.dist-info/WHEEL +4 -0
  62. assertpy-2.35.0.dist-info/entry_points.txt +2 -0
  63. assertpy-2.35.0.dist-info/licenses/LICENSE +28 -0
assertpy/dict.py ADDED
@@ -0,0 +1,249 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, cast
4
+
5
+ from ._engine._mixin_base import _MixinBase
6
+ from ._engine._ordering import REFUSALS, equals, lookup, member
7
+ from ._engine._require import argument, refuse
8
+ from .contains import _entry_not_found, _value_not_found
9
+ from .errors import _capped
10
+
11
+ if TYPE_CHECKING:
12
+ from ._engine._compat import Self
13
+
14
+ __tracebackhide__ = True
15
+
16
+
17
+ class DictMixin(_MixinBase):
18
+ """Dict assertions mixin."""
19
+
20
+ def contains_key(self, *keys: object) -> Self:
21
+ """Asserts the val is a dict and contains the given key or keys.
22
+
23
+ Alias for [`contains()`][assertpy.contains.ContainsMixin.contains].
24
+
25
+ Checks if the dict contains the given key or keys using ``in`` operator.
26
+
27
+ Args:
28
+ *keys: the key or keys expected to be contained
29
+
30
+ Examples:
31
+ Usage:
32
+
33
+ assert_that({'a': 1, 'b': 2}).contains_key('a')
34
+ assert_that({'a': 1, 'b': 2}).contains_key('a', 'b')
35
+
36
+ Returns:
37
+ AssertionBuilder: returns this instance to chain to the next assertion
38
+
39
+ Raises:
40
+ AssertionError: if val does **not** contain the key or keys
41
+ """
42
+ self._require_dict_like(self.val, check_values=False, check_getitem=False)
43
+ return self.contains(*keys)
44
+
45
+ def does_not_contain_key(self, *keys: object) -> Self:
46
+ """Asserts the val is a dict and does not contain the given key or keys.
47
+
48
+ Alias for [`does_not_contain()`][assertpy.contains.ContainsMixin.does_not_contain].
49
+
50
+ Checks if the dict excludes the given key or keys using ``in`` operator.
51
+
52
+ Args:
53
+ *keys: the key or keys expected to be excluded
54
+
55
+ Examples:
56
+ Usage:
57
+
58
+ assert_that({'a': 1, 'b': 2}).does_not_contain_key('x')
59
+ assert_that({'a': 1, 'b': 2}).does_not_contain_key('x', 'y')
60
+
61
+ Returns:
62
+ AssertionBuilder: returns this instance to chain to the next assertion
63
+
64
+ Raises:
65
+ AssertionError: if val **does** contain the key or keys
66
+ """
67
+ self._require_dict_like(self.val, check_values=False, check_getitem=False)
68
+ return self.does_not_contain(*keys)
69
+
70
+ def contains_value(self, *values: object) -> Self:
71
+ """Asserts that val is a dict and contains the given value or values.
72
+
73
+ Checks if the dict contains the given value or values in *any* key.
74
+
75
+ Args:
76
+ *values: the value or values expected to be contained
77
+
78
+ Examples:
79
+ Usage:
80
+
81
+ assert_that({'a': 1, 'b': 2}).contains_value(1)
82
+ assert_that({'a': 1, 'b': 2}).contains_value(1, 2)
83
+
84
+ Returns:
85
+ AssertionBuilder: returns this instance to chain to the next assertion
86
+
87
+ Raises:
88
+ AssertionError: if val does **not** contain the value or values
89
+ """
90
+ self._require_dict_like(self.val, check_getitem=False)
91
+ if len(values) == 0:
92
+ raise ValueError("one or more value args must be given")
93
+ missing = [value for value in values if not member(value, self.val.values())]
94
+ if missing:
95
+ return self.error(
96
+ f"Expected <{_capped(self.val)}> to contain values {self._fmt_items(values)},"
97
+ f" but did not contain {self._fmt_items(missing)}." + _value_not_found(self.val, missing),
98
+ expected=values,
99
+ )
100
+ return self
101
+
102
+ def does_not_contain_value(self, *values: object) -> Self:
103
+ """Asserts that val is a dict and does not contain the given value or values.
104
+
105
+ Checks if the dict excludes the given value or values across *all* keys.
106
+
107
+ Args:
108
+ *values: the value or values expected to be excluded
109
+
110
+ Examples:
111
+ Usage:
112
+
113
+ assert_that({'a': 1, 'b': 2}).does_not_contain_value(3)
114
+ assert_that({'a': 1, 'b': 2}).does_not_contain_value(3, 4)
115
+
116
+ Returns:
117
+ AssertionBuilder: returns this instance to chain to the next assertion
118
+
119
+ Raises:
120
+ AssertionError: if val **does** contain the value or values
121
+ """
122
+ self._require_dict_like(self.val, check_getitem=False)
123
+ if len(values) == 0:
124
+ raise ValueError("one or more value args must be given")
125
+ else:
126
+ found = [value for value in values if member(value, self.val.values())]
127
+ if found:
128
+ return self.error(
129
+ f"Expected <{_capped(self.val)}> to not contain values {self._fmt_items(values)},"
130
+ f" but did contain {self._fmt_items(found)}."
131
+ )
132
+ return self
133
+
134
+ def contains_entry(self, *args: object, **kwargs: object) -> Self:
135
+ """Asserts that val is a dict and contains the given entry or entries.
136
+
137
+ Checks if the dict contains the given key-value pair or pairs.
138
+
139
+ Args:
140
+ *args (object): the entry or entries expected to be contained (as ``{k: v}`` args)
141
+ **kwargs (object): the entry or entries expected to be contained (as ``k=v`` kwargs)
142
+
143
+ Examples:
144
+ Usage:
145
+
146
+ # using args
147
+ assert_that({'a': 1, 'b': 2, 'c': 3}).contains_entry({'a': 1})
148
+ assert_that({'a': 1, 'b': 2, 'c': 3}).contains_entry({'a': 1}, {'b': 2})
149
+ assert_that({'a': 1, 'b': 2, 'c': 3}).contains_entry({'a': 1}, {'b': 2}, {'c': 3})
150
+
151
+ # using kwargs
152
+ assert_that({'a': 1, 'b': 2, 'c': 3}).contains_entry(a=1)
153
+ assert_that({'a': 1, 'b': 2, 'c': 3}).contains_entry(a=1, b=2)
154
+ assert_that({'a': 1, 'b': 2, 'c': 3}).contains_entry(a=1, b=2, c=3)
155
+
156
+ # or args and kwargs
157
+ assert_that({'a': 1, 'b': 2, 'c': 3}).contains_entry({'c': 3}, a=1, b=2)
158
+
159
+
160
+ Returns:
161
+ AssertionBuilder: returns this instance to chain to the next assertion
162
+
163
+ Raises:
164
+ AssertionError: if val does **not** contain the entry or entries
165
+
166
+ Note:
167
+ Where one entry is not found, the failure says what its key holds, or that the key is not there, and
168
+ on a line under the message why the value held is not the one expected, where one fact says so.
169
+ """
170
+ self._require_dict_like(self.val, check_values=False)
171
+ entries = list(args) + [{key: value} for key, value in kwargs.items()]
172
+ if len(entries) == 0:
173
+ raise ValueError("one or more entry args must be given")
174
+ missing = []
175
+ for entry in entries:
176
+ if type(entry) is not dict:
177
+ refuse(entry, "a dict", subject=argument("entry"))
178
+ # `object` not `dict`: the check above is `type(entry) is dict`, so a subclass is refused here
179
+ pair = cast("dict[Any, Any]", entry)
180
+ if len(pair) != 1:
181
+ raise ValueError("given entry args must contain exactly one key-value pair")
182
+ entry_key = next(iter(pair))
183
+ try:
184
+ if entry_key in self.val and equals(self.val[entry_key], pair[entry_key]):
185
+ continue
186
+ except REFUSALS as refusal:
187
+ found, held = lookup(self.val, entry_key, refusal)
188
+ if found and equals(held, pair[entry_key]):
189
+ continue
190
+ missing.append(pair)
191
+ if missing:
192
+ return self.error(
193
+ f"Expected <{_capped(self.val)}> to contain entries {self._fmt_items(entries)},"
194
+ f" but did not contain {self._fmt_items(missing)}." + _entry_not_found(self.val, missing),
195
+ expected=entries,
196
+ )
197
+ return self
198
+
199
+ def does_not_contain_entry(self, *args: object, **kwargs: object) -> Self:
200
+ """Asserts that val is a dict and does not contain the given entry or entries.
201
+
202
+ Checks if the dict excludes the given key-value pair or pairs.
203
+
204
+ Args:
205
+ *args (object): the entry or entries expected to be excluded (as ``{k: v}`` args)
206
+ **kwargs (object): the entry or entries expected to be excluded (as ``k=v`` kwargs)
207
+
208
+ Examples:
209
+ Usage:
210
+
211
+ # using args
212
+ assert_that({'a': 1, 'b': 2, 'c': 3}).does_not_contain_entry({'a': 2})
213
+ assert_that({'a': 1, 'b': 2, 'c': 3}).does_not_contain_entry({'a': 2}, {'x': 4})
214
+
215
+ # using kwargs
216
+ assert_that({'a': 1, 'b': 2, 'c': 3}).does_not_contain_entry(a=2)
217
+ assert_that({'a': 1, 'b': 2, 'c': 3}).does_not_contain_entry(a=2, x=4)
218
+
219
+ Returns:
220
+ AssertionBuilder: returns this instance to chain to the next assertion
221
+
222
+ Raises:
223
+ AssertionError: if val **does** contain the entry or entries
224
+ """
225
+ self._require_dict_like(self.val, check_values=False)
226
+ entries = list(args) + [{key: value} for key, value in kwargs.items()]
227
+ if len(entries) == 0:
228
+ raise ValueError("one or more entry args must be given")
229
+ found = []
230
+ for entry in entries:
231
+ if type(entry) is not dict:
232
+ refuse(entry, "a dict", subject=argument("entry"))
233
+ pair = cast("dict[Any, Any]", entry)
234
+ if len(pair) != 1:
235
+ raise ValueError("given entry args must contain exactly one key-value pair")
236
+ entry_key = next(iter(pair))
237
+ try:
238
+ present = entry_key in self.val and equals(self.val[entry_key], pair[entry_key])
239
+ except REFUSALS as refusal:
240
+ held_there, held = lookup(self.val, entry_key, refusal)
241
+ present = held_there and equals(held, pair[entry_key])
242
+ if present:
243
+ found.append(pair)
244
+ if found:
245
+ return self.error(
246
+ f"Expected <{_capped(self.val)}> to not contain entries {self._fmt_items(entries)},"
247
+ f" but did contain {self._fmt_items(found)}."
248
+ )
249
+ return self
assertpy/dynamic.py ADDED
@@ -0,0 +1,222 @@
1
+ from __future__ import annotations
2
+
3
+ import collections.abc
4
+ import inspect
5
+ from typing import Any, cast
6
+
7
+ from ._engine._diff import _build_equality_diff, _pair_itself
8
+ from ._engine._equality import mapping_shaped
9
+ from ._engine._introspection import is_namedtuple
10
+ from ._engine._mixin_base import _MixinBase
11
+ from ._engine._path import _ROOT
12
+ from ._hints import Roles, not_found
13
+ from .errors import DiffResult, _first_difference, _formatted, _parted, _told_apart, _truncated
14
+ from .helpers import _both_list_like, _elided_seq_repr, _elided_walks
15
+ from .http_mixin import response_of
16
+ from .outcome import MISSING, Requirement
17
+
18
+ __tracebackhide__ = True
19
+
20
+
21
+ _ONE_OPERAND = inspect.Signature([inspect.Parameter("other", inspect.Parameter.POSITIONAL_OR_KEYWORD)])
22
+
23
+
24
+ def _one_operand(args: tuple[object, ...], kwargs: dict[str, object]) -> dict[str, object]:
25
+ """The operand by name, or the call as given when the arity is what the wrapper is about to refuse."""
26
+ try:
27
+ return dict(_ONE_OPERAND.bind_partial(*args, **kwargs).arguments)
28
+ except TypeError:
29
+ return {"args": args, "kwargs": kwargs}
30
+
31
+
32
+ def _near(name: str, value: object, *, keyed: bool) -> str:
33
+ """A line, on a line of its own, on a key or an attribute of *value* that reads almost as *name*, or nothing.
34
+
35
+ The keys are read by walking the value once, as `contains_key` walks it. The attributes are the public
36
+ names its ``__dir__`` lists that the object does hold, looked up without calling a property: a ``__dir__``
37
+ of the value's own may list what is not there. Asked of the type and not through `dir`, which sorts what
38
+ it is given, and sorting compares names that may be no plain text.
39
+ """
40
+ try:
41
+ if keyed:
42
+ names = cast("collections.abc.Iterable[object]", value)
43
+ line = not_found(name, names, Roles("the key not found", "a key", named=True))
44
+ else:
45
+ listed = type.__getattribute__(type(value), "__dir__")(value)
46
+ public = [each for each in listed if type(each) is str and not each.startswith("_")]
47
+ held = [each for each in public if inspect.getattr_static(value, each, MISSING) is not MISSING]
48
+ line = not_found(name, held, Roles("the attribute not found", "an attribute", named=True))
49
+ except Exception: # a diagnostic must never outrank the failure it is describing
50
+ return ""
51
+ return "" if line is None else f"\n{line}"
52
+
53
+
54
+ def _read_again(builder: Any, actual: object, expected: object) -> tuple[tuple[str, str] | None, DiffResult]:
55
+ """What `is_equal_to` reads of two values found unequal: their two collapsed texts, where they have any, and
56
+ their diff.
57
+
58
+ Two mappings, as the pair stands once ``==`` has answered, by the call `is_equal_to` fails with on two
59
+ mappings (`_failure_views`): it renders each side and then builds the entries, three readings of the
60
+ pair, and where the values refuse any of them the three are given up together, for no texts and the
61
+ pair itself. Two plain sequences are collapsed as `is_equal_to` collapses them, and there the texts
62
+ and the diff are given up each on its own. ``==`` of two lists stops at the first pair that differs,
63
+ and every one of these readings goes on past it, to an element the verdict never asked.
64
+ """
65
+ if mapping_shaped(actual, check_values=False) and mapping_shaped(expected, check_values=False):
66
+ try:
67
+ kind, entries, actual_text, expected_text, _copies = builder._failure_views(
68
+ actual, expected, None, None, None, *_elided_walks(None)
69
+ )
70
+ except Exception: # a diagnostic must never outrank the failure it is describing
71
+ return None, _pair_itself(actual, expected)
72
+ diff = DiffResult(kind=kind, entries=entries or [_ROOT.leaf_entry(actual=actual, expected=expected)])
73
+ return (actual_text, expected_text), diff
74
+ texts = None
75
+ if _both_list_like(actual, expected):
76
+ try:
77
+ texts = _truncated(_elided_seq_repr(actual, expected)), _truncated(_elided_seq_repr(expected, actual))
78
+ except Exception: # as above
79
+ texts = None
80
+ try:
81
+ return texts, _build_equality_diff(actual, expected)
82
+ except Exception: # as above
83
+ return texts, _pair_itself(actual, expected)
84
+
85
+
86
+ def _as_equality_fails(builder: Any, actual: object, expected: object) -> tuple[str, str, DiffResult]:
87
+ """The two sides of a failed ``has_<name>()`` as its message prints them, and the diff it carries.
88
+
89
+ Collapsed as `is_equal_to` collapses them where they are two mappings or two plain sequences
90
+ (`_read_again`). Any other pair as two texts cut around the place they part, which is also what those two
91
+ fall back to where the collapsed texts read alike or could not be made. So the message differs from the
92
+ one `is_equal_to` prints exactly there: it keeps two texts that read alike.
93
+ """
94
+ collapsed, diff = _read_again(builder, actual, expected)
95
+ if collapsed is not None and collapsed[0] != collapsed[1]:
96
+ return *collapsed, diff
97
+ actual_text, expected_text = _formatted(actual), _formatted(expected)
98
+ parted = _parted(actual_text, expected_text, _first_difference(actual_text, expected_text))
99
+ return *_told_apart(*parted, actual, expected), diff
100
+
101
+
102
+ class DynamicMixin(_MixinBase):
103
+ """Dynamic assertions mixin.
104
+
105
+ When testing attributes of an object (or the contents of a dict), the
106
+ [`is_equal_to()`][assertpy.base.BaseMixin.is_equal_to] assertion can be a bit verbose:
107
+
108
+ fred = Person('Fred', 'Smith')
109
+
110
+ assert_that(fred.first_name).is_equal_to('Fred')
111
+ assert_that(fred.name).is_equal_to('Fred Smith')
112
+ assert_that(fred.say_hello()).is_equal_to('Hello, Fred!')
113
+
114
+ Instead, use dynamic assertions in the form of ``has_<name>()`` where ``<name>`` is the name of
115
+ any attribute, property, or zero-argument method on the given object. Dynamic equality
116
+ assertions test if actual is equal to expected using the ``==`` operator. Using dynamic
117
+ assertions, we can rewrite the above example as:
118
+
119
+ assert_that(fred).has_first_name('Fred')
120
+ assert_that(fred).has_name('Fred Smith')
121
+ assert_that(fred).has_say_hello('Hello, Fred!')
122
+
123
+ Similarly, dynamic assertions also work on any *dict-like* object:
124
+
125
+ fred = {
126
+ 'first_name': 'Fred',
127
+ 'last_name': 'Smith',
128
+ 'shoe_size': 12
129
+ }
130
+
131
+ assert_that(fred).has_first_name('Fred')
132
+ assert_that(fred).has_last_name('Smith')
133
+ assert_that(fred).has_shoe_size(12)
134
+
135
+ A name the value does not have fails with ``not_`` as well: negation inverts the comparison, never
136
+ whether there is a field to compare, so ``not_.has_frist_name('Joe')`` does not pass on a typo.
137
+ """
138
+
139
+ def __getattr__(self, attr: str) -> Any:
140
+ """Asserts that val has attribute attr and that its value is equal to other via a dynamic
141
+ assertion of the form ``has_<attr>()``.
142
+
143
+ The return stays ``Any`` on purpose, and the reason is written down because the annotation looks
144
+ like an oversight worth fixing. Two unrelated things resolve through this hook: a ``has_``
145
+ wrapper, which takes one argument and hands back the builder, and any name registered with
146
+ `add_extension`, which takes and returns whatever it declares. No single signature is true of
147
+ both. Pinning the argument list reported a correct zero-argument extension call as too few
148
+ arguments, and pinning the return type reported a correct extension result as the wrong type
149
+ where it was used. Both were measured against mypy, pyright and ty.
150
+
151
+ The narrower annotation also bought less than it looked like. It could not catch an assertion
152
+ chained onto a dynamic step, because that access resolves back through this same hook; the only
153
+ thing it added was a verdict in assignment position, which is where it was wrong for extensions.
154
+ """
155
+ if not attr.startswith("has_"):
156
+ raise AttributeError(f"assertpy has no assertion <{attr}()>")
157
+
158
+ attr_name = attr[4:]
159
+ err_msg: str | None = None
160
+ val_is_namedtuple = is_namedtuple(self.val)
161
+ # a django response reads its headers by key, yet `has_status_code()` means the attribute, as on every client
162
+ is_dict = (
163
+ isinstance(self.val, collections.abc.Iterable)
164
+ and hasattr(self.val, "__getitem__")
165
+ and response_of(self.val) is None
166
+ )
167
+
168
+ if is_dict and not val_is_namedtuple:
169
+ # dict-likes are read by key below, so a real method absent as a key would skip this gate and raise KeyError
170
+ if attr_name not in self.val:
171
+ err_msg = f"Expected key <{attr_name}>, but val has no key <{attr_name}>."
172
+ elif not hasattr(self.val, attr_name):
173
+ err_msg = f"Expected attribute <{attr_name}>, but val has no attribute <{attr_name}>."
174
+
175
+ def _wrapper(*args, **kwargs):
176
+ # named here rather than read off the stack: the operation is the attribute, not this closure
177
+ asked = Requirement(attr, _one_operand(args, kwargs))
178
+ if err_msg:
179
+ # ok to raise now that we are inside wrapper
180
+ near = _near(attr_name, self.val, keyed=is_dict and not val_is_namedtuple)
181
+ self._unmet(err_msg + near, expected=asked.parameters.get("other", MISSING), requirement=asked)
182
+ return self
183
+ else:
184
+ try:
185
+ # bound rather than counted: the signature says `other`, and writing it was refused
186
+ bound = _ONE_OPERAND.bind(*args, **kwargs)
187
+ except TypeError:
188
+ given = len(args) + len(kwargs)
189
+ raise TypeError(f"assertion <{attr}()> takes exactly 1 argument ({given} given)") from None
190
+
191
+ val_attr = self.val[attr_name] if is_dict and not val_is_namedtuple else getattr(self.val, attr_name)
192
+
193
+ if callable(val_attr):
194
+ try:
195
+ inspect.signature(val_attr).bind()
196
+ except TypeError: # the method needs arguments, so it is not a zero-arg method
197
+ raise TypeError(f"val does not have zero-arg method <{attr_name}()>") from None
198
+ except ValueError: # some builtins expose no introspectable signature; just call it
199
+ pass
200
+ actual = val_attr() # a TypeError from here comes from the method body, not arity
201
+ else:
202
+ actual = val_attr
203
+
204
+ expected = bound.arguments["other"]
205
+ if actual == expected:
206
+ return self
207
+ kind = "key" if is_dict else "attribute"
208
+ actual_text, expected_text, diff = _as_equality_fails(self, actual, expected)
209
+ self._compared = (actual, expected)
210
+ return self.error(
211
+ f"Expected <{actual_text}> to be equal to <{expected_text}> on {kind} <{attr_name}>, but was not.",
212
+ actual=actual,
213
+ expected=expected,
214
+ diff=diff,
215
+ requirement=asked,
216
+ )
217
+
218
+ # the arity check above is the wrapper's own, so its signature says what a caller writes and
219
+ # `Requirement.parameters` reads the same key whether the call was negated or not. Through
220
+ # `setattr` because typeshed declares no `__signature__` on a function, which both gates report
221
+ setattr(_wrapper, "__signature__", _ONE_OPERAND) # noqa: B010 - see above
222
+ return _wrapper