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
@@ -0,0 +1,729 @@
1
+ """Runtime-checkable protocols for duck-typed object introspection.
2
+
3
+ The comparison and diff code accepts arbitrary user objects, so it inspects them structurally: pydantic
4
+ models (``model_dump``), ``attrs`` classes (``__attrs_attrs__``) and namedtuples (``_fields`` /
5
+ ``_asdict``). Expressing those shapes as ``@runtime_checkable`` protocols lets the diff code use
6
+ ``isinstance`` checks the type checker can follow - narrowing the value instead of probing it with
7
+ ``hasattr`` and then suppressing the attribute access.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import collections
13
+ import collections.abc
14
+ import dataclasses
15
+ import datetime
16
+ import enum
17
+ import itertools
18
+ import numbers
19
+ import os
20
+ import pathlib
21
+ import reprlib
22
+ import sys
23
+ import types
24
+ import uuid
25
+ from typing import TYPE_CHECKING, Any, Final, Protocol, TypeGuard, TypeVar, cast, runtime_checkable
26
+
27
+ if TYPE_CHECKING:
28
+ from collections.abc import Callable, Iterable, Iterator
29
+ from types import CellType
30
+
31
+ _T = TypeVar("_T")
32
+
33
+ # absolute paths in a set: one hash lookup a frame, and a user file of the same name cannot shadow ours
34
+ ASSERTPY_FILES: Final = frozenset(
35
+ os.path.join(os.path.dirname(os.path.dirname(__file__)), name)
36
+ for name in os.listdir(os.path.dirname(os.path.dirname(__file__)))
37
+ if name.endswith(".py")
38
+ )
39
+
40
+
41
+ def caller_stacklevel() -> int:
42
+ """The ``stacklevel`` that points a warning its caller raises at the first frame outside the package.
43
+
44
+ Counted, not fixed: a builder a `log_passes()` block made runs each assertion through frames of its own, and a
45
+ fixed count pointed into them.
46
+ """
47
+ frame = sys._getframe(1) # the caller, which `warnings.warn(stacklevel=1)` names
48
+ level = 1
49
+ while frame is not None and frame.f_code.co_filename in ASSERTPY_FILES:
50
+ frame = frame.f_back
51
+ level += 1
52
+ return level
53
+
54
+
55
+ # exact builtins carrying no `__dict__`, so the isinstance is skipped; a subclass takes the structural check
56
+ _ATOMIC_TYPES: Final = frozenset(
57
+ {type(None), bool, int, float, complex, str, bytes, bytearray, list, tuple, set, frozenset}
58
+ )
59
+
60
+
61
+ @runtime_checkable
62
+ class SupportsModelDump(Protocol):
63
+ """A pydantic-style model exposing ``model_dump()``."""
64
+
65
+ def model_dump(self) -> dict[str, Any]: ...
66
+
67
+
68
+ @runtime_checkable
69
+ class NamedTupleLike(Protocol):
70
+ """A ``collections.namedtuple`` / ``typing.NamedTuple`` instance."""
71
+
72
+ _fields: tuple[str, ...]
73
+
74
+ def _asdict(self) -> dict[str, Any]: ...
75
+
76
+
77
+ @runtime_checkable
78
+ class AttrsInstance(Protocol):
79
+ """An ``attrs``-decorated instance exposing ``__attrs_attrs__``."""
80
+
81
+ __attrs_attrs__: tuple[Any, ...]
82
+
83
+
84
+ class WarningLogger(Protocol):
85
+ """What a warning-mode assertion asks of a logger: the one method it calls when an assertion fails.
86
+
87
+ Structural rather than either concrete type, because both are promised and both work. The
88
+ docstring of `assert_warn` says `Logger`, the default the builder installs is a `LoggerAdapter`,
89
+ and naming one of them in the signature refuses the other for no reason the runtime shares.
90
+
91
+ Widest on both sides. `str` because a logger declared to take only a string is a working one and
92
+ the message here is always a string, and `object` because a return this never reads is not a
93
+ reason to refuse a logger that has one.
94
+ """
95
+
96
+ def warning(self, msg: str) -> object: ...
97
+
98
+
99
+ @runtime_checkable
100
+ class Readable(Protocol):
101
+ """A file-like object, recognised by the one method that reads it whole.
102
+
103
+ The return is spelled out because the caller promises a `str`: a text handle answers with one and a
104
+ binary handle with `bytes`, which is decoded, and a reader that answers with neither would be
105
+ handed straight back through a signature that says `str`.
106
+ """
107
+
108
+ def read(self) -> str | bytes: ...
109
+
110
+
111
+ @runtime_checkable
112
+ class MappingLike(Protocol):
113
+ """A dict-like object that can be iterated over and subscripted by key."""
114
+
115
+ def keys(self) -> Any: ...
116
+
117
+ def __iter__(self) -> Any: ...
118
+
119
+ def __getitem__(self, key: Any, /) -> Any: ... # positional, so a plain `dict` satisfies it
120
+
121
+
122
+ def is_model_dump_object(obj: object) -> TypeGuard[SupportsModelDump]:
123
+ """Return whether ``obj``'s type exposes a callable ``model_dump()`` (e.g. a pydantic model)."""
124
+ if type(obj) is dict or type(obj) in _ATOMIC_TYPES:
125
+ return False
126
+ # on the type first, which a `unittest.mock` object's fabricated attribute never reaches; then on the
127
+ # value, because an ordinary method is a non-data descriptor and `obj.model_dump = None` shadows it
128
+ return hasattr(type(obj), "model_dump") and callable(getattr(obj, "model_dump", None))
129
+
130
+
131
+ def is_pydantic_model(obj: object) -> TypeGuard[SupportsModelDump]:
132
+ """Whether *obj* is an instance of pydantic's ``BaseModel``, asked without importing pydantic.
133
+
134
+ No model exists unless pydantic is loaded already, so an absent module answers no.
135
+ """
136
+ pydantic = sys.modules.get("pydantic")
137
+ return pydantic is not None and isinstance(obj, pydantic.BaseModel)
138
+
139
+
140
+ def is_pydantic_model_class(candidate: object) -> bool:
141
+ """Whether *candidate* is a subclass of pydantic's ``BaseModel``, asked without importing pydantic."""
142
+ pydantic = sys.modules.get("pydantic")
143
+ return pydantic is not None and isinstance(candidate, type) and issubclass(candidate, pydantic.BaseModel)
144
+
145
+
146
+ class TakenApart(dict):
147
+ """The fields of one value as it holds them, with its class and the keys attrs compares fields through.
148
+
149
+ Read as a plain mapping, a nested value of another class with the same fields passed a comparison
150
+ under ``strict_types`` and ``ignore`` together, which the rule says holds at every depth. A field
151
+ declared with an ``eq=`` key is compared through it against the same field of another instance, as
152
+ attrs' own ``==`` does, and as held against anything else, a payload's value included.
153
+
154
+ Printed as the class it was read from, ``Row(name='a')`` for a record and ``Kept({'x': 1})`` for a dict of
155
+ a class of its own, so a failure names what was compared.
156
+ """
157
+
158
+ __slots__ = ("compared_by", "kind")
159
+
160
+ def __init__(
161
+ self,
162
+ kind: type,
163
+ fields: collections.abc.Mapping,
164
+ compared_by: collections.abc.Mapping[object, Callable[[object], object]] | None = None,
165
+ ) -> None:
166
+ super().__init__(fields)
167
+ self.kind = kind
168
+ self.compared_by = compared_by or {}
169
+
170
+ @property
171
+ def keyed(self) -> bool:
172
+ """Whether the class this was read from is a dict's, whose parts are keys rather than fields."""
173
+ return issubclass(self.kind, dict)
174
+
175
+ @reprlib.recursive_repr()
176
+ def __repr__(self) -> str:
177
+ if self.keyed:
178
+ return f"{class_name(self.kind)}({dict.__repr__(self)})"
179
+ fields = ", ".join(f"{field}={value!r}" for field, value in self.items() if not self._says_its_class(field))
180
+ return f"{class_name(self.kind)}({fields})"
181
+
182
+ def _says_its_class(self, field: object) -> bool:
183
+ """Whether *field* is the class an exception is read with, which the name ahead of the bracket has said."""
184
+ return field == "__class__" and self[field] is self.kind and issubclass(self.kind, BaseException)
185
+
186
+ def __eq__(self, other: object) -> bool:
187
+ if not isinstance(other, dict):
188
+ return NotImplemented
189
+ if not self.compared_by and not (isinstance(other, TakenApart) and other.compared_by):
190
+ return dict.__eq__(self, other)
191
+ keyed = keyed_names(self, other)
192
+ pairs = (keyed_pair(self, other, name) if name in keyed else (self[name], other[name]) for name in self)
193
+ return self.keys() == other.keys() and all(left is right or bool(left == right) for left, right in pairs)
194
+
195
+
196
+ def laid_out(value: object) -> object:
197
+ """*value* with every record taken apart turned into a plain dict of its fields, for a reader that lays
198
+ containers out and prints anything else on one line.
199
+
200
+ Followed through the values of a ``dict`` and the items of a ``list`` or a ``tuple``, each of exactly that
201
+ class, told by identity so that no class is asked. A mapping is turned only where every key in it is one
202
+ the interpreter hashes and compares by code of its own: exactly a ``str``, an ``int``, a ``bool``, a
203
+ ``float``, a ``bytes`` or ``None``. Putting a value back looks its key up, and beside a key of any other
204
+ class that runs the class's ``__hash__`` or ``__eq__``, so such a mapping stays as it is, a record among
205
+ them. A dict with nothing to turn comes back as the same object. A part two places share is turned once,
206
+ and a value that holds itself is left where the walk meets it again.
207
+ """
208
+ return _laid_out(value, {}, set())
209
+
210
+
211
+ def plainly_hashed(key: object) -> bool:
212
+ """Whether *key* is exactly one of the six plain kinds, whose own hash and equality are the interpreter's.
213
+
214
+ A lookup runs no other code only where every key the dict holds is one too: a key of another class that
215
+ hashes the same is asked its own ``__eq__``.
216
+ """
217
+ kind = type(key)
218
+ return kind is str or kind is int or kind is bool or kind is float or kind is bytes or key is None
219
+
220
+
221
+ def _laid_out(value: object, done: dict[int, object], above: set[int]) -> object:
222
+ kind = type(value)
223
+ if not (kind is TakenApart or kind is dict or kind is list or kind is tuple):
224
+ return value
225
+ mark = id(value)
226
+ if mark in above:
227
+ return value
228
+ if mark in done:
229
+ return done[mark]
230
+ above.add(mark)
231
+ made: object = value
232
+ if kind is list or kind is tuple:
233
+ items = cast("list[object] | tuple[object, ...]", value)
234
+ parts = [_laid_out(part, done, above) for part in items]
235
+ if any(new is not old for new, old in zip(parts, items, strict=True)):
236
+ made = kind(parts)
237
+ else:
238
+ held = list(dict.items(cast("dict[object, object]", value)))
239
+ if all(plainly_hashed(key) for key, _ in held):
240
+ # a copy keeps the hashes it was made with, and putting a value back compares plain keys alone
241
+ fields = dict.copy(cast("dict[object, object]", value))
242
+ for key, old in held:
243
+ if (new := _laid_out(old, done, above)) is not old:
244
+ fields[key] = new
245
+ if kind is TakenApart or any(fields[key] is not old for key, old in held):
246
+ made = fields
247
+ above.discard(mark)
248
+ done[mark] = made
249
+ return made
250
+
251
+
252
+ _PART_READERS = tuple(
253
+ type.__getattribute__(kind, "__dict__")["__eq__"]
254
+ for kind in (
255
+ list,
256
+ tuple,
257
+ dict,
258
+ collections.OrderedDict,
259
+ collections.abc.Mapping,
260
+ types.MappingProxyType,
261
+ TakenApart,
262
+ )
263
+ )
264
+ """The ``__eq__`` of each kind whose values are equal exactly as what they hold is."""
265
+
266
+
267
+ class KeyedValue:
268
+ """One of two fields declared with an ``eq=`` key, for the one comparison of them: equal when the keys are.
269
+
270
+ Built for the comparison and never kept, so what a failure hands out and prints is the value held.
271
+ """
272
+
273
+ __slots__ = ("held", "key")
274
+
275
+ def __init__(self, held: object, key: Callable[[object], object]) -> None:
276
+ self.held = held
277
+ self.key = key
278
+
279
+ def __eq__(self, other: object) -> bool:
280
+ # read at the comparison: `ignore_null` looks at what is held first, and `str.lower(None)` raises
281
+ return self.key(self.held) == other.key(other.held) if isinstance(other, KeyedValue) else NotImplemented
282
+
283
+
284
+ def keyed_names(fields: object, counterpart: object) -> collections.abc.Set[object]:
285
+ """The fields two sides both compare through an attrs key, and none unless both are a `TakenApart`.
286
+
287
+ Asked once for a pair of mappings rather than once per field: asked per field, a 200-key dict diff
288
+ took 20% longer, on dicts that hold no key at all.
289
+ """
290
+ if type(fields) is TakenApart and type(counterpart) is TakenApart:
291
+ return fields.compared_by.keys() & counterpart.compared_by.keys()
292
+ return frozenset()
293
+
294
+
295
+ def keyed_pair(fields: Any, counterpart: Any, name: object) -> tuple[KeyedValue, KeyedValue]:
296
+ """Field *name* of two sides, each read through its own key, for a name `keyed_names` answered."""
297
+ return (
298
+ KeyedValue(fields[name], fields.compared_by[name]),
299
+ KeyedValue(counterpart[name], counterpart.compared_by[name]),
300
+ )
301
+
302
+
303
+ def class_name(kind: type) -> str:
304
+ """The name of a class as an exact `str`, read off the slot: a metaclass can make the attribute raise."""
305
+ return str.__str__(type.__dict__["__name__"].__get__(kind))
306
+
307
+
308
+ def holds_only_its_dict(kind: type) -> bool:
309
+ """Whether an instance of *kind* holds nothing but its ``__dict__``, read off the layout of its class.
310
+
311
+ A slot, the items of a builtin container and the fields of a type written in C are all room in the instance
312
+ past the object's own header, its ``__dict__`` and its weak reference list. A list of kinds was the first
313
+ answer, and each one found missing was a comparison that passed: `list`, `array.array`, an exception.
314
+ """
315
+ spare = basic_size(kind) - object.__basicsize__
316
+ spare -= tuple.__itemsize__ * ((_dict_offset(kind) > 0) + (weakref_offset(kind) > 0))
317
+ return spare == 0
318
+
319
+
320
+ # off the slots of `type`: a metaclass may spell a size or an offset of its own
321
+ basic_size, _dict_offset, weakref_offset = (
322
+ type.__dict__[name].__get__ for name in ("__basicsize__", "__dictoffset__", "__weakrefoffset__")
323
+ )
324
+
325
+
326
+ def own_fields(obj: object) -> dict[str, Any] | None:
327
+ """The ``__dict__`` of an object that holds nothing else, or ``None`` for a value that is no bag of fields.
328
+
329
+ A value of a builtin kind is none even when it carries a ``__dict__``: a subclass of `Decimal` or `str` has
330
+ an empty one, and reading it made every two such values compare equal. Nor is any value that holds
331
+ something outside its ``__dict__`` (`holds_only_its_dict`): two lists of a class of the caller's own were
332
+ equal under ``ignore=`` whatever they held.
333
+ """
334
+ builtin_kinds = (
335
+ type,
336
+ numbers.Number,
337
+ str,
338
+ bytes,
339
+ bytearray,
340
+ datetime.date,
341
+ datetime.time,
342
+ datetime.timedelta,
343
+ enum.Enum,
344
+ uuid.UUID,
345
+ pathlib.PurePath,
346
+ )
347
+ if hasattr(obj, "__dict__") and not isinstance(obj, builtin_kinds) and holds_only_its_dict(type(obj)):
348
+ return vars(obj)
349
+ return None
350
+
351
+
352
+ def kind_of(value: object) -> type:
353
+ """The class *value* was read from: its own, the one a `TakenApart` holds the fields of, or a keyed field's."""
354
+ if type(value) is KeyedValue:
355
+ return type(value.held)
356
+ # by the type itself: `isinstance` reads a ``__class__`` the value may answer with code of its own
357
+ return value.kind if type(value) is TakenApart else type(value)
358
+
359
+
360
+ def eq_keyed(attribute: Any, value: object) -> object:
361
+ """An attrs field's value as its ``==`` compares it: through the key given as ``eq=``, when there is one."""
362
+ key = getattr(attribute, "eq_key", None)
363
+ return value if key is None else key(value)
364
+
365
+
366
+ def as_held(value: object) -> object:
367
+ """*value* as the instance held it, past the `KeyedValue` one comparison reads it through."""
368
+ return value.held if type(value) is KeyedValue else value
369
+
370
+
371
+ def model_field_values(model: SupportsModelDump) -> dict[Any, Any]:
372
+ """A model's declared fields and extras as the values they hold, not what serialising makes of them.
373
+
374
+ `model_dump()` runs every `field_serializer`, leaves out a field declared ``exclude=True`` and adds the
375
+ computed ones, and pydantic's ``==`` does none of that. Read through it, `M(f=1)` and `M(f="1")`
376
+ compared equal under ``ignore=``, ``include=``, ``strict_types=`` and ``tolerance=`` while ``==`` told
377
+ them apart, and the structured diff showed serialised values beside a message showing the held ones.
378
+
379
+ Only a pydantic ``BaseModel`` is read this way. A duck type may carry a ``model_fields`` of its own
380
+ meaning, pydantic's own attribute names, or no ``__dict__`` at all, and is read through `model_dump()`,
381
+ which is all the duck type promises.
382
+
383
+ What pydantic's ``==`` compares beyond these is not read: the model's class, which a configured
384
+ comparison leaves to ``strict_types``, and private attributes, so a pair differing only there fails
385
+ plainly and passes a configured comparison, as it did through the dump.
386
+
387
+ An extra named like a declared field is refused. Validation never produces one, and the model then
388
+ holds two values under one name, which no reading field by field can show: `model_dump()` keeps the
389
+ extra's, ``==`` compares both.
390
+ """
391
+ if not is_pydantic_model(model):
392
+ return model.model_dump()
393
+ # `Any` because pydantic comes off `sys.modules`, and `model_fields` because `__pydantic_fields__` is 2.10+
394
+ model_class: Any = type(model)
395
+ declared = model_class.model_fields
396
+ held = vars(model)
397
+ values = {name: held[name] for name in declared if name in held}
398
+ extra = getattr(model, "__pydantic_extra__", None)
399
+ if isinstance(extra, collections.abc.Mapping):
400
+ shadowing = next((name for name in extra if name in declared), None)
401
+ if shadowing is not None:
402
+ raise TypeError(
403
+ f"{type(model).__name__} holds an extra named {shadowing!r} beside the field of that name,"
404
+ " which validation never produces, so its fields cannot be read one by one"
405
+ )
406
+ values.update(extra)
407
+ return values
408
+
409
+
410
+ def is_namedtuple(obj: object) -> TypeGuard[NamedTupleLike]:
411
+ """Return whether ``obj`` is a namedtuple instance (a ``tuple`` whose type carries the whole surface).
412
+
413
+ Both ``_fields`` and ``_asdict``, because the callers use both: a ``tuple`` subclass with a bare
414
+ ``_fields`` is not one, and asking for only that let it through.
415
+
416
+ Asked of the type and not the instance, as its two neighbours are and for the same reason. A
417
+ ``unittest.mock`` object fabricates every attribute through ``__getattr__``, so it answered all three
418
+ of these and the diff walk read a call's own elements as field names, then as keys. Its class
419
+ carries none of them.
420
+
421
+ Cheaper as well as narrower: a ``runtime_checkable`` protocol was measured here at 876 ns against
422
+ 41 ns for the attribute, on the walk that asks this about every differing element.
423
+ """
424
+ return isinstance(obj, tuple) and hasattr(type(obj), "_fields") and hasattr(type(obj), "_asdict")
425
+
426
+
427
+ def is_attrs_instance(obj: object) -> TypeGuard[AttrsInstance]:
428
+ """Return whether ``obj`` is an ``attrs``-decorated instance (not the class itself, which also
429
+ carries ``__attrs_attrs__`` but has no field values to read).
430
+
431
+ The atomic fast path is the same one its two neighbours above have, and it was the one predicate
432
+ missing it. That mattered out of proportion to the line: the diff walk asks this about every
433
+ differing element, and a profile of a 2000-element sequence diff put 39% of the whole walk inside
434
+ the ``runtime_checkable`` isinstance this skips.
435
+ """
436
+ if type(obj) in _ATOMIC_TYPES:
437
+ return False
438
+ return not isinstance(obj, type) and hasattr(type(obj), "__attrs_attrs__")
439
+
440
+
441
+ def is_mapping_like(obj: object) -> TypeGuard[MappingLike]:
442
+ """Return whether ``obj`` is dict-like: iterable with ``keys()`` and ``[]`` access."""
443
+ if type(obj) is dict:
444
+ return True
445
+ if type(obj) in _ATOMIC_TYPES:
446
+ return False
447
+ # read with a default: a value may say it is a dict through `__class__` and have no `keys` at all
448
+ return isinstance(obj, MappingLike) and callable(getattr(obj, "keys", None))
449
+
450
+
451
+ def keyed_snapshot(candidate: object) -> MappingLike | None:
452
+ """*candidate* as something safe to walk by key, or `None` when it cannot be walked that way.
453
+
454
+ Iteration yields keys for a mapping and values for a sequence, and no test of the type tells which a
455
+ duck-typed value does. `unittest.mock.call_args` is a tuple subclass answering `keys`, so a renderer
456
+ walked it for keys, got its items and indexed the tuple with one.
457
+
458
+ A snapshot rather than a yes-or-no answer, because a probe and the walk that follows it are two
459
+ readings of the same value: an iterator that yields `0` on the first pass and `"bad"` on the second
460
+ passes the probe and raises in the walk. Reading once and rendering from what was read closes that.
461
+
462
+ Only a `dict` is handed back as it is, and a `TakenApart`, read once already and carrying the class
463
+ ``strict_types`` compares. Registering as a `Mapping` promises an interface and not stability between
464
+ reads, so a custom one is snapshotted like anything else.
465
+ """
466
+ if type(candidate) is dict or type(candidate) is TakenApart:
467
+ return cast("MappingLike", candidate)
468
+ keyed = cast("MappingLike", candidate)
469
+ try:
470
+ # an OrderedDict stays one, since its `==` reads the order a plain dict would forget
471
+ snapshot = collections.OrderedDict() if isinstance(candidate, collections.OrderedDict) else {}
472
+ snapshot.update((key, keyed[key]) for key in keyed)
473
+ except Exception:
474
+ return None
475
+ return snapshot
476
+
477
+
478
+ def is_same_implementation(existing: object, candidate: object) -> bool:
479
+ """Whether two registrations are the same implementation rather than two claiming one name.
480
+
481
+ Object identity is too strict. A ``def`` inside a pytest fixture builds a new function object
482
+ every time the fixture runs, and a module-scoped fixture in ``conftest.py`` is the documented way
483
+ to share an extension across test files, so that shape would otherwise look like a clash on the
484
+ second module. CPython reuses the enclosing code object across those rebuilds, and it differs the
485
+ moment the body does, which is exactly the line wanted here.
486
+
487
+ Callables without a ``__code__`` (instances with ``__call__``, builtins, partials) fall back to
488
+ identity, which is the strictest answer available for them.
489
+
490
+ A decorator's wrapper shares its code with everything that decorator wraps, so the functions a
491
+ wrapper holds, in its closure, its defaults or its ``__wrapped__``, are compared the same way: two
492
+ wrappers are one implementation only if what they wrap is. Anything else they hold is data and is
493
+ not compared, a callable without code (a builtin, a partial, a mock) included. A fixture rebuilding
494
+ its function carries fresh data every time, and the latest registration replacing the earlier one
495
+ is the point of accepting it.
496
+ """
497
+ return _same_implementation(existing, candidate, set())
498
+
499
+
500
+ def _same_implementation(existing: object, candidate: object, seen: set[tuple[int, int]]) -> bool:
501
+ if existing is candidate:
502
+ return True
503
+ left = getattr(existing, "__code__", None)
504
+ if left is None or left is not getattr(candidate, "__code__", None):
505
+ return False
506
+ # a closure that holds its own function reaches this pair again
507
+ if (id(existing), id(candidate)) in seen:
508
+ return True
509
+ seen.add((id(existing), id(candidate)))
510
+ return all(
511
+ _same_implementation(inner, other, seen)
512
+ for inner, other in _held(existing, candidate)
513
+ if isinstance(inner, types.FunctionType) or isinstance(other, types.FunctionType)
514
+ )
515
+
516
+
517
+ def _held(existing: object, candidate: object) -> Iterator[tuple[object, object]]:
518
+ """What two functions of one code object hold, side by side: closure cells, defaults, what they wrap."""
519
+ # one code object, so one set of free variables on both sides
520
+ cells = zip(
521
+ getattr(existing, "__closure__", None) or (), getattr(candidate, "__closure__", None) or (), strict=True
522
+ )
523
+ yield from ((_cell_contents(mine), _cell_contents(theirs)) for mine, theirs in cells)
524
+ yield from itertools.zip_longest(
525
+ getattr(existing, "__defaults__", None) or (), getattr(candidate, "__defaults__", None) or ()
526
+ )
527
+ mine, theirs = getattr(existing, "__kwdefaults__", None) or {}, getattr(candidate, "__kwdefaults__", None) or {}
528
+ yield from ((mine.get(name), theirs.get(name)) for name in mine.keys() | theirs.keys())
529
+ yield getattr(existing, "__wrapped__", None), getattr(candidate, "__wrapped__", None)
530
+
531
+
532
+ def _cell_contents(cell: CellType) -> object:
533
+ """What a closure cell holds, or ``None`` for one not yet filled."""
534
+ try:
535
+ return cell.cell_contents
536
+ except ValueError:
537
+ return None
538
+
539
+
540
+ def is_own_iterator(value: object) -> bool:
541
+ """Whether *value* is an iterator, so the first pass over it is the only one there is.
542
+
543
+ Asked of its type rather than through `iter()`: this is asked while a failure is being described,
544
+ and a closed file answers `iter()` with `ValueError`.
545
+ """
546
+ return issubclass(type(value), collections.abc.Iterator)
547
+
548
+
549
+ class Replay:
550
+ """What a one-shot iterator has handed out so far, with the iterator for the rest.
551
+
552
+ A builder keeps one for a value that is its own iterator, so every link of a chain reads the same items:
553
+ read by the first link alone, the second passed or failed over nothing. Each walk starts at the first item
554
+ and takes one from the iterator only past what was handed out, so a walk that stops early leaves the rest
555
+ untaken and an endless generator is read no further than before. An item is kept before it is handed on:
556
+ code of the caller's that raises over it does not lose it for the next link. An error the iterator
557
+ itself raises is its own to repeat or not: asked again, a generator that raised is at its end, and an
558
+ iterator that goes on is read on.
559
+ """
560
+
561
+ __slots__ = ("_rest", "_seen", "given_away", "source")
562
+
563
+ def __init__(self, source: Iterator[Any]) -> None:
564
+ self.source = source
565
+ self._rest: Iterator[Any] | None = source
566
+ self._seen: list[Any] = []
567
+ self.given_away = False
568
+
569
+ def give_away(self) -> None:
570
+ """Note that code of the caller's is handed the iterator itself after a part of it was read here.
571
+
572
+ What that code takes of the rest then goes missing between what was read and what is left, and stays
573
+ missing: once noted, it is not taken back. With nothing read yet, or all of it, there is no such gap.
574
+ A walk still going when it is noted is refused at its next item past what was read, as the next link is.
575
+ """
576
+ if self._rest is not None and self._seen:
577
+ self.given_away = True
578
+
579
+ def drained(self) -> list[Any]:
580
+ """Every item in a list of its own, the rest taken in one go: through `__iter__`, 200 cost 7 times more."""
581
+ rest = self._rest
582
+ if rest is not None:
583
+ self._seen.extend(rest)
584
+ self._rest = None
585
+ return list(self._seen)
586
+
587
+ def __iter__(self) -> Iterator[Any]:
588
+ seen = self._seen
589
+ index = 0
590
+ while True:
591
+ if index == len(seen):
592
+ rest = self._rest
593
+ if rest is None:
594
+ return
595
+ if self.given_away:
596
+ raise handed_away()
597
+ try:
598
+ seen.append(next(rest))
599
+ except StopIteration:
600
+ self._rest = None
601
+ return
602
+ yield seen[index]
603
+ index += 1
604
+
605
+
606
+ def handed_away() -> TypeError:
607
+ """The refusal of a read of a one-shot iterator past the part read before a predicate was handed it."""
608
+ return TypeError(
609
+ "val is a one-shot iterator that a predicate was handed after a part of it was read, so what"
610
+ " is left of it cannot be told. Assert on list(...) of it instead"
611
+ )
612
+
613
+
614
+ def materialized(value: Iterable[_T]) -> Iterable[_T]:
615
+ """``value``, drained into a list when it is a one-shot iterator, handed back untouched otherwise.
616
+
617
+ An object that is its own iterator (a generator, a file, ``map``/``filter``/``zip``) is consumed by
618
+ the first pass over it. Assertions that walk their subject more than once - once to decide and
619
+ again to describe, or once per argument - therefore see an empty value on the second pass, and
620
+ either report a wrong verdict or a message with the wrong contents in it.
621
+
622
+ Re-iterable values are returned as they are, so nothing is copied on the ordinary path. A value
623
+ that cannot be iterated at all is also returned as it is, leaving the caller's own guard to produce
624
+ the error message it wants.
625
+
626
+ Being its own iterator is not the only way to have one position: a wrapper handing out one shared
627
+ iterator answers no here and still loses what a first walk read, so ``satisfies()`` over one
628
+ reports the remainder rather than the value. That is left uncaught on purpose. Copying anything
629
+ that merely lacks a length instead turns a Pydantic model into a list of its field pairs, and
630
+ walking one to find out costs the position when the position is shared.
631
+ """
632
+ try:
633
+ return list(value) if iter(value) is value else value
634
+ except TypeError:
635
+ return value
636
+
637
+
638
+ def compares_by_parts(kind: type) -> bool:
639
+ """Whether the ``==`` of *kind* is one known to read the parts a walk takes its values apart into.
640
+
641
+ Known by being the thing itself, never by where it says it came from: the method a builtin container, `Mapping`
642
+ or pydantic's ``BaseModel`` defines, or code that is what `dataclasses` or attrs writes for the fields the class
643
+ has (`_written_again`). Any other ``==`` may hold two values apart for a reason their parts do not show.
644
+
645
+ Kept once found, for at most 256 classes, with the ``__eq__`` it was found for and the code that ran: building
646
+ the twin cost a ring of a hundred records 15 ms, and a class given another ``__eq__``, or one given other code,
647
+ is asked again.
648
+ """
649
+ owner, written = definition_of(kind, "__eq__") or (object, None)
650
+ ran = getattr(written, "__code__", None)
651
+ if kind in _COMPARED_BY_PARTS and _COMPARED_BY_PARTS[kind] == (id(written), id(ran)):
652
+ return True
653
+ pydantic = sys.modules.get("pydantic")
654
+ known = (
655
+ any(written is reader for reader in _PART_READERS)
656
+ or (pydantic is not None and written is type.__getattribute__(pydantic.BaseModel, "__dict__").get("__eq__"))
657
+ or _written_again(owner, written)
658
+ )
659
+ if known and len(_COMPARED_BY_PARTS) < 256:
660
+ _COMPARED_BY_PARTS[kind] = (id(written), id(ran))
661
+ return known
662
+
663
+
664
+ _COMPARED_BY_PARTS: dict[type, tuple[int, int]] = {}
665
+ """What `compares_by_parts` has found: a class, and which ``__eq__`` and which code of it the answer was for. The
666
+ class holds both, so neither id is taken by another object while the class is here."""
667
+
668
+
669
+ def _written_again(owner: type, written: object) -> bool:
670
+ """Whether *written*, the ``__eq__`` *owner* defines, is the code `dataclasses` or attrs writes for its fields.
671
+
672
+ Asked by having the same library write one for a twin with the same fields and comparing the two code objects.
673
+ A file name would not do: the method `dataclasses` writes names none, and neither does one put there by ``exec``.
674
+ Only a plain function is read: any other object can carry a ``__code__`` it does not run. The keys attrs
675
+ compares fields through are globals of the function it writes, so those are held to being the same objects.
676
+ """
677
+ namespace = type.__getattribute__(owner, "__dict__")
678
+ attr = sys.modules.get("attr")
679
+ if type(written) is not types.FunctionType:
680
+ return False
681
+ if "__dataclass_fields__" in namespace:
682
+ fields = dataclasses.fields(cast("Any", owner))
683
+ twin = dataclasses.dataclass(
684
+ type(
685
+ "twin",
686
+ (),
687
+ {
688
+ "__annotations__": dict.fromkeys((field.name for field in fields), object),
689
+ **{field.name: dataclasses.field(default=None, compare=field.compare) for field in fields},
690
+ },
691
+ )
692
+ )
693
+ elif "__attrs_attrs__" in namespace and attr is not None:
694
+ twin = attr.make_class(
695
+ "twin",
696
+ {
697
+ attribute.name: attr.ib(default=None, eq=attribute.eq if attribute.eq_key is None else attribute.eq_key)
698
+ for attribute in namespace["__attrs_attrs__"]
699
+ },
700
+ )
701
+ else:
702
+ return False
703
+ again = type.__getattribute__(twin, "__dict__")["__eq__"]
704
+ code, ours = written.__code__, again.__code__
705
+ keys = again.__globals__ if "__attrs_attrs__" in namespace else {}
706
+ return (code.co_code, code.co_names, code.co_consts) == (ours.co_code, ours.co_names, ours.co_consts) and all(
707
+ written.__globals__.get(name) is key for name, key in keys.items() if name in ours.co_names
708
+ )
709
+
710
+
711
+ # off the slots of `type`: asked through the class, a metaclass may spell a tree or a namespace of its own
712
+ class_tree, class_namespace = (type.__dict__[name].__get__ for name in ("__mro__", "__dict__"))
713
+
714
+
715
+ def definition_of(klass: type, name: str) -> tuple[type, object] | None:
716
+ """The class in *klass*'s tree that defines *name*, and the definition itself, or ``None``.
717
+
718
+ Walked and read raw, which is the reading the interpreter does for an operator: no attribute access
719
+ on the class, so neither a metaclass nor a descriptor of the class's own gets to answer for it.
720
+
721
+ A definition of ``None`` reads as no definition, so a class declaring `__hash__ = None` reports the
722
+ hashable base above it. Callers that care ask the class about `__hash__` directly, which is the
723
+ cheaper question anyway.
724
+ """
725
+ for base in class_tree(klass):
726
+ found = class_namespace(base).get(name)
727
+ if found is not None:
728
+ return base, found
729
+ return None