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/_hints.py ADDED
@@ -0,0 +1,1213 @@
1
+ """One line naming *why* two values differ, when the whole difference has a single explanation.
2
+
3
+ The diff says what differs, which is where every assertion library stops. It leaves the reader to
4
+ work out why, and there are failures where that is genuinely hard: two strings that render
5
+ identically and differ in a trailing space, a comparison that can never pass because a NaN is in it.
6
+
7
+ A hint is stated only when it accounts for **every** entry in the diff. A partial explanation is
8
+ worse than none, because the reader acts on it and lands back at the same failure. That rule is what
9
+ keeps this from becoming the kind of advice people learn to scroll past, and it is measurable: over a
10
+ corpus of 20000 ordinary failures (a wrong leaf somewhere in a generated structure) the checks below
11
+ stay silent on all of them, and over a set of near misses built to resemble each form without being
12
+ it, likewise.
13
+
14
+ Everything here reads the diff rather than the values. That is not only cheaper, though it is: the
15
+ diff of a failure over 500 records holds one entry, so the cost does not grow with the data. It also
16
+ means a difference nested six levels down is examined exactly like a top-level one, with no separate
17
+ code path.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import dataclasses
23
+ import datetime
24
+ import decimal
25
+ import difflib
26
+ import enum
27
+ import fractions
28
+ import json
29
+ import re
30
+ import sys
31
+ from collections import Counter
32
+ from typing import TYPE_CHECKING, Final, NamedTuple, cast
33
+
34
+ from ._engine._compare import _non_finite
35
+ from ._engine._equality import comparable_fields
36
+ from ._engine._introspection import (
37
+ class_name,
38
+ class_namespace,
39
+ class_tree,
40
+ is_attrs_instance,
41
+ is_mapping_like,
42
+ is_model_dump_object,
43
+ kind_of,
44
+ )
45
+ from ._engine._ordering import equals, nan_operand
46
+ from .errors import _capped, _class_names, _safe_repr, _safe_str
47
+
48
+ if TYPE_CHECKING:
49
+ from collections.abc import Callable, Iterable, Mapping, Sequence
50
+
51
+ from .errors import DiffEntry, DiffResult, Step
52
+
53
+ # a step's wording: fixed, or decided from the shape of the pairs it is describing
54
+ _Label = str | Callable[[Sequence[tuple[object, object]]], str]
55
+
56
+ _OBJECT_EQUALITY: Final = object.__dict__["__eq__"]
57
+ _NAN_FACT = "a NaN takes part in this comparison, and a NaN is equal to nothing, not even itself"
58
+ _IDENTITY_FACT = (
59
+ "these values compare with object's __eq__, so equality is identity and no two separate instances are equal"
60
+ )
61
+
62
+ _UNSEEN_FACT = "a difference here prints the same on both sides, so what holds the two apart is not in their repr"
63
+ _UNSEEN_IDENTITY_FACT = (
64
+ "a difference here prints the same on both sides, and the class of the two leaves __eq__ to object,"
65
+ " so two separate instances are never equal"
66
+ )
67
+ _ORDER_FACT = "both sides hold the same elements, in a different order"
68
+ _UNSEEN_ORDER_FACT = f"{_ORDER_FACT}, and their repr does not show which is which"
69
+ # exact types whose repr differs wherever their values do, so ``==`` answers for the repr at a fraction of its cost
70
+ _PRINTED_AS_HELD: Final = frozenset({int, str, bytes, bool})
71
+ # the plain values a payload spells one way and a test another: an id as text, a flag as a word
72
+ _READ_AS_TEXT: Final = frozenset({int, float, str, bool})
73
+
74
+ _VALUE_KINDS: Final = frozenset(
75
+ {"dict", "sequence", "dataclass", "namedtuple", "model", "attrs", "set", "string", "scalar"}
76
+ )
77
+ """Diff kinds whose entries hold two values compared for equality.
78
+
79
+ Everything here reasons about a pair as two values, so it only applies where both sides are one. A
80
+ ``match`` entry holds the actual value against a *description* of a predicate ("a value equal to
81
+ <guest>"), and an ``openapi`` one against a schema violation. Treating a description as a value is
82
+ not a false positive waiting to happen so much as a category error, and one that would surface as an
83
+ authoritative-sounding line about whitespace in a predicate.
84
+ """
85
+
86
+
87
+ def _newlines(value: object) -> object:
88
+ if isinstance(value, bytes):
89
+ return bytes.replace(value, b"\r\n", b"\n")
90
+ return str.replace(value, "\r\n", "\n") if isinstance(value, str) else value
91
+
92
+
93
+ def _stripped(value: object) -> object:
94
+ if isinstance(value, bytes):
95
+ return bytes.strip(value)
96
+ return str.strip(value) if isinstance(value, str) else value
97
+
98
+
99
+ def _spaced(value: object) -> object:
100
+ if isinstance(value, bytes):
101
+ return b" ".join(bytes.split(value))
102
+ return " ".join(str.split(value)) if isinstance(value, str) else value
103
+
104
+
105
+ def _parsed_json(value: object) -> object:
106
+ """A string that is a JSON document, parsed. Anything else passes through."""
107
+ if isinstance(value, str) and value[:1] in "{[":
108
+ try:
109
+ return json.loads(value)
110
+ except ValueError:
111
+ return value
112
+ return value
113
+
114
+
115
+ def _decoded(value: object) -> object:
116
+ if isinstance(value, bytes):
117
+ try:
118
+ return value.decode("utf-8")
119
+ except UnicodeDecodeError:
120
+ return value
121
+ return value
122
+
123
+
124
+ def _enum_value(value: object) -> object:
125
+ return value.value if isinstance(value, enum.Enum) else value
126
+
127
+
128
+ def _json_label(pairs: Sequence[tuple[object, object]]) -> str:
129
+ """What parsing actually resolved, which is not the same thing on both shapes.
130
+
131
+ One side a string and the other a container is the familiar forgotten ``.json()``. Two strings
132
+ that parse to the same document differ in their formatting instead, and calling either of them
133
+ unparsed would be plainly wrong about both.
134
+ """
135
+ if all(isinstance(left, str) and isinstance(right, str) for left, right in pairs):
136
+ return "the same JSON written differently"
137
+ return "unparsed JSON text"
138
+
139
+
140
+ _OF_SPACE: list[tuple[Callable[[object], object], _Label]] = [
141
+ (_newlines, "line endings"),
142
+ (_stripped, "surrounding whitespace"),
143
+ ]
144
+ # ordered, narrower first: a step explains a pair only if the pair differed before it ran
145
+ _STEPS: list[tuple[Callable[[object], object], _Label]] = [
146
+ (_parsed_json, _json_label),
147
+ (_decoded, "bytes against decoded text"),
148
+ (_enum_value, "enum members against their values"),
149
+ *_OF_SPACE,
150
+ ]
151
+
152
+
153
+ def _explains(pairs: Sequence[tuple[object, object]], steps: Sequence[Callable[[object], object]]) -> bool:
154
+ """Whether applying ``steps`` to both sides of every pair leaves nothing differing.
155
+
156
+ A pair that already matches counts against every step rather than for it. Under
157
+ ``strict_types=True`` the two sides of an entry can compare equal and still be a difference,
158
+ because what differs is their type - and then any normalisation "explains" them by doing nothing,
159
+ so the first step in the ladder took the credit and the reader was told that a comparison holding
160
+ no JSON at all was one of unparsed JSON text.
161
+
162
+ A pair the steps left as it was is not accounted for either, and is not asked again: an ``==`` that answered
163
+ otherwise the second time credited a step that had done nothing.
164
+
165
+ Comparison failures count as "not explained" rather than propagating. This runs while a failure
166
+ is already being raised, on values the caller wrote, and a numpy array or any object with an
167
+ opinionated ``__eq__`` can raise from ``==``. Letting that out would replace the assertion error
168
+ the reader needs with a crash from the line that was only trying to be helpful.
169
+ """
170
+ try:
171
+ for left, right in pairs:
172
+ if equals(left, right):
173
+ return False
174
+ read_left, read_right = left, right
175
+ for step in steps:
176
+ read_left, read_right = step(read_left), step(read_right)
177
+ if (read_left is left and read_right is right) or not equals(read_left, read_right):
178
+ return False
179
+ except Exception: # a diagnostic must never outrank the failure it is describing
180
+ return False
181
+ return True
182
+
183
+
184
+ def _typed(pairs: Sequence[tuple[object, object]], kind: str) -> str | None:
185
+ """Whether every pair differs in its type, and what kind of type difference it is.
186
+
187
+ The leaf twin of the DTO-against-payload claim above, and the case a REST payload produces more
188
+ than any other: the ids came back as ``"7"`` where the test expects ``7``. Two facts live here
189
+ rather than one, because they are not the same news. Values that compare equal differ only in
190
+ their type, which is a difference at all only under ``strict_types``. Values that do not compare
191
+ equal but read alike are the same text on one side and a parsed value on the other.
192
+
193
+ Silent where the headline already said it. A scalar failure whose two sides render alike is
194
+ tagged by `_disambiguated` as ``<1:str>`` / ``<1:int>``, which names the two types outright, and
195
+ a line under it restating that in general terms is worse than nothing.
196
+ """
197
+ try:
198
+ if not all(kind_of(left) is not kind_of(right) for left, right in pairs):
199
+ return None
200
+ if kind == "scalar" and all(str(left) == str(right) for left, right in pairs):
201
+ return None
202
+ if all(equals(left, right) for left, right in pairs):
203
+ return "the values on both sides are equal, and only their types differ"
204
+ if all(str(left) == str(right) for left, right in pairs):
205
+ return "every difference here is the same text against a value of another type"
206
+ except Exception: # a diagnostic must never outrank the failure it is describing
207
+ return None
208
+ return None
209
+
210
+
211
+ def diagnose(
212
+ diff: DiffResult | None,
213
+ actual: object = None,
214
+ expected: object = None,
215
+ *,
216
+ identity: bool = False,
217
+ comparators: bool = False,
218
+ ) -> str | None:
219
+ """The one line to add to a failure message, or ``None`` when nothing can be said.
220
+
221
+ Args:
222
+ diff: the structured diff already built for this failure.
223
+ actual: the value asserted on, needed only for the string case below.
224
+ expected: the value it was compared against.
225
+ identity: whether the comparison was an unconfigured equality one that `identity_candidate`
226
+ found identity-bound *before* it ran. Only then does identity account for the failure.
227
+ comparators: whether the comparison was given ``comparators=``. Identity is then not said of a pair
228
+ inside the diff, whose verdict may be the predicate's.
229
+
230
+ Returns:
231
+ A lowercase clause to put on its own line under the message, or ``None``.
232
+ """
233
+ if diff is None or diff.kind not in _VALUE_KINDS:
234
+ return None
235
+ # asked of the two values compared, never of a pair inside the diff: identity equality can never pass
236
+ if identity:
237
+ return _IDENTITY_FACT
238
+ if not diff.entries:
239
+ return None
240
+ entries = diff.entries
241
+
242
+ # one flat pass: this is the only part whose cost grows with the entry count
243
+ pairs: list[tuple[object, object]] = []
244
+ # by the container each pair sits in: values swapped between two lists are not one list rearranged
245
+ per_container: dict[tuple[object, ...], list[tuple[object, object]]] = {}
246
+ absent_seen = False
247
+ absent_expected_only = True
248
+ positional = True
249
+ for entry in entries:
250
+ left, right = entry.actual, entry.expected
251
+ if nan_operand(left) or nan_operand(right):
252
+ # with a NaN in the comparison no other value would make it pass, so it comes before anything else
253
+ return _beside_a_nan(entries, comparators=comparators)
254
+ absent = entry.absent
255
+ if absent is None:
256
+ pairs.append((left, right))
257
+ per_container.setdefault(entry.steps[:-1], []).append((left, right))
258
+ absent_expected_only = False
259
+ else:
260
+ absent_seen = True
261
+ if absent != "expected":
262
+ absent_expected_only = False
263
+ # a step and not the rendered text: a mapping key ending in a bracket used to read as an index
264
+ if positional and not (entry.steps and entry.steps[-1].kind == "index"):
265
+ positional = False
266
+
267
+ if diff.kind == "string":
268
+ # the whole strings: `splitlines()` reads "\r\n" as "\n", so a text differing in both yields one entry
269
+ if not isinstance(actual, (str, bytes)) or not isinstance(expected, (str, bytes)):
270
+ return None
271
+ return _named([(actual, expected)])
272
+
273
+ # restricted to plain mappings, since telling someone their list carries extra keys helps nobody
274
+ if diff.kind == "dict" and absent_seen and absent_expected_only:
275
+ return "every shared key matches, and actual carries keys the expected side does not"
276
+
277
+ # a mixture of absent sides and differing values has no single statement that covers it
278
+ if absent_seen:
279
+ return _unseen(pairs, comparators=comparators)
280
+
281
+ # a DTO against the payload it was built from, said before the leaf steps because the shape is the narrower claim
282
+ if all(type(left) is not type(right) and _fields_match(left, right) for left, right in pairs):
283
+ return "the contents match field for field, and only the type of the two sides differs"
284
+
285
+ named = _named(pairs)
286
+ if named is not None:
287
+ return named
288
+
289
+ # after the ladder: a step that resolves the pairs has named the encoding they differ in, which is narrower
290
+ typed = _typed(pairs, diff.kind)
291
+ if typed is not None:
292
+ return typed
293
+
294
+ # last, as the broadest thing that can be said; one differing value can never be a rearrangement
295
+ if (
296
+ len(pairs) >= 2
297
+ and positional
298
+ and all(len(group) >= 2 and _same_values(group) for group in per_container.values())
299
+ ):
300
+ # the elements moved, and where two of them print the same no row shows which went where
301
+ return _UNSEEN_ORDER_FACT if any(_prints_alike(left, right) for left, right in pairs) else _ORDER_FACT
302
+ return _unseen(pairs, comparators=comparators)
303
+
304
+
305
+ def placed(diff: DiffResult | None) -> str | None:
306
+ """Where the differences of a diff sit, when they repeat at one to three places down a sequence, or ``None``.
307
+
308
+ Forty rows that differ in one field are forty rows of diff to read before that can be said, and past the
309
+ fiftieth the rows are not printed at all: a single difference of another field among them went unseen.
310
+ A place is the path with every position replaced by ``[*]``. It is a fact about where, said beside
311
+ whatever is said about why, and it recommends nothing.
312
+ """
313
+ if diff is None or diff.kind not in _VALUE_KINDS or len(diff.entries) < 3:
314
+ return None
315
+ counted: dict[tuple[tuple[str, str], ...], int] = {}
316
+ for entry in diff.entries:
317
+ place = _place(entry.steps)
318
+ if place is None:
319
+ return None
320
+ counted[place] = counted.get(place, 0) + 1
321
+ if len(counted) > 3:
322
+ return None
323
+ total = len(diff.entries)
324
+ spelled = {place: _spelled(place) for place in counted}
325
+ # nothing repeats, two places read the same (a key and a field of one name), or one is too long to be a line
326
+ if (
327
+ len(counted) == total
328
+ or len(set(spelled.values())) != len(spelled)
329
+ or any(len(text) > 200 for text in spelled.values())
330
+ ):
331
+ return None
332
+ if len(counted) == 1:
333
+ return f"all {total} differences here are at <{next(iter(spelled.values()))}>"
334
+ # by how many, and among equals by which came first, which is the order a dict keeps and a stable sort leaves
335
+ ranked = [f"<{spelled[place]}> ({count})" for place, count in sorted(counted.items(), key=lambda item: -item[1])]
336
+ return f"the {total} differences here are at {', '.join(ranked[:-1])} and {ranked[-1]}"
337
+
338
+
339
+ def _place(steps: Sequence[Step]) -> tuple[tuple[str, str], ...] | None:
340
+ """*steps* with every position made one, or ``None`` where they name no field.
341
+
342
+ The kind of each hop is kept, so a key and a field of one name are two places. Only a name that is
343
+ exactly a `str` counts: ``1`` and ``"1"`` are two keys and read as one. A place with no position in it
344
+ is met once and so never repeats, which is why none is asked for.
345
+ """
346
+ place: list[tuple[str, str]] = []
347
+ named = False
348
+ for step in steps:
349
+ kind = step.kind
350
+ if kind == "index":
351
+ place.append(("index", "*"))
352
+ elif kind in ("key", "attr") and type(step.value) is str:
353
+ place.append((kind, step.value))
354
+ named = True
355
+ elif kind != "json":
356
+ return None
357
+ return tuple(place) if named else None
358
+
359
+
360
+ def _spelled(place: tuple[tuple[str, str], ...]) -> str:
361
+ """A place as a path. A name that is no identifier is quoted: ``[*]['a.b']`` is one key, ``[*].a.b`` two."""
362
+ parts: list[str] = []
363
+ for kind, name in place:
364
+ if kind == "index":
365
+ parts.append("[*]")
366
+ elif not name.isidentifier():
367
+ parts.append(f"[{name!r}]")
368
+ else:
369
+ parts.append(f".{name}" if parts else name)
370
+ return "".join(parts)
371
+
372
+
373
+ def reads_as(one: object, other: object) -> bool:
374
+ """Whether two plain values of two types read the same: ``7`` and ``"7"``."""
375
+ kind, other_kind = type(one), type(other)
376
+ return kind is not other_kind and kind in _READ_AS_TEXT and other_kind in _READ_AS_TEXT and str(one) == str(other)
377
+
378
+
379
+ class Roles(NamedTuple):
380
+ """What the assertion asking calls the item it looked for and a candidate it looked among.
381
+
382
+ ``is_in`` looks for the value among the items given, a subset for its own element in the superset.
383
+ """
384
+
385
+ sought: str = "the item not found"
386
+ held: str = "an element"
387
+ named: bool = False
388
+ """Whether what it looked among are names, keys or attributes: one spelled almost the same is then said."""
389
+
390
+
391
+ _OF_AN_ITEM: Final = Roles()
392
+
393
+
394
+ def not_found(item: object, searched: Iterable[object], roles: Roles = _OF_AN_ITEM) -> str | None:
395
+ """One line on why *item* was not found in *searched*, where a fact about the item says it, or ``None``.
396
+
397
+ Said of the item sought and never of the collection: a NaN somewhere in a list does not explain a ``2`` that
398
+ is missing from it. Three facts are looked for. The item is a NaN. An element reads the same and is of
399
+ another plain type. An element prints the same, is of the item's class, and that class leaves ``==`` to
400
+ `object`. Each states what is so and leaves the reader to draw the rest. Among names a fourth: one that
401
+ reads almost the same (`_near_name`).
402
+ """
403
+ sought, held = roles.sought, roles.held
404
+ names: list[str] = []
405
+ try:
406
+ if nan_operand(item):
407
+ return f"{sought} is a NaN, and one NaN is not equal to another"
408
+ kind = type(item)
409
+ identity: bool | None = None
410
+ for element in searched:
411
+ if type(element) is not kind:
412
+ if reads_as(element, item):
413
+ types = f"{class_name(type(element))}, not {class_name(kind)}"
414
+ return f"{held} reads the same as {sought} and is of another type: {types}"
415
+ continue
416
+ if roles.named and kind is str and isinstance(element, str):
417
+ names.append(element)
418
+ if identity is None:
419
+ # a fact about the class, so asked of the first element of it and not of each
420
+ identity = identity_candidate(item, element)
421
+ # asked again past the two reprs, which are code of the class and may change what it compares by
422
+ if identity and _safe_repr(element) == _safe_repr(item) and identity_candidate(item, element):
423
+ return (
424
+ f"{held} prints the same as {sought}, and their class leaves __eq__ to object,"
425
+ " which compares by identity"
426
+ )
427
+ if names and isinstance(item, str):
428
+ return _near_name(item, names, held)
429
+ except Exception: # a diagnostic must never outrank the failure it is describing
430
+ return None
431
+ return None
432
+
433
+
434
+ _RUNS: Final = re.compile(r"\s+")
435
+
436
+
437
+ def under(line: str | None) -> str:
438
+ """*line* on a line of its own under a sentence, or nothing."""
439
+ return "" if line is None else f"\n{line}"
440
+
441
+
442
+ _INSTANCE_MATCHER: Final = "the matcher for an instance of a class is match.is_instance_of"
443
+
444
+
445
+ def matcher_for_a_class(given: object) -> str:
446
+ """What follows the refusal of *given* where a matcher was wanted, where it is a class `type` itself built.
447
+
448
+ Such a class takes an instance check and cannot refuse one, which a typed dict or a plain protocol does.
449
+ """
450
+ return f": {_INSTANCE_MATCHER}" if type(given) is type else ""
451
+
452
+
453
+ def class_in_spec(leaves: Iterable[tuple[str, object, object]]) -> str | None:
454
+ """One line for the first place where a spec holds a class and the value there is an instance of it.
455
+
456
+ A class in a spec is a value like any other, compared with ``==``. Said where `match.is_instance_of` takes the
457
+ value on the class tree alone: the class is built by `type` itself, so its instance check is the base one, and
458
+ it stands in the tree of the value's class, so neither is asked anything.
459
+ """
460
+ for where, value, literal in leaves:
461
+ if _an_instance_by_the_tree(value, literal):
462
+ return f"at <{where}> the spec holds a class itself and the value is an instance of it: {_INSTANCE_MATCHER}"
463
+ return None
464
+
465
+
466
+ def _an_instance_by_the_tree(value: object, literal: object) -> bool:
467
+ return type(literal) is type and any(base is literal for base in class_tree(type(value)))
468
+
469
+
470
+ def type_beside_a_class(value: object, literal: object) -> str:
471
+ """What follows a value held against a class it was not found to be an instance of: its type, as the matcher for
472
+ an instance words it.
473
+
474
+ A sentence prints a value by its text, so the text ``'7'`` beside the class `int` read ``<7>``. Nothing where
475
+ `class_in_spec` speaks of the same pair.
476
+ """
477
+ if not issubclass(type(literal), type) or _an_instance_by_the_tree(value, literal):
478
+ return ""
479
+ return f" of type <{class_name(type(value))}>"
480
+
481
+
482
+ def not_at_an_end(value: object, piece: object, *, start: bool, cased: bool = True) -> str | None:
483
+ """One line on why a text does not start with another, or end with it, where one fact says it.
484
+
485
+ Four are looked for, the narrowest first, each the relation itself holding once one thing is set aside:
486
+ whitespace at that end of the value, case, whitespace round the text given, and the text held somewhere
487
+ else in the value. *cased* is whether the assertion minded case: where it did not, the two are read
488
+ lowercased, so case is never the fact found. Read off exact `str` alone, which runs no code of the values.
489
+ """
490
+ if type(value) is not str or type(piece) is not str:
491
+ return None
492
+ named, edge, side = ("prefix", "starts", "past") if start else ("suffix", "ends", "before")
493
+ holds = str.startswith if start else str.endswith
494
+ text, wanted = (value, piece) if cased else (value.lower(), piece.lower())
495
+ if holds(text, wanted):
496
+ return None
497
+ bare = text.lstrip() if start else text.rstrip()
498
+ if bare != text and holds(bare, wanted):
499
+ gap = text[: len(text) - len(bare)] if start else text[len(bare) :]
500
+ what = "a line break" if gap in ("\n", "\r\n", "\r") else "whitespace"
501
+ return f"the value {edge} with {what}, and {edge} with the {named} {side} it"
502
+ if holds(text.lower(), wanted.lower()):
503
+ return f"the value {edge} with the {named} once case is ignored"
504
+ trimmed = wanted.strip()
505
+ if trimmed and trimmed != wanted and holds(text, trimmed):
506
+ return f"the {named} given starts or ends with whitespace, and the value {edge} with it without that"
507
+ if wanted in text:
508
+ return f"the value holds the {named}, and not at its {'start' if start else 'end'}"
509
+ return None
510
+
511
+
512
+ def not_in_text(value: object, piece: object, *, cased: bool = True) -> str | None:
513
+ """One line on why a text does not hold another, where one fact says it.
514
+
515
+ It holds it once case is ignored, or once every run of whitespace is read as one space, which is what a
516
+ line break, a tab, a no-break space and two spaces in a row all come to, or once both are. *cased* as in
517
+ `not_at_an_end`. Read off exact `str` alone.
518
+ """
519
+ if type(value) is not str or type(piece) is not str:
520
+ return None
521
+ text, wanted = (value, piece) if cased else (value.lower(), piece.lower())
522
+ if wanted in text:
523
+ return None
524
+ if wanted.lower() in text.lower():
525
+ return "the value holds it once case is ignored"
526
+ # a run is read as one space and not taken out: stripped, a text given with a space before it was held
527
+ spaced_text, spaced = _RUNS.sub(" ", text), _RUNS.sub(" ", wanted)
528
+ if spaced in spaced_text:
529
+ return "the value holds it once every run of whitespace is read as one space"
530
+ if spaced.lower() in spaced_text.lower():
531
+ return "the value holds it once case is ignored and every run of whitespace is read as one space"
532
+ return None
533
+
534
+
535
+ def but_for_whitespace(value: object, other: object) -> str | None:
536
+ """One line on two texts that are not equal without regard to case, where whitespace alone holds them apart.
537
+
538
+ It names the whitespace as `is_equal_to` does for two texts. Read off exact `str` alone.
539
+ """
540
+ if type(value) is not str or type(other) is not str:
541
+ return None
542
+ apart = _accounted([(value.lower(), other.lower())], _OF_SPACE)
543
+ return None if apart is None else f"the two are equal without regard to case but for {apart}"
544
+
545
+
546
+ def but_for_case(value: object, other: object) -> str | None:
547
+ """One line on two texts that are not equal ignoring whitespace, where case alone holds them apart.
548
+
549
+ Read off exact `str` alone.
550
+ """
551
+ if type(value) is not str or type(other) is not str:
552
+ return None
553
+ if "".join(value.split()).lower() == "".join(other.split()).lower():
554
+ return "the two are equal ignoring whitespace once case is ignored too"
555
+ return None
556
+
557
+
558
+ def _own_datetime(standing: type) -> type[datetime.datetime]:
559
+ """The standard library's datetime under the class that stands at `datetime.datetime`.
560
+
561
+ A library that freezes time puts a class of its own at that name, made of the real one. The real one is
562
+ the last to write `utcoffset` on the line of bases the class takes its layout from (`__base__`). Not the
563
+ last of the whole tree: a mixin that writes one comes after it there. And not the class of an instance
564
+ the class hands out: its `min` may be an instance of the class itself.
565
+ """
566
+ found, base = standing, standing
567
+ while base is not None:
568
+ if "utcoffset" in class_namespace(base):
569
+ found = base
570
+ base = type.__dict__["__base__"].__get__(base)
571
+ return cast("type[datetime.datetime]", found)
572
+
573
+
574
+ # in a tuple: such a library also rewrites each attribute of a module that is the real class
575
+ _MOMENT: Final = (_own_datetime(datetime.datetime),)
576
+ _ORDERED_BY: Final = ("__lt__", "__le__", "__gt__", "__ge__")
577
+
578
+
579
+ def out_of_order(value: object, other: object, *, before: bool, strict: bool) -> str | None:
580
+ """One line on a moment that is not before another, or not after it: how far on the other side it is.
581
+
582
+ Said by the four assertions of dates and by the four single relations of numbers, which take two datetimes
583
+ too. *before* is the side the assertion asked for, and *strict* whether the same moment fails it as well. The
584
+ distance is `datetime.datetime`'s own subtraction, exact to the microsecond. Where that is only the
585
+ distance between two readings of one clock (`_time_between`), the line says so.
586
+ """
587
+ measured = _time_between(value, other, _ORDERED_BY)
588
+ if measured is None:
589
+ return None
590
+ apart, moments = measured
591
+ if not apart:
592
+ return _same(moments) if strict else None
593
+ late = apart > datetime.timedelta()
594
+ if late is not before:
595
+ return None
596
+ side = "after" if late else "before"
597
+ if moments:
598
+ return f"the value is {abs(apart)} {side} the moment given"
599
+ return f"the value reads {abs(apart)} {side} the moment given on the clock the two share"
600
+
601
+
602
+ def apart_in_time(value: object, other: object, tolerance: object, *, close: bool) -> str | None:
603
+ """One line on two moments held against a tolerance: how far apart they are, and how far that is from it.
604
+
605
+ *close* is what the assertion asked for. Measured as `out_of_order` measures. Nothing is said of a pair
606
+ found close whose distance is past the tolerance: a window round either moment reached the other where the
607
+ distance did not. The tolerance is read where it is exactly a `datetime.timedelta`, and the assertion has
608
+ refused one below nothing.
609
+ """
610
+ measured = _time_between(
611
+ value, other, (*_ORDERED_BY, "__eq__", "__sub__", "__rsub__", "__add__", "__radd__", "__float__")
612
+ )
613
+ if measured is None or type(tolerance) is not datetime.timedelta:
614
+ return None
615
+ apart, moments = measured
616
+ distance = abs(apart)
617
+ if not distance:
618
+ return None if close else _same(moments)
619
+ told = f"the two are {distance} apart" if moments else f"the two read {distance} apart on the clock they share"
620
+ if close:
621
+ return f"{told}, {distance - tolerance} more than the tolerance" if distance > tolerance else None
622
+ if distance > tolerance:
623
+ return None
624
+ if distance == tolerance:
625
+ return f"{told}, which is the tolerance"
626
+ return f"{told}, {tolerance - distance} less than the tolerance"
627
+
628
+
629
+ def _same(moments: bool) -> str:
630
+ return "the two are the same moment" if moments else "the two read the same on the clock they share"
631
+
632
+
633
+ # every setting named: one left out is copied from `decimal.DefaultContext`, which a caller may have changed
634
+ _SHOWN_EXACTLY: Final = decimal.Context(
635
+ prec=34,
636
+ rounding=decimal.ROUND_HALF_EVEN,
637
+ Emin=decimal.MIN_EMIN,
638
+ Emax=decimal.MAX_EMAX,
639
+ capitals=1,
640
+ clamp=0,
641
+ flags=[],
642
+ traps=[decimal.Inexact, decimal.Overflow, decimal.InvalidOperation],
643
+ )
644
+ _SHOWN_ROUGHLY: Final = decimal.Context(
645
+ prec=12,
646
+ rounding=decimal.ROUND_HALF_EVEN,
647
+ Emin=decimal.MIN_EMIN,
648
+ Emax=decimal.MAX_EMAX,
649
+ capitals=1,
650
+ clamp=0,
651
+ flags=[],
652
+ traps=[],
653
+ )
654
+
655
+ _SIDE: Final = {
656
+ "is_less_than": "before",
657
+ "is_less_than_or_equal_to": "before",
658
+ "is_greater_than": "after",
659
+ "is_greater_than_or_equal_to": "after",
660
+ "is_before": "before",
661
+ "is_before_or_equal_to": "before",
662
+ "is_after": "after",
663
+ "is_after_or_equal_to": "after",
664
+ }
665
+ """The single bounds, by the side of the bound given each asks for."""
666
+
667
+
668
+ def passed_by(operation: str, value: object, parameters: Mapping[str, object], *, negated: bool) -> str | None:
669
+ """How far a value that passed was from the bound it was held against, or ``None`` where that is no distance.
670
+
671
+ Said for the bounds a threshold is written with: the four single relations of numbers and of dates, a range,
672
+ and closeness. The distance is taken between the values the comparison compared, exactly: a float is the
673
+ binary number it holds, so ``5 - 4.9`` is ``0.0999999999999996447...``, shown as ``about 0.1``. A distance
674
+ is printed exactly where 34 digits hold it and rounded to twelve, with ``about``, where they do not.
675
+
676
+ Only operands whose class is exactly a number of the standard library, a ``datetime``, a ``date`` or a
677
+ ``timedelta`` are measured, so no code of anybody's runs. Python orders those exactly, so the side a distance
678
+ falls on is the side the verdict found. A number whose exact ratio has a numerator or a denominator longer than
679
+ 3322 bits is not measured, so neither is one larger than about ``10**1000`` or smaller than about ``10**-1000``,
680
+ nor a `Decimal` written with more than a thousand digits: a pure-Python `decimal` refuses to read so many.
681
+ """
682
+ if operation in _SIDE:
683
+ return _from_bound(value, parameters.get("other"), _SIDE[operation])
684
+ if operation in ("is_between", "is_not_between"):
685
+ return _in_range(value, parameters.get("low"), parameters.get("high"))
686
+ if operation in ("is_close_to", "is_not_close_to"):
687
+ close = (operation == "is_close_to") is not negated
688
+ return _near(value, parameters.get("other"), parameters.get("tolerance"), parameters.get("rel"), close=close)
689
+ return None
690
+
691
+
692
+ def _exact(number: object) -> fractions.Fraction | None:
693
+ """*number* as the exact rational it is, or ``None`` for anything else, a non-finite or an outsized number."""
694
+ kind = type(number)
695
+ if kind is int:
696
+ whole = cast("int", number)
697
+ return fractions.Fraction(whole) if int.bit_length(whole) <= 3322 else None
698
+ if kind is float:
699
+ return None if _non_finite(number) else fractions.Fraction(cast("float", number))
700
+ if kind is decimal.Decimal:
701
+ held = cast("decimal.Decimal", number)
702
+ if _non_finite(held) or not -1000 <= decimal.Decimal.adjusted(held) <= 1000:
703
+ return None
704
+ if len(decimal.Decimal.as_tuple(held).digits) > 1000:
705
+ return None
706
+ return _bounded(fractions.Fraction(held))
707
+ if kind is fractions.Fraction:
708
+ return _bounded(cast("fractions.Fraction", number))
709
+ return None
710
+
711
+
712
+ def _bounded(ratio: fractions.Fraction) -> fractions.Fraction | None:
713
+ """*ratio*, unless its numerator or denominator is longer than 3322 bits, past about ``10**1000``."""
714
+ return ratio if max(int.bit_length(ratio.numerator), int.bit_length(ratio.denominator)) <= 3322 else None
715
+
716
+
717
+ def _minus(left: object, right: object) -> fractions.Fraction | None:
718
+ """*left* minus *right*, exactly, where both are numbers `_exact` reads."""
719
+ first, second = _exact(left), _exact(right)
720
+ if first is None or second is None:
721
+ return None
722
+ return first - second
723
+
724
+
725
+ def _durations(left: object, right: object) -> tuple[int, datetime.timedelta] | None:
726
+ """Which way *left* lies from *right*, and how far, for two durations or two dates of exactly the standard classes.
727
+
728
+ The size is the larger less the smaller: `-timedelta.max` is past the range though `timedelta.max` is not.
729
+ """
730
+ kinds = (type(left), type(right))
731
+ if kinds != (datetime.timedelta, datetime.timedelta) and kinds != (datetime.date, datetime.date):
732
+ return None
733
+ first, second = cast("datetime.timedelta", left), cast("datetime.timedelta", right)
734
+ larger, smaller, sign = (first, second, 1) if first >= second else (second, first, -1)
735
+ try:
736
+ return (sign if larger != smaller else 0), larger - smaller
737
+ except OverflowError:
738
+ # two durations at the two ends of the range compare, and their distance is past it
739
+ return None
740
+
741
+
742
+ def _from_bound(value: object, bound: object, side: str) -> str | None:
743
+ """Where *value* stands against one bound: how far below or above it, or before or after it.
744
+
745
+ *side* is the one the assertion asked for, which `out_of_order` takes as the side to say nothing on. Numbers
746
+ are asked first, being what a threshold usually is: the moments' reading looks at the classes, which cost a
747
+ passing line half its time when asked first.
748
+ """
749
+ difference = _minus(value, bound)
750
+ words = ("above", "below")
751
+ if difference is None:
752
+ moment = out_of_order(value, bound, before=side == "after", strict=True) or out_of_order(
753
+ value, bound, before=side != "after", strict=True
754
+ )
755
+ if moment is not None:
756
+ return moment
757
+ apart = _durations(value, bound)
758
+ if apart is None:
759
+ return None
760
+ sign, size = apart
761
+ if type(bound) is datetime.date:
762
+ given, shown, words = "date", f"{size.days} day{'s' if size.days != 1 else ''}", ("after", "before")
763
+ else:
764
+ given, shown = "duration", str(size)
765
+ else:
766
+ sign, given, shown = (difference > 0) - (difference < 0), "number", _shown(abs(difference))
767
+ if not sign:
768
+ return f"the value is the {given} given"
769
+ return f"the value is {shown} {words[0] if sign > 0 else words[1]} the {given} given"
770
+
771
+
772
+ def _shown(number: fractions.Fraction) -> str:
773
+ """*number* in decimal: exactly where 34 digits hold it, else rounded to twelve and said to be."""
774
+ numerator, denominator = decimal.Decimal(number.numerator), decimal.Decimal(number.denominator)
775
+ try:
776
+ written, about = _SHOWN_EXACTLY.divide(numerator, denominator), ""
777
+ except decimal.Inexact:
778
+ written, about = _SHOWN_ROUGHLY.divide(numerator, denominator), "about "
779
+ plain = written.normalize(_SHOWN_ROUGHLY if about else _SHOWN_EXACTLY)
780
+ # asked before formatting: a positional `1E+999` is a thousand characters. `E`, as `str()` reads the ambient context
781
+ return about + format(plain, "f" if -12 <= plain.adjusted() <= 20 else "E")
782
+
783
+
784
+ def _in_range(value: object, low: object, high: object) -> str | None:
785
+ """Where *value* stands against a range of numbers: how far within each end, or how far past the nearer."""
786
+ from_low, to_high = _minus(value, low), _minus(high, value)
787
+ if from_low is None or to_high is None:
788
+ return None
789
+ if from_low < 0 or to_high < 0:
790
+ if from_low < 0:
791
+ return f"the value is {_shown(-from_low)} below the low end"
792
+ return f"the value is {_shown(-to_high)} above the high end"
793
+ if not from_low:
794
+ return "the value is the low end"
795
+ if not to_high:
796
+ return "the value is the high end"
797
+ return f"the value is {_shown(from_low)} above the low end and {_shown(to_high)} below the high end"
798
+
799
+
800
+ def _near(value: object, other: object, tolerance: object, rel: object, *, close: bool) -> str | None:
801
+ """How far apart two values that passed a closeness are, against what they were allowed.
802
+
803
+ Either allowance holding is enough, so the larger is the one measured against. Nothing is said where the
804
+ exact distance disagrees with the verdict: a window round either value reached the other where it did not.
805
+ """
806
+ if isinstance(tolerance, datetime.timedelta) and rel is None:
807
+ return apart_in_time(value, other, tolerance, close=not close)
808
+ difference, allowed = _minus(value, other), _allowance(other, tolerance, rel)
809
+ if difference is None or allowed is None:
810
+ return None
811
+ distance = abs(difference)
812
+ spare = allowed - distance
813
+ if (spare >= 0) is not close:
814
+ return None
815
+ if not distance:
816
+ return "the two are equal"
817
+ told = "the tolerance" if rel is None else f"the allowance of {_shown(allowed)}"
818
+ if not spare:
819
+ return f"the two are {_shown(distance)} apart, which is {told}"
820
+ if close:
821
+ return f"the two are {_shown(distance)} apart, {_shown(spare)} less than {told}"
822
+ return f"the two are {_shown(distance)} apart, {_shown(-spare)} more than {told}"
823
+
824
+
825
+ def _allowance(other: object, tolerance: object, rel: object) -> fractions.Fraction | None:
826
+ """The larger of the two allowances asked, exactly, or ``None`` where either given is past measuring.
827
+
828
+ One past measuring, an infinite one among them, would be the larger, so no smaller one stands in for it.
829
+ """
830
+ absolute = _exact(tolerance) if tolerance is not None else None
831
+ if tolerance is not None and absolute is None:
832
+ return None
833
+ if rel is None:
834
+ return absolute
835
+ scale, size = _exact(rel), _exact(other)
836
+ if scale is None or size is None:
837
+ return None
838
+ relative = scale * abs(size)
839
+ return relative if absolute is None or relative > absolute else absolute
840
+
841
+
842
+ def _time_between(value: object, other: object, asked: tuple[str, ...]) -> tuple[datetime.timedelta, bool] | None:
843
+ """*value* minus *other* by `datetime.datetime`'s own subtraction, and whether that is the time between them.
844
+
845
+ Read only for two whose classes leave *asked* to the base type: a class that writes one of those operators
846
+ made the verdict its own way, on what it holds past the microsecond or anywhere else.
847
+
848
+ Two that share one zone object, or have none, the base type compares and subtracts on their wall clocks,
849
+ asking the zone nothing. That is the time between the two where there is no zone, or where the zone gives
850
+ both one offset. Across a change of its clocks it is not: four hours read off a clock that went back for
851
+ five that passed, and the two readings of the repeated hour found equal. Then, and for a zone that is not
852
+ asked, the answer is ``False`` and the line speaks of the clock the two share.
853
+
854
+ A zone is asked only where it is one of the standard library's own (`_own_zone`), since a line read past the
855
+ verdict cannot know what another zone's code told the verdict. Two in different zones are measured as
856
+ moments, which asks both, so a pair with a zone from elsewhere among them gets nothing.
857
+ """
858
+ moment = _MOMENT[0]
859
+ if not isinstance(value, moment) or not isinstance(other, moment):
860
+ return None
861
+ try:
862
+ if not (_left_to_datetime(type(value), asked) and _left_to_datetime(type(other), asked)):
863
+ return None
864
+ zone, other_zone = moment.tzinfo.__get__(value), moment.tzinfo.__get__(other)
865
+ if zone is not other_zone:
866
+ if not (_own_zone(zone) and _own_zone(other_zone)):
867
+ return None
868
+ return moment.__sub__(value, other), True
869
+ apart = moment.__sub__(value, other)
870
+ if zone is None:
871
+ return apart, True
872
+ if not _own_zone(zone):
873
+ return apart, False
874
+ offset = moment.utcoffset(value)
875
+ one = offset is not None and datetime.timedelta.__eq__(offset, moment.utcoffset(other)) is True
876
+ return apart, one
877
+ except Exception: # a naive moment against an aware one refuses to subtract, and no line outranks the failure
878
+ return None
879
+
880
+
881
+ def _own_zone(zone: object) -> bool:
882
+ """Whether *zone* is none, or one of the standard library's own, whose offset its code alone answers.
883
+
884
+ The class of `zoneinfo` is read where it is loaded and not imported: no zone of its class exists without
885
+ it, and the import costs 4.9 ms, measured, which every importer of the library would pay. It is read off
886
+ the two modules that implement it and not off `zoneinfo.ZoneInfo`: that is a name a test may put a class
887
+ of its own at, and what such a class says of itself, its name and its module, is the class's to say.
888
+ """
889
+ kind = type(zone)
890
+ if zone is None or kind is datetime.timezone:
891
+ return True
892
+ implemented_in = ("_zoneinfo", "zoneinfo._zoneinfo")
893
+ return any(kind is getattr(sys.modules.get(name), "ZoneInfo", None) for name in implemented_in)
894
+
895
+
896
+ def _left_to_datetime(klass: type, asked: tuple[str, ...]) -> bool:
897
+ """Whether *klass* is `datetime.datetime`, or leaves every operator in *asked* to it, read off its tree.
898
+
899
+ The keys are read before any name is looked up: a key of a class of its own answers a lookup with its code.
900
+ """
901
+ if klass is _MOMENT[0]:
902
+ return True
903
+ if not _plainly_keyed(klass):
904
+ return False
905
+ for base in class_tree(klass):
906
+ if base is _MOMENT[0]:
907
+ return True
908
+ written = class_namespace(base)
909
+ if any(name in written for name in asked):
910
+ return False
911
+ return False
912
+
913
+
914
+ _JOINTS: Final = re.compile(r"[\s_.\-]+")
915
+ """What joins the words of a name: a space, an underscore, a dot, a hyphen."""
916
+
917
+
918
+ def _near_name(name: str, names: Sequence[str], held: str) -> str | None:
919
+ """One line on a name among *names* that reads almost as *name*, which was asked for and is not there.
920
+
921
+ Two facts and one likeness, the narrowest first. A name is the same once both are lowercased. A name is
922
+ the same once both are lowercased and what joins their words is taken out, ``userId`` beside ``user_id``.
923
+ One spelling alone is almost the same (`_almost`), and the names of that spelling are named, the first
924
+ three of them and how many more: two that differ only in case or joints are one spelling. The last is a
925
+ likeness and is worded as one: which name was meant is the reader's to say, and where two spellings come
926
+ that close nothing is said.
927
+
928
+ Read off exact `str` alone, which runs no code of the values. By ``lower()`` and not ``casefold()``, which
929
+ calls ``ß`` and ``ss`` one text, a difference that is not of case. The likeness is looked for only where
930
+ the name asked for and the name held are of a hundred characters at most, as they were written: past that
931
+ it is no name, and the search is quadratic.
932
+ """
933
+ others = [each for each in names if each != name]
934
+ lowered = name.lower()
935
+ alike = [each for each in others if each.lower() == lowered]
936
+ if alike:
937
+ return f"{held} reads the same but for its case: {_listed(alike)}"
938
+ plain = _JOINTS.sub("", lowered)
939
+ if not plain:
940
+ return None
941
+ joined: dict[str, list[str]] = {}
942
+ for each in others:
943
+ joined.setdefault(_JOINTS.sub("", each.lower()), []).append(each)
944
+ if plain in joined:
945
+ return f"{held} reads the same but for how its words are joined: {_listed(joined[plain])}"
946
+ if len(name) > 100:
947
+ return None
948
+ # one matcher for the name: built for every key, the ratio cost a failure over 200 keys 0.8 ms
949
+ texts = difflib.SequenceMatcher(None, b=plain, autojunk=False)
950
+ short = (each for each, held_as in joined.items() if each and all(len(key) <= 100 for key in held_as))
951
+ close = [each for each in short if _almost(plain, each, texts)]
952
+ if len(close) == 1:
953
+ return f"{held} is spelled almost the same: {_listed(joined[close[0]])}"
954
+ return None
955
+
956
+
957
+ def _almost(one: str, other: str, texts: difflib.SequenceMatcher[str]) -> bool:
958
+ """Whether two names are one slip apart, or nearly one text.
959
+
960
+ A slip is two neighbouring letters swapped, one letter too many, or one too few. A letter in place of
961
+ another is not one: that is how two words differ, ``date`` and ``data``. Nearly one text is a `difflib`
962
+ ratio of 0.85, which a long name reaches with a few letters dropped or replaced, ``created`` beside
963
+ ``created_at`` and seventeen of one letter beside twenty. Measured on 54 labelled typos and 46 pairs of
964
+ unrelated key names: 52 named and 6, four of the six a singular beside its plural. The ratio alone named
965
+ 49 and 5 at this floor, and 52 and 9 at 0.8. *texts* holds *one* and is asked the ratio last, past its
966
+ two upper bounds.
967
+ """
968
+ if len(one) == len(other):
969
+ apart = [index for index in range(len(one)) if one[index] != other[index]]
970
+ if (
971
+ len(apart) == 2
972
+ and apart[1] == apart[0] + 1
973
+ and one[apart[0]] == other[apart[1]]
974
+ and one[apart[1]] == other[apart[0]]
975
+ ):
976
+ return True
977
+ elif abs(len(one) - len(other)) == 1:
978
+ shorter, longer = sorted((one, other), key=len)
979
+ parted = next((index for index in range(len(shorter)) if shorter[index] != longer[index]), len(shorter))
980
+ if longer[parted + 1 :] == shorter[parted:]:
981
+ return True
982
+ texts.set_seq1(other)
983
+ return texts.real_quick_ratio() >= 0.85 and texts.quick_ratio() >= 0.85 and texts.ratio() >= 0.85
984
+
985
+
986
+ def _listed(names: Sequence[str]) -> str:
987
+ shown = ", ".join(f"<{_capped(each)}>" for each in names[:3])
988
+ return shown if len(names) <= 3 else f"{shown} and {len(names) - 3} more"
989
+
990
+
991
+ def _beside_a_nan(entries: Sequence[DiffEntry], *, comparators: bool) -> str:
992
+ """The NaN fact, and under it what is said of a pair no NaN takes part in that prints the same on both sides."""
993
+ others = [
994
+ (entry.actual, entry.expected)
995
+ for entry in entries
996
+ if entry.absent is None and not (nan_operand(entry.actual) or nan_operand(entry.expected))
997
+ ]
998
+ unseen = _unseen(others, comparators=comparators)
999
+ return _NAN_FACT if unseen is None else f"{_NAN_FACT}\n{unseen}"
1000
+
1001
+
1002
+ def _prints_alike(left: object, right: object) -> bool:
1003
+ """Whether a row of these two reads the same on both sides: one class, or two nothing tells apart, one repr."""
1004
+ kind = type(left)
1005
+ if kind is type(right) and kind in _PRINTED_AS_HELD:
1006
+ return left == right
1007
+ return _class_names(left, right) is None and _safe_repr(left) == _safe_repr(right)
1008
+
1009
+
1010
+ def _unseen(pairs: Sequence[tuple[object, object]], *, comparators: bool) -> str | None:
1011
+ """What to say of pairs that print the same on both sides and are one class, or ``None`` where none does.
1012
+
1013
+ Such a row names no difference a reader can see, and nothing above accounted for it. Where no comparator
1014
+ took part and every one of them is two instances of a class that leaves ``==`` to `object`, that is the
1015
+ reason. Otherwise the reason is in the comparison itself, a comparator or an ``__eq__`` of the value's
1016
+ own, and the line says what is known: the repr does not show it, and which attributes of the two differ
1017
+ where they hold any. Asked of every pair: how many rows a diff prints is the renderer's to decide.
1018
+
1019
+ The identity line here is not the top pair's. That one speaks of the two values compared, and is asked
1020
+ before they are. A pair inside has no before short of walking both values on every passing comparison
1021
+ (23 times the cost of one over 200 rows), so its line speaks of the row and of the class as it stands now.
1022
+ """
1023
+ alike = [(left, right) for left, right in pairs if _prints_alike(left, right)]
1024
+ if not alike:
1025
+ return None
1026
+ if not comparators and all(identity_candidate(left, right) for left, right in alike):
1027
+ return _UNSEEN_IDENTITY_FACT
1028
+ names: dict[str, None] = {}
1029
+ for left, right in alike:
1030
+ _attributes_apart(left, right, names)
1031
+ if len(names) > 5:
1032
+ break
1033
+ if not names:
1034
+ return _UNSEEN_FACT
1035
+ shown = list(names)
1036
+ return f"{_UNSEEN_FACT} (attributes that differ: {', '.join(shown[:5])}{', ..' if len(shown) > 5 else ''})"
1037
+
1038
+
1039
+ def _attributes_apart(left: object, right: object, known: dict[str, None]) -> None:
1040
+ """Add to *known* the attributes two values hold that differ between them, up to the six a line can use.
1041
+
1042
+ An attribute whose comparison raises is left out: nothing is known of it.
1043
+ """
1044
+ try:
1045
+ held, other = vars(left), vars(right)
1046
+ for name in (*held, *(name for name in other if name not in held)):
1047
+ if name not in held or name not in other or _held_apart(held[name], other[name]):
1048
+ known[_safe_str(name)] = None
1049
+ if len(known) > 5:
1050
+ return
1051
+ except Exception: # a diagnostic must never outrank the failure it is describing
1052
+ return
1053
+
1054
+
1055
+ def _held_apart(one: object, other: object) -> bool:
1056
+ try:
1057
+ return not equals(one, other)
1058
+ except Exception: # a diagnostic must never outrank the failure it is describing
1059
+ return False
1060
+
1061
+
1062
+ def _fields_of(value: object) -> dict | None:
1063
+ """A dataclass, attrs instance or pydantic-style model as its fields, read as its ``==`` reads them.
1064
+
1065
+ Read field by field, a field ``==`` leaves out kept the hint away from a payload that matched, and so
1066
+ did a nested object against the nested mapping it was built from.
1067
+ """
1068
+ if (
1069
+ is_model_dump_object(value)
1070
+ or is_attrs_instance(value)
1071
+ or (dataclasses.is_dataclass(value) and not isinstance(value, type))
1072
+ ):
1073
+ return comparable_fields(value)
1074
+ return None
1075
+
1076
+
1077
+ def identity_candidate(left: object, right: object) -> bool:
1078
+ """Whether ``==`` between these two comes down to identity, asked *before* the comparison runs.
1079
+
1080
+ Two separate instances of a type that leaves ``__eq__`` to ``object`` are never equal, whatever they
1081
+ hold. That is a fact about the type rather than about what the two hold, which is the only claim worth
1082
+ making: state can live in a slot, in a descriptor's own table or in a C field, and a line that promised
1083
+ to have read all of it would be promising more than any reading can deliver. It is said of two of one
1084
+ type only: a value of another type may answer the reflected comparison its own way.
1085
+
1086
+ Three details make the answer trustworthy, and each was put here by a case that defeated the one
1087
+ before it. It is asked before the comparison, because a type may rewrite its own ``__eq__`` while
1088
+ answering one, and a question asked afterwards would be answered by the type it left behind. It
1089
+ reads no attribute of the class, not even through `type.__getattribute__`: that runs a descriptor
1090
+ the class holds under the name, and one that installs an equality while being asked failed a pair
1091
+ its own ``==`` held equal. And what it reads is the class tree's own namespaces, in the order the
1092
+ tree has them, the way `slot_tp_richcompare` reads them, off the slots of `type`, which a metaclass
1093
+ cannot spell. The tree is the real one: a metaclass's own ``mro()`` can put another class first, and
1094
+ that class's equality is then the one ``==`` runs.
1095
+
1096
+ ``None`` is an answer: a class that writes ``__eq__ = None`` has said what its equality is. A tree in
1097
+ which no class writes one, which reassigning ``__bases__`` under such a metaclass can leave behind, is
1098
+ no claim of identity. A list and a tuple are answered before any namespace is read. Scalars reach
1099
+ none of it: their comparison returns before this is asked.
1100
+
1101
+ One thing a namespace can still run: a key of a class of its own, a `str` subclass, that compares itself
1102
+ and may answer one lookup one way and the next another. Identity is therefore claimed only of a tree whose
1103
+ namespaces hold exact `str` keys alone (`_plainly_keyed`), which is read where the claim is about to be
1104
+ made and nowhere else: a tree that writes an equality of its own never pays for it.
1105
+
1106
+ The first class of the tree answers for a type that writes its equality itself, at 105 ns where the
1107
+ look through the class it replaces took 76. One that inherits it pays about 50 ns more a level.
1108
+
1109
+ ``__ne__`` is not asked about: the comparison a failing assertion runs is ``actual == expected``, so a
1110
+ type defining only ``__ne__`` still compares by identity.
1111
+ """
1112
+ try:
1113
+ klass = type(left)
1114
+ # one object against itself is equal under identity too, so a failure can never be about that
1115
+ if left is right or klass is not type(right) or klass is list or klass is tuple:
1116
+ return False
1117
+ for base in class_tree(klass):
1118
+ written = class_namespace(base)
1119
+ if "__eq__" in written:
1120
+ return written["__eq__"] is _OBJECT_EQUALITY and _plainly_keyed(klass)
1121
+ except Exception: # a key of a class of its own raised from its ``__eq__``: no claim, and a failure is on the way
1122
+ return False
1123
+ return False
1124
+
1125
+
1126
+ def _plainly_keyed(klass: type) -> bool:
1127
+ """Whether every namespace of *klass*'s tree holds exact `str` keys alone, so a lookup in it ran no code."""
1128
+ return all(type(key) is str for base in class_tree(klass) for key in class_namespace(base))
1129
+
1130
+
1131
+ def _fields_match(left: object, right: object) -> bool:
1132
+ """Whether one side is an object whose fields are exactly the other side's mapping."""
1133
+ try:
1134
+ for obj, other in ((left, right), (right, left)):
1135
+ fields = _fields_of(obj)
1136
+ if fields is not None and is_mapping_like(other) and fields == dict(other):
1137
+ return True
1138
+ except Exception: # a diagnostic must never outrank the failure it is describing
1139
+ return False
1140
+ return False
1141
+
1142
+
1143
+ def _same_values(pairs: Sequence[tuple[object, object]]) -> bool:
1144
+ """Whether the differing values are the same on both sides, sitting in different places.
1145
+
1146
+ Counted rather than sorted wherever the values are hashable, which is the common case and the one
1147
+ that used to be expensive: sorting by ``repr`` calls ``repr`` on every value on both sides, and a
1148
+ failure over a two-thousand element sequence paid four thousand of them for an answer that is
1149
+ almost always no. The sort stays as the fallback, since it is what handles values that are
1150
+ unhashable or not orderable against each other.
1151
+ """
1152
+ try:
1153
+ return Counter(value for value, _ in pairs) == Counter(value for _, value in pairs)
1154
+ except TypeError: # unhashable values, which only the slower route can compare
1155
+ pass
1156
+ except Exception: # a diagnostic must never outrank the failure it is describing
1157
+ return False
1158
+ try:
1159
+ return sorted((value for value, _ in pairs), key=repr) == sorted((value for _, value in pairs), key=repr)
1160
+ except Exception: # a diagnostic must never outrank the failure it is describing
1161
+ return False
1162
+
1163
+
1164
+ def _named(pairs: Sequence[tuple[object, object]]) -> str | None:
1165
+ """The narrowest set of steps that accounts for every pair, worded, or ``None``."""
1166
+ accounted = _accounted(pairs, _STEPS)
1167
+ return None if accounted is None else f"every difference here is one of {accounted}"
1168
+
1169
+
1170
+ def _accounted(
1171
+ pairs: Sequence[tuple[object, object]], steps: Sequence[tuple[Callable[[object], object], _Label]]
1172
+ ) -> str | None:
1173
+ """What accounts for every pair, of *steps* and of whitespace of any kind and amount, the narrowest first.
1174
+
1175
+ One step, then two. Then the kind or amount of whitespace, alone and beside one step: it takes in line
1176
+ endings and surrounding whitespace, so said first it would stand in for the two narrower facts.
1177
+
1178
+ Nothing is asked where no step reads the first pair (`_unread_by_any_step`): every step has to account for
1179
+ it, and asking all nineteen was most of what a failure of two numbers spent here.
1180
+ """
1181
+ if _unread_by_any_step(pairs[0]):
1182
+ return None
1183
+ for step, label in steps:
1184
+ if _explains(pairs, (step,)):
1185
+ return _worded(label, pairs)
1186
+ # only now pairs of steps: `"a\r\nb "` against `"a\nb"` needs both, and neither alone equalises it
1187
+ for index, (first_step, first_label) in enumerate(steps):
1188
+ for second_step, second_label in steps[index + 1 :]:
1189
+ if _explains(pairs, (first_step, second_step)):
1190
+ return f"{_worded(first_label, pairs)} and {_worded(second_label, pairs)}"
1191
+ if _explains(pairs, (_spaced,)):
1192
+ return "the kind or amount of whitespace"
1193
+ for step, label in steps:
1194
+ if step not in (_newlines, _stripped) and _explains(pairs, (step, _spaced)):
1195
+ return f"{_worded(label, pairs)} and the kind or amount of whitespace"
1196
+ return None
1197
+
1198
+
1199
+ def _unread_by_any_step(pair: tuple[object, object]) -> bool:
1200
+ """Whether neither side of *pair* is a text, a ``bytes`` or an enum member, which is all a step reads.
1201
+
1202
+ Asked as each step asks it. Where a value's ``__class__`` is code of its own that raises, the pair is unread
1203
+ too: every step would have ended in that same error, and `_explains` counts it as no.
1204
+ """
1205
+ try:
1206
+ return not any(isinstance(side, (str, bytes, enum.Enum)) for side in pair)
1207
+ except Exception: # a diagnostic must never outrank the failure it is describing
1208
+ return True
1209
+
1210
+
1211
+ def _worded(label: _Label, pairs: Sequence[tuple[object, object]]) -> str:
1212
+ """A step's label, letting one that depends on the shape of the pairs decide for itself."""
1213
+ return label if isinstance(label, str) else label(pairs)