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,70 @@
1
+ """Maximum pairing between two sides, for every assertion that says "these each, in any order"."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from collections.abc import Callable, Iterable, Iterator, Sequence
9
+
10
+ __tracebackhide__ = True
11
+
12
+
13
+ def maximum_pairing(candidates: Sequence[set[int]]) -> dict[int, int]:
14
+ """As many entries paired with distinct options as can be, returned as ``{option: entry}``.
15
+
16
+ Kuhn's algorithm: every entry takes a free option first, and each one left over then tries to
17
+ displace an owner along a chain of alternatives.
18
+ """
19
+ paired: dict[int, int] = {}
20
+ unpaired = []
21
+ for entry, options in enumerate(candidates):
22
+ free = next((index for index in options if index not in paired), None)
23
+ if free is None:
24
+ unpaired.append(entry)
25
+ else:
26
+ paired[free] = entry
27
+ return completed_pairing(paired, unpaired, candidates.__getitem__)
28
+
29
+
30
+ def completed_pairing(
31
+ paired: dict[int, int], unpaired: Iterable[int], options_of: Callable[[int], Iterable[int]]
32
+ ) -> dict[int, int]:
33
+ """*paired*, any valid pairing, grown to a maximum one by the entries it left out, and returned.
34
+
35
+ An entry no displacement reaches now cannot be reached later, so each is tried once. *options_of* is
36
+ asked only for the entries a chain of displacements meets, which lets a caller work them out on demand.
37
+ """
38
+ for entry in unpaired:
39
+ _augmented(entry, options_of, paired)
40
+ return paired
41
+
42
+
43
+ def _augmented(entry: int, options_of: Callable[[int], Iterable[int]], paired: dict[int, int]) -> bool:
44
+ """Whether one entry can be paired by displacing others, updating `paired` when it can.
45
+
46
+ Iterative, because the chain of displacements is as long as the input is wide rather than as deep:
47
+ 1100 entries against 1100 options every one of them accepts recursed past the limit and reported a
48
+ `RecursionError` for a pairing that exists.
49
+ """
50
+ seen: set[int] = set()
51
+ path: list[tuple[int, Iterator[int]]] = [(entry, iter(options_of(entry)))]
52
+ taken: list[int] = []
53
+ while path:
54
+ options = path[-1][1]
55
+ for index in options:
56
+ if index in seen:
57
+ continue
58
+ seen.add(index)
59
+ taken.append(index)
60
+ if index not in paired:
61
+ for step, (displaced, _) in reversed(list(enumerate(path))):
62
+ paired[taken[step]] = displaced
63
+ return True
64
+ path.append((paired[index], iter(options_of(paired[index]))))
65
+ break
66
+ else:
67
+ path.pop()
68
+ if taken:
69
+ taken.pop()
70
+ return False
@@ -0,0 +1,158 @@
1
+ """Where a diff entry sits, in both the forms its two readers need.
2
+
3
+ `DiffEntry.path` is written for a person and is lossy on purpose: a mapping key goes through ``str()``,
4
+ so ``{3: ...}`` and ``{"3": ...}`` land on the same text, and a key holding a dot or a bracket cannot be
5
+ read back out. That is the right trade for a message and the wrong one for anything that wants to walk
6
+ back into the value, which is what `DiffEntry.steps` is for.
7
+
8
+ Both forms are carried by one object so they cannot drift. Nine producers build paths, and each of them
9
+ would otherwise have written the text and the steps separately, twice, in agreement only by hand.
10
+
11
+ The rendering rules are **not** uniform across producers and are not made uniform here. A mapping key
12
+ renders bare at the root (``b``) while a field renders dotted (``.b``), because a top-level dict is
13
+ reported by `HelpersMixin._dict_err()` with bare keys and has been since before this file existed.
14
+ Each hop keeps its own rule, in one place, instead of at 35 call sites.
15
+
16
+ Building a path is deferred to the point where an entry is actually produced. The walkers used to
17
+ format one per child before deciding whether the child differed, so a sequence of a thousand equal
18
+ elements paid for a thousand strings nobody read.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from dataclasses import dataclass
24
+ from typing import Literal
25
+
26
+ from ..errors import DiffEntry, Step, _safe_str
27
+
28
+ __tracebackhide__ = True
29
+
30
+
31
+ _Joint = Literal["append", "dotted", "reset"]
32
+ """How a hop's text meets the text before it: after it, after a dot unless nothing came before, or in its place."""
33
+
34
+
35
+ @dataclass(frozen=True, slots=True, eq=False, repr=False)
36
+ class _Path:
37
+ """A location inside a compared value, as rendered text plus the steps that reached it.
38
+
39
+ Not a `NamedTuple`, though it is a pair: ``index`` and ``count`` are already taken by ``tuple``, and
40
+ a hop named ``index`` is the one this is used for most. It was tried, and it measured the same.
41
+
42
+ Held as one hop and the path it was taken from, and read out only when an entry is made. A node's path
43
+ stays alive while its children are walked, so holding the whole text and every step at each hop made the
44
+ paths of a walk quadratic in its depth: 48 MB for three thousand levels, against 0.6 MB held as hops.
45
+ """
46
+
47
+ piece: str = ""
48
+ step: Step | None = None
49
+ parent: _Path | None = None
50
+ joint: _Joint = "reset"
51
+
52
+ def key(self, key: object) -> _Path:
53
+ """Into a mapping. Bare at the root, dotted below it, and the key itself is kept unstringified."""
54
+ rendered = _safe_str(key)
55
+ if type(self.piece) is not str:
56
+ text = self.text
57
+ return _Path(f"{text}.{rendered}" if text else rendered, Step("key", key), self)
58
+ return _Path(rendered, Step("key", key), self, "dotted")
59
+
60
+ def attr(self, name: str, *, dotted_at_root: bool = True) -> _Path:
61
+ """Into a field of a dataclass, namedtuple, attrs class or model.
62
+
63
+ ``dotted_at_root`` is not a style knob. The diff walkers render a root-level field as ``.name``
64
+ and the leaf walker renders it as ``name``, and both are load-bearing: the first matches how the
65
+ dict path has rendered nested keys since before either existed, the second feeds messages that
66
+ name a field of the value under test. Below the root the two agree.
67
+ """
68
+ if type(name) is not str or type(self.piece) is not str:
69
+ text = self.text
70
+ return _Path(f"{text}.{name}" if text or dotted_at_root else name, Step("attr", name), self)
71
+ if dotted_at_root:
72
+ return _Path(f".{name}", Step("attr", name), self, "append")
73
+ return _Path(name, Step("attr", name), self, "dotted")
74
+
75
+ def index(self, index: int) -> _Path:
76
+ """Into a position both sequences share."""
77
+ if type(self.piece) is not str:
78
+ return _Path(f"{self.text}[{index}]", Step("index", index), self)
79
+ return _Path(f"[{index}]", Step("index", index), self, "append")
80
+
81
+ def side_index(self, side: Literal["actual", "expected"], index: int) -> _Path:
82
+ """Into a position only one sequence has, once alignment has shifted the two apart."""
83
+ if type(self.piece) is not str:
84
+ return _Path(f"{self.text}{side}[{index}]", Step("index", index, side=side), self)
85
+ return _Path(f"{side}[{index}]", Step("index", index, side=side), self, "append")
86
+
87
+ def line(self, number: int) -> _Path:
88
+ """Into one line of a text or bytes diff, numbered from 1."""
89
+ return _Path(f"line {number}", Step("line", number), self)
90
+
91
+ def member(self, item: object, label: str) -> _Path:
92
+ """Into a member of a set, which has no position to name it by.
93
+
94
+ The text stays the label the set renderer groups on (``extra`` / ``missing``), because a member
95
+ has no coordinate to print. The step carries the member itself, which is the only handle on it.
96
+ """
97
+ return _Path(label, Step("item", item), self)
98
+
99
+ @property
100
+ def text(self) -> str:
101
+ """The location as a person reads it."""
102
+ return self._read()[0]
103
+
104
+ def _read(self) -> tuple[str, tuple[Step, ...]]:
105
+ """Both forms of the location: the text joined from the nearest hop that starts it, the steps from the root.
106
+
107
+ A hop that starts the text holds it whole. Every hop where something other than an exact `str` meets the
108
+ text is one: its text is worked out when it is taken, by 2.28.0's own f-string, whose ``__format__``,
109
+ ``__bool__`` and order a `str` subclass can tell apart from a join. Joining is only ever done over exact
110
+ strings, where the two agree.
111
+ """
112
+ parent = self.parent
113
+ if parent is _ROOT and self.step is not None:
114
+ # one hop from the root reads as the hop itself: the join below cost such an entry 420 ns more
115
+ return self.piece, (self.step,)
116
+ hops: list[_Path] = []
117
+ hop: _Path | None = self
118
+ while hop is not None:
119
+ hops.append(hop)
120
+ hop = hop.parent
121
+ steps = tuple(hop.step for hop in reversed(hops) if hop.step is not None)
122
+ start = next(position for position, hop in enumerate(hops) if hop.joint == "reset")
123
+ if start == 0:
124
+ return self.piece, steps
125
+ pieces = [hops[start].piece]
126
+ written = bool(pieces[0])
127
+ for hop in reversed(hops[:start]):
128
+ if hop.joint == "dotted" and written:
129
+ pieces.append(".")
130
+ pieces.append(hop.piece)
131
+ written = written or bool(hop.piece)
132
+ return "".join(pieces), steps
133
+
134
+ def entry(
135
+ self,
136
+ *,
137
+ actual: object = None,
138
+ expected: object = None,
139
+ absent: Literal["actual", "expected"] | None = None,
140
+ ) -> DiffEntry:
141
+ """A `DiffEntry` at this location, carrying both forms of it."""
142
+ text, steps = self._read()
143
+ return DiffEntry(path=text, steps=steps, actual=actual, expected=expected, absent=absent)
144
+
145
+ def leaf_entry(
146
+ self,
147
+ *,
148
+ actual: object = None,
149
+ expected: object = None,
150
+ absent: Literal["actual", "expected"] | None = None,
151
+ ) -> DiffEntry:
152
+ """A `DiffEntry` for the whole value, whose text at the root is ``.`` rather than empty."""
153
+ text, steps = self._read()
154
+ return DiffEntry(path=text or ".", steps=steps, actual=actual, expected=expected, absent=absent)
155
+
156
+
157
+ _ROOT: _Path = _Path()
158
+ """The value under comparison itself, before any hop into it."""