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.
- assertpy/__init__.py +87 -0
- assertpy/_clustering.py +511 -0
- assertpy/_dangling.py +505 -0
- assertpy/_engine/__init__.py +1 -0
- assertpy/_engine/_builder_check_typing.py +1365 -0
- assertpy/_engine/_capable_typing.py +772 -0
- assertpy/_engine/_check_typing.py +753 -0
- assertpy/_engine/_compare.py +983 -0
- assertpy/_engine/_compat.py +38 -0
- assertpy/_engine/_contract.py +1926 -0
- assertpy/_engine/_diff.py +1058 -0
- assertpy/_engine/_equality.py +843 -0
- assertpy/_engine/_introspection.py +729 -0
- assertpy/_engine/_membership.py +522 -0
- assertpy/_engine/_mixin_base.py +258 -0
- assertpy/_engine/_negated_typing.py +2176 -0
- assertpy/_engine/_operations.py +75 -0
- assertpy/_engine/_ordering.py +884 -0
- assertpy/_engine/_pairing.py +70 -0
- assertpy/_engine/_path.py +158 -0
- assertpy/_engine/_poll_typing.py +6039 -0
- assertpy/_engine/_require.py +312 -0
- assertpy/_engine/_size.py +31 -0
- assertpy/_engine/_text.py +39 -0
- assertpy/_engine/_typing.py +1226 -0
- assertpy/_hints.py +1213 -0
- assertpy/_inline.py +484 -0
- assertpy/_matcher_impls.py +1876 -0
- assertpy/_passes.py +249 -0
- assertpy/_renamed.py +64 -0
- assertpy/_satisfies.py +835 -0
- assertpy/_snapshot_codec.py +290 -0
- assertpy/assertpy.py +2640 -0
- assertpy/async_assertions.py +864 -0
- assertpy/base.py +862 -0
- assertpy/behave_matchers.py +96 -0
- assertpy/bytes_mixin.py +158 -0
- assertpy/collection.py +528 -0
- assertpy/contains.py +1068 -0
- assertpy/dataframe.py +200 -0
- assertpy/date.py +327 -0
- assertpy/dict.py +249 -0
- assertpy/dynamic.py +222 -0
- assertpy/errors.py +782 -0
- assertpy/exception.py +838 -0
- assertpy/extracting.py +266 -0
- assertpy/file.py +324 -0
- assertpy/helpers.py +747 -0
- assertpy/http_mixin.py +218 -0
- assertpy/json_mixin.py +1265 -0
- assertpy/matchers.py +639 -0
- assertpy/numeric.py +696 -0
- assertpy/outcome.py +163 -0
- assertpy/py.typed +0 -0
- assertpy/pytest_plugin.py +1650 -0
- assertpy/snapshot.py +1031 -0
- assertpy/string.py +803 -0
- assertpy/warning.py +137 -0
- assertpy-2.35.0.dist-info/METADATA +324 -0
- assertpy-2.35.0.dist-info/RECORD +63 -0
- assertpy-2.35.0.dist-info/WHEEL +4 -0
- assertpy-2.35.0.dist-info/entry_points.txt +2 -0
- assertpy-2.35.0.dist-info/licenses/LICENSE +28 -0
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
"""Membership as one decision: which of the wanted items a value does not contain.
|
|
2
|
+
|
|
3
|
+
`contains` is the second relation that a builder could express and a matcher could not. A structural
|
|
4
|
+
spec had `each_item` (every element matches) and `is_in` (the value is one of these), but no way to say
|
|
5
|
+
the plain thing: *this collection contains that*. Writing it a second time inside the matcher would
|
|
6
|
+
have repeated the part that is easy to get subtly wrong, since membership is not one rule: a mapping is
|
|
7
|
+
searched by key, a matcher argument is satisfied by any element rather than compared to the whole, and a
|
|
8
|
+
one-shot iterator has to be drained before it is searched more than once.
|
|
9
|
+
|
|
10
|
+
So the decision moves here and both spellings ask it. What stays outside is the wording: the builder
|
|
11
|
+
turns the answer into a message with a closest-element hint, and a matcher turns it into a verdict.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import datetime
|
|
17
|
+
import decimal
|
|
18
|
+
from collections import Counter
|
|
19
|
+
from collections.abc import Iterable
|
|
20
|
+
from typing import TYPE_CHECKING, Any, NoReturn, cast
|
|
21
|
+
|
|
22
|
+
from ._introspection import definition_of, is_mapping_like, materialized
|
|
23
|
+
from ._ordering import (
|
|
24
|
+
_PLAIN,
|
|
25
|
+
MEMBERSHIP_REFUSALS,
|
|
26
|
+
REFUSALS,
|
|
27
|
+
equals,
|
|
28
|
+
lookup,
|
|
29
|
+
may_broadcast,
|
|
30
|
+
member,
|
|
31
|
+
member_past,
|
|
32
|
+
mixes_broadcasting,
|
|
33
|
+
)
|
|
34
|
+
from ._require import raised_inside, verdict
|
|
35
|
+
|
|
36
|
+
if TYPE_CHECKING:
|
|
37
|
+
from collections.abc import Callable, Sequence
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def searchable(value: object) -> Any:
|
|
41
|
+
"""*value* in a form that can be searched more than once.
|
|
42
|
+
|
|
43
|
+
A generator is consumed by the first pass, and membership is tested once per wanted item. A value
|
|
44
|
+
that cannot be iterated at all is handed back untouched, which is why the parameter is `object`:
|
|
45
|
+
deciding whether membership can be asked of it is `is_searchable`'s job, not this one's.
|
|
46
|
+
|
|
47
|
+
Only a one-shot iterator is copied. A list, set, tuple or mapping is handed back as it is, so a
|
|
48
|
+
matcher built from one stays a live view of it, the way `match.equal_to(expected)` does:
|
|
49
|
+
|
|
50
|
+
known = [1]
|
|
51
|
+
constraint = match.is_subset_of(known)
|
|
52
|
+
known.append(2)
|
|
53
|
+
constraint.matches([2]) # True: the same list, not a snapshot of it
|
|
54
|
+
|
|
55
|
+
The copy for an iterator is therefore not a snapshot policy but the only way to answer twice at all,
|
|
56
|
+
and it is eager: an endless iterator handed in as the expected operand never finishes being drained.
|
|
57
|
+
"""
|
|
58
|
+
return materialized(cast("Iterable[Any]", value))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def is_searchable(value: object) -> bool:
|
|
62
|
+
"""Whether membership can be asked of *value*: `in` needs a container or an iterable.
|
|
63
|
+
|
|
64
|
+
Wider than `is_walkable` on purpose, and the two are not interchangeable. A value that answers
|
|
65
|
+
`__contains__` and nothing else can be asked whether it holds something, and cannot be listed: it
|
|
66
|
+
fits `contains` and does not fit `contains_only`, which has to see everything that is there.
|
|
67
|
+
"""
|
|
68
|
+
# the builtins skip the ABC's 88 ns walk; each has `__contains__`, so the probe below said yes to them anyway
|
|
69
|
+
return isinstance(value, (list, tuple, str, bytes, dict, set, frozenset, Iterable)) or hasattr(
|
|
70
|
+
type(value), "__contains__"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def is_walkable(value: object) -> bool:
|
|
75
|
+
"""Whether *value* can be listed, which is what "only these" and "a subset of" both need."""
|
|
76
|
+
return isinstance(value, Iterable)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# types whose `__hash__` and `__eq__` agree; `bytearray` looks like one and turned `contains_only` into TypeError
|
|
80
|
+
_HASH_SAFE = frozenset(
|
|
81
|
+
{
|
|
82
|
+
int,
|
|
83
|
+
float,
|
|
84
|
+
complex,
|
|
85
|
+
bool,
|
|
86
|
+
str,
|
|
87
|
+
bytes,
|
|
88
|
+
frozenset,
|
|
89
|
+
type(None),
|
|
90
|
+
# `Fraction` qualifies and is left out: `fractions` drags in thirteen modules at import
|
|
91
|
+
decimal.Decimal,
|
|
92
|
+
datetime.date,
|
|
93
|
+
datetime.datetime,
|
|
94
|
+
datetime.time,
|
|
95
|
+
datetime.timedelta,
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# measured 2x slower at ten elements and several times faster at a hundred
|
|
101
|
+
_WALK_UNDER = 400
|
|
102
|
+
|
|
103
|
+
# the one safe-list type that refuses to hash for a single value: `Decimal("snan")` raises
|
|
104
|
+
_HASH_MAY_REFUSE = frozenset({decimal.Decimal})
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _worth_hashing(container: Any, probes: Any) -> bool:
|
|
108
|
+
"""Whether the pair is large enough that a set can win at all, asked before anything expensive.
|
|
109
|
+
|
|
110
|
+
The comparison count is what decides, so the two questions differ: searching one collection for the
|
|
111
|
+
elements of another costs `len * len`, while looking for a repeat inside one collection costs about
|
|
112
|
+
`len` squared over two but pays for only one set. Passing the same collection twice is how the
|
|
113
|
+
second question spells itself.
|
|
114
|
+
"""
|
|
115
|
+
try:
|
|
116
|
+
return len(container) * max(len(probes), 1) >= _WALK_UNDER
|
|
117
|
+
except TypeError: # no length: an iterator or a view, where the walk cost is unknown but not small
|
|
118
|
+
return True
|
|
119
|
+
except Exception: # a length refused: the walk never sizes the value, and this is a cost, not a verdict
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _kinds(items: Any) -> set[type] | None:
|
|
124
|
+
"""The distinct types in *items*, or ``None`` when even that cannot be asked.
|
|
125
|
+
|
|
126
|
+
Collected in the interpreter's own loop rather than a Python one: over ten thousand elements the loop
|
|
127
|
+
would cost more than the set it is deciding about, and what is left to judge is one entry per type,
|
|
128
|
+
which is almost always one.
|
|
129
|
+
|
|
130
|
+
Asking can itself fail, which is not hypothetical: a class whose metaclass declares `__hash__ = None`
|
|
131
|
+
cannot go into a set although its instances compare perfectly well. Unanswerable means "no".
|
|
132
|
+
"""
|
|
133
|
+
try:
|
|
134
|
+
return set(map(type, items))
|
|
135
|
+
except TypeError: # the class objects themselves refuse to be hashed
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _safe(kinds: set[type] | None) -> bool:
|
|
140
|
+
"""Whether every one of *kinds* hashes and compares by the same rule."""
|
|
141
|
+
return kinds is not None and all(_agrees(kind) for kind in kinds)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _agrees(kind: type) -> bool:
|
|
145
|
+
"""Whether this type's ``==`` and ``hash()`` answer consistently, so a set may stand in for a walk.
|
|
146
|
+
|
|
147
|
+
Hashability is asked of the type, not tried on the value: an unhashable type carries
|
|
148
|
+
`__hash__ = None`. It is not implied by the rest of the rule, since a class can inherit identity
|
|
149
|
+
equality and still declare itself unhashable, and without this the shortcut raised where it answered.
|
|
150
|
+
|
|
151
|
+
The third clause is what admits a subclass. A name on the safe list is not enough on its own,
|
|
152
|
+
because that list holds exact types, and `IntEnum`, `StrEnum` and a plain `class Money(int)` all
|
|
153
|
+
took the quadratic walk although they answer exactly as their base does: four thousand of them cost
|
|
154
|
+
30 ms against 0.09 ms. Asked of the owner of each definition rather than of the definition itself,
|
|
155
|
+
so a class that assigns `__hash__ = str.__hash__` beside an inherited `int.__eq__` is refused: its
|
|
156
|
+
owner is the class, not a safe type. A base cannot supply the two from different safe types either,
|
|
157
|
+
since inheriting from two of them is a layout conflict the interpreter refuses.
|
|
158
|
+
"""
|
|
159
|
+
return kind.__hash__ is not None and (
|
|
160
|
+
kind in _HASH_SAFE
|
|
161
|
+
or kind.__eq__ is object.__eq__
|
|
162
|
+
or (_owned_by_a_safe_type(kind, "__eq__") and _owned_by_a_safe_type(kind, "__hash__"))
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _owned_by_a_safe_type(kind: type, name: str) -> bool:
|
|
167
|
+
"""Whether *kind* inherits *name* unchanged from one of the types on the safe list."""
|
|
168
|
+
found = definition_of(kind, name)
|
|
169
|
+
return found is not None and found[0] in _HASH_SAFE
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _hash_safe(items: Any) -> bool:
|
|
173
|
+
"""Whether *items* may be indexed. Kept as a name of its own because the tests read better with it."""
|
|
174
|
+
return _safe(_kinds(items))
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class MembershipRefusedError(TypeError):
|
|
178
|
+
"""The operands refuse the question: this item cannot be searched for in this value at all.
|
|
179
|
+
|
|
180
|
+
Its own type so a matcher can answer "no match" to it, while a `TypeError` raised inside the value's
|
|
181
|
+
own `__contains__` or `__eq__` still reaches the reader as the bug in that value it is. Raised only for
|
|
182
|
+
a caller that asks with ``marked=True``: a builder's caller gets the operator's own `TypeError`, as a
|
|
183
|
+
single item does, where the class used to name a private module whenever two or more were asked.
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _told_apart(pairs: Sequence[tuple[Any, Any]], original: TypeError, *, marked: bool) -> NoReturn:
|
|
188
|
+
"""Ask again one expression at a time, so an operand refusal can be told from a bug in their code.
|
|
189
|
+
|
|
190
|
+
Reached only where a walk has already raised, so the second ask costs a passing run nothing. Asked
|
|
191
|
+
whole, the walk is several frames deep and `raised_inside` cannot see which frame tried the operator;
|
|
192
|
+
asked one `in` at a time, it reads exactly that.
|
|
193
|
+
"""
|
|
194
|
+
for items, container in pairs:
|
|
195
|
+
for item in items:
|
|
196
|
+
try:
|
|
197
|
+
if item in container:
|
|
198
|
+
continue
|
|
199
|
+
except (decimal.InvalidOperation, OverflowError):
|
|
200
|
+
continue # a signalling NaN or a `numpy` overflow, which `member` answers: not the refusal sought
|
|
201
|
+
except TypeError as refusal:
|
|
202
|
+
if raised_inside(refusal):
|
|
203
|
+
raise
|
|
204
|
+
if _answered(item, container):
|
|
205
|
+
continue
|
|
206
|
+
if not marked:
|
|
207
|
+
raise
|
|
208
|
+
raise MembershipRefusedError(str(refusal)) from refusal
|
|
209
|
+
raise original
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _answered(item: Any, container: Any) -> bool:
|
|
213
|
+
"""Whether `member` answers what ``in`` refused, as it does a `Decimal` against a `numpy` integer."""
|
|
214
|
+
try:
|
|
215
|
+
member(item, container)
|
|
216
|
+
except TypeError:
|
|
217
|
+
return False
|
|
218
|
+
return True
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def missing_items(
|
|
222
|
+
value: Any, items: Sequence[Any], is_matcher: Callable[[object], bool], *, marked: bool = False
|
|
223
|
+
) -> list[Any]:
|
|
224
|
+
"""Which of *items* are not in *value*, in the order they were asked for.
|
|
225
|
+
|
|
226
|
+
A matcher among the items is satisfied by any single element, which is what makes this more than
|
|
227
|
+
`item in value`: `contains(match.greater_than(3))` asks whether *some* element is greater than 3,
|
|
228
|
+
not whether the collection holds that matcher object.
|
|
229
|
+
|
|
230
|
+
The matcher test is passed in rather than imported: matchers are a layer above this one, and having
|
|
231
|
+
the core reach up for them would make the dependency circular.
|
|
232
|
+
"""
|
|
233
|
+
walked = materialized(value)
|
|
234
|
+
wanted = materialized(items)
|
|
235
|
+
present = _index(walked, wanted) if isinstance(walked, (list, tuple)) else None
|
|
236
|
+
return _absent_from(walked if present is None else present, wanted, is_matcher, walked, marked)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _absent_from(
|
|
240
|
+
present: Any, items: Any, is_matcher: Callable[[object], bool], walked: Any, marked: bool
|
|
241
|
+
) -> list[Any]:
|
|
242
|
+
"""The loop itself, so the fast and the walking form stay one piece of logic rather than two.
|
|
243
|
+
|
|
244
|
+
A refusal is named by the item it came from. A matcher is asked of the elements and never by ``in``, so an
|
|
245
|
+
error of its own is its own and leaves as it was raised; an item `member` refused is asked again alone.
|
|
246
|
+
"""
|
|
247
|
+
absent = []
|
|
248
|
+
# the index `_index` built holds only types that hash and compare alike, which no `numpy` scalar or list is
|
|
249
|
+
verify = present is walked
|
|
250
|
+
for item in items:
|
|
251
|
+
if is_matcher(item):
|
|
252
|
+
if not any(verdict(item.matches(element), subject="the matcher") for element in walked):
|
|
253
|
+
absent.append(item)
|
|
254
|
+
continue
|
|
255
|
+
try:
|
|
256
|
+
if not _held(item, present, verify):
|
|
257
|
+
absent.append(item)
|
|
258
|
+
except TypeError as refusal:
|
|
259
|
+
_told_apart((((item,), present),), refusal, marked=marked)
|
|
260
|
+
return absent
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _held(item: Any, present: Any, verify: bool) -> bool:
|
|
264
|
+
"""`member`, asking ``in`` itself where no item that may broadcast can come: through it cost 100 items 11%."""
|
|
265
|
+
if not verify or type(item) in _PLAIN:
|
|
266
|
+
try:
|
|
267
|
+
return item in present
|
|
268
|
+
except MEMBERSHIP_REFUSALS as refusal:
|
|
269
|
+
found = member_past(item, present, refusal)
|
|
270
|
+
if found is None:
|
|
271
|
+
raise
|
|
272
|
+
return found
|
|
273
|
+
return member(item, present, verify)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def only_faults(value: Any, items: Sequence[Any], *, marked: bool = False) -> tuple[list[Any], list[Any]]:
|
|
277
|
+
"""``(extra, missing)`` for "contains only these": what is there but unwanted, and what is absent.
|
|
278
|
+
|
|
279
|
+
Both halves at once, because reporting only the extras sends the reader to fix one problem and rerun
|
|
280
|
+
into the other. A matcher looks at whether either list is non-empty; the assertion words them.
|
|
281
|
+
"""
|
|
282
|
+
# walked three times below, and idempotent for the callers that already did it
|
|
283
|
+
walked, wanted_items = materialized(value), materialized(items)
|
|
284
|
+
# each side is searched for the other, and building both together is what makes either refusal fall back
|
|
285
|
+
# only over sequences, as the other two searches are: a set of a string's characters answers by character
|
|
286
|
+
# where the walk answers by substring, and the verdict then moved with the length of the string
|
|
287
|
+
indexable = isinstance(walked, (list, tuple)) and isinstance(wanted_items, (list, tuple))
|
|
288
|
+
both = _index_both(wanted_items, walked) if indexable else None
|
|
289
|
+
try:
|
|
290
|
+
if both is None:
|
|
291
|
+
return _without(walked, wanted_items), _without(wanted_items, walked)
|
|
292
|
+
wanted, present = both
|
|
293
|
+
extra = _without(walked, wanted)
|
|
294
|
+
missing = _without(wanted_items, present)
|
|
295
|
+
except TypeError as refusal:
|
|
296
|
+
_told_apart(((walked, wanted_items), (wanted_items, walked)), refusal, marked=marked)
|
|
297
|
+
return extra, missing
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _without(items: Iterable[Any], container: Any) -> list[Any]:
|
|
301
|
+
"""The *items* not in *container*, asked by ``in`` and, once that raised, as `member` asks it.
|
|
302
|
+
|
|
303
|
+
A `TypeError` is asked again too, since a signalling NaN refuses to hash for a set: `member` answers that
|
|
304
|
+
one and hands every other refusal on.
|
|
305
|
+
"""
|
|
306
|
+
# a walk of a sequence is quadratic, so the pass that finds a `numpy` scalar meeting a list costs it nothing
|
|
307
|
+
if isinstance(container, (list, tuple)) and mixes_broadcasting(items, container):
|
|
308
|
+
return [item for item in items if not member(item, container)]
|
|
309
|
+
try:
|
|
310
|
+
return [item for item in items if item not in container]
|
|
311
|
+
except REFUSALS:
|
|
312
|
+
return [item for item in items if not member(item, container)]
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def has_duplicates(values: Sequence[Any]) -> bool:
|
|
316
|
+
"""Whether any element appears twice, by the same equality the rest of membership uses."""
|
|
317
|
+
if _classified_alone(values):
|
|
318
|
+
try:
|
|
319
|
+
# what a set replaces is a growing list of seen elements, quadratic from the first few
|
|
320
|
+
return len(set(values)) != len(values)
|
|
321
|
+
except Exception: # a value refused to hash after all; only hashing is inside this `try`
|
|
322
|
+
pass
|
|
323
|
+
if mixes_broadcasting(values):
|
|
324
|
+
return any(member(value, values[:index]) for index, value in enumerate(values))
|
|
325
|
+
# `in` rather than a generator of `==`: the interpreter asks it, and it short-circuits on identity
|
|
326
|
+
seen: list[Any] = []
|
|
327
|
+
try:
|
|
328
|
+
for value in values:
|
|
329
|
+
if value in seen:
|
|
330
|
+
return True
|
|
331
|
+
seen.append(value)
|
|
332
|
+
except REFUSALS:
|
|
333
|
+
return any(member(value, values[:index]) for index, value in enumerate(values))
|
|
334
|
+
return False
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def occurrences(values: Sequence[Any], items: Sequence[Any]) -> list[int]:
|
|
338
|
+
"""How often each of *items* appears in *values*, in the order they were asked for.
|
|
339
|
+
|
|
340
|
+
`list.count()` per item walks the whole collection once per item: a thousand items over ten thousand
|
|
341
|
+
elements is ten million comparisons, and the failure report asked for another pass. Where both sides
|
|
342
|
+
are safe to hash, by the same rule membership uses, one `Counter` answers all of them.
|
|
343
|
+
|
|
344
|
+
The probes are hashed inside the guard and looked up outside it, as `_index` does: a lookup runs
|
|
345
|
+
`__eq__` on a collision, and a comparison that raises is a bug in the value rather than a count.
|
|
346
|
+
"""
|
|
347
|
+
counted = None
|
|
348
|
+
if _classified(values, items):
|
|
349
|
+
try:
|
|
350
|
+
counted = Counter(values)
|
|
351
|
+
for item in items:
|
|
352
|
+
hash(item)
|
|
353
|
+
except TypeError: # a value that refuses to hash despite its type, such as a signalling NaN
|
|
354
|
+
counted = None
|
|
355
|
+
if counted is None:
|
|
356
|
+
return [_count(values, item) for item in items]
|
|
357
|
+
return [counted[item] for item in items]
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _count(values: Sequence[Any], item: Any) -> int:
|
|
361
|
+
"""``values.count(item)``, each element asked as `equals` asks it once ``count`` raised."""
|
|
362
|
+
if may_broadcast(item) and mixes_broadcasting(values, (item,)):
|
|
363
|
+
return sum(1 for value in values if value is item or equals(value, item))
|
|
364
|
+
try:
|
|
365
|
+
return values.count(item)
|
|
366
|
+
except REFUSALS as refusal:
|
|
367
|
+
if raised_inside(refusal):
|
|
368
|
+
raise
|
|
369
|
+
return sum(1 for value in values if value is item or equals(value, item))
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def repeated_counts(values: Sequence[Any]) -> list[tuple[Any, int]]:
|
|
373
|
+
"""Each element appearing more than once with how often, in order of first appearance.
|
|
374
|
+
|
|
375
|
+
Counting with `list.count()` per element re-walks the whole sequence every time, so naming the
|
|
376
|
+
duplicates in a collection of a few thousand costs as much as the assertion it explains. Where the
|
|
377
|
+
elements are safe to hash (same rule as membership uses), one pass counts them all.
|
|
378
|
+
"""
|
|
379
|
+
counts = None
|
|
380
|
+
if _classified_alone(values):
|
|
381
|
+
try:
|
|
382
|
+
counts = Counter(values)
|
|
383
|
+
except TypeError: # a value that refuses to hash despite its type, such as a signalling NaN
|
|
384
|
+
counts = None
|
|
385
|
+
if counts is None:
|
|
386
|
+
named: list[tuple[Any, int]] = []
|
|
387
|
+
for value in values:
|
|
388
|
+
total = _count(values, value)
|
|
389
|
+
if total > 1 and not any(earlier is value or equals(earlier, value) for earlier, _ in named):
|
|
390
|
+
named.append((value, total))
|
|
391
|
+
return named
|
|
392
|
+
seen: set[Any] = set()
|
|
393
|
+
repeated = []
|
|
394
|
+
for value in values:
|
|
395
|
+
if counts[value] > 1 and value not in seen:
|
|
396
|
+
seen.add(value)
|
|
397
|
+
repeated.append((value, counts[value]))
|
|
398
|
+
return repeated
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def repeated_items(values: Sequence[Any]) -> list[Any]:
|
|
402
|
+
"""Which elements appear more than once, each named once, in order of first appearance."""
|
|
403
|
+
return [value for value, _count in repeated_counts(values)]
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def flattened_supersets(supersets: Sequence[Any]) -> list[Any]:
|
|
407
|
+
"""The supersets of `is_subset_of` as one collection of items, which is what the value is searched in.
|
|
408
|
+
|
|
409
|
+
A superset that cannot be iterated is a single item, and a string is its characters, which is what
|
|
410
|
+
``in`` over a flattened list answers. The matcher used to keep the superset whole, so a string
|
|
411
|
+
superset answered substring membership and a `set` superset hashed items that refuse to hash.
|
|
412
|
+
"""
|
|
413
|
+
collected: list[Any] = []
|
|
414
|
+
for superset in supersets:
|
|
415
|
+
try:
|
|
416
|
+
collected.extend(materialized(superset))
|
|
417
|
+
except TypeError: # noqa: PERF203 # a non-iterable superset is one item, as the assertion has always read it
|
|
418
|
+
collected.append(superset)
|
|
419
|
+
return collected
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def subset_faults(value: Any, supersets: Sequence[Any]) -> list[Any]:
|
|
423
|
+
"""What no superset holds: the pairs of a mapping value, or the elements of anything else.
|
|
424
|
+
|
|
425
|
+
A pair holds when any one superset holds it, rather than when the mapping left after merging them
|
|
426
|
+
does: merging let a later superset overwrite an earlier one, so the verdict moved with the order the
|
|
427
|
+
supersets were written in.
|
|
428
|
+
"""
|
|
429
|
+
if is_mapping_like(value):
|
|
430
|
+
pairs = [(key, value[key]) for key in value]
|
|
431
|
+
return [{key: item} for key, item in pairs if not _pair_held(key, item, supersets)]
|
|
432
|
+
return not_contained_in(list(materialized(value)), flattened_supersets(supersets))
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _pair_held(key: Any, item: Any, supersets: Sequence[Any]) -> bool:
|
|
436
|
+
"""Whether one key/value pair is in any of the supersets, by identity first, as a container asks."""
|
|
437
|
+
for superset in supersets:
|
|
438
|
+
try:
|
|
439
|
+
if not is_mapping_like(superset) or key not in superset:
|
|
440
|
+
continue
|
|
441
|
+
held = superset[key]
|
|
442
|
+
except REFUSALS as refusal:
|
|
443
|
+
found, held = lookup(superset, key, refusal)
|
|
444
|
+
if not found:
|
|
445
|
+
continue
|
|
446
|
+
if held is item or equals(held, item):
|
|
447
|
+
return True
|
|
448
|
+
return False
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def not_contained_in(value: Any, container: Any) -> list[Any]:
|
|
452
|
+
"""Which elements of *value* the *container* does not hold, for "is a subset of"."""
|
|
453
|
+
value, container = materialized(value), materialized(container)
|
|
454
|
+
indexed = _index(container, value) if isinstance(container, (list, tuple)) else None
|
|
455
|
+
allowed = container if indexed is None else indexed
|
|
456
|
+
return _without(value, allowed)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _classified(container: Any, probes: Any) -> bool:
|
|
460
|
+
"""Whether a set may stand in for the walk here: worth the preparation, and safe to build."""
|
|
461
|
+
return _worth_hashing(container, probes) and _hash_safe(container) and _hash_safe(probes)
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _index(container: Any, probes: Any) -> set[Any] | None:
|
|
465
|
+
"""*container* as a set, or ``None`` when the walk has to answer instead.
|
|
466
|
+
|
|
467
|
+
Everything that can raise is inside, and nothing else is: building the set hashes the container, and
|
|
468
|
+
the probes are asked separately only where a classified type is still allowed to refuse, because
|
|
469
|
+
`x in some_set` hashes `x` too. The search itself always runs outside any `except`.
|
|
470
|
+
|
|
471
|
+
Asking every probe unconditionally was the first shape of this, and it hashed the same values twice
|
|
472
|
+
on the successful path, which is a pass over the whole collection for `is_subset_of`.
|
|
473
|
+
|
|
474
|
+
That boundary is the point. A wider `try` around the search would swallow a failing comparison or a
|
|
475
|
+
matcher raising from user code, and this library treats those as bugs in the test that must travel
|
|
476
|
+
out, not as verdicts. What is caught here is narrower and real: a type may pass the rule and still
|
|
477
|
+
hold a value that refuses to hash, and before this shortcut existed nothing hashed it at all.
|
|
478
|
+
|
|
479
|
+
Two limits, both deliberate. Only subclasses of `Exception` are caught, so a `KeyboardInterrupt`
|
|
480
|
+
during hashing still stops the run, as it should. And a `__hash__` that answers once and raises
|
|
481
|
+
later is not covered by anything here: a hash is required to be deterministic, and a value that
|
|
482
|
+
breaks that contract is left to break it.
|
|
483
|
+
"""
|
|
484
|
+
container_kinds, probe_kinds = _kinds(container), _kinds(probes)
|
|
485
|
+
if not (_worth_hashing(container, probes) and _safe(container_kinds) and _safe(probe_kinds)):
|
|
486
|
+
return None
|
|
487
|
+
try:
|
|
488
|
+
indexed = set(container)
|
|
489
|
+
if probe_kinds is not None and (probe_kinds & _HASH_MAY_REFUSE or not probe_kinds <= _HASH_SAFE):
|
|
490
|
+
# anything but an exact builtin may still refuse: a `Decimal("snan")`, or a class of one's own
|
|
491
|
+
# that inherits identity equality and writes a `__hash__` that raises
|
|
492
|
+
for probe in probes:
|
|
493
|
+
hash(probe)
|
|
494
|
+
except Exception: # any Exception raised while hashing means the walk answers instead
|
|
495
|
+
return None
|
|
496
|
+
return indexed
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _index_both(left: Any, right: Any) -> tuple[set[Any], set[Any]] | None:
|
|
500
|
+
"""Both collections as sets, or nothing.
|
|
501
|
+
|
|
502
|
+
Where each side is searched for the other, building both is already the whole question: a value that
|
|
503
|
+
refuses to hash stops one of the two constructions, and there is nothing left to check separately.
|
|
504
|
+
Asking `_index` twice would hash every element twice over for no added safety.
|
|
505
|
+
"""
|
|
506
|
+
if not _classified(left, right):
|
|
507
|
+
return None
|
|
508
|
+
try:
|
|
509
|
+
return set(left), set(right)
|
|
510
|
+
except Exception: # either side refusing is enough, and neither index is used then
|
|
511
|
+
return None
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def _classified_alone(values: Any) -> bool:
|
|
515
|
+
"""The same decision for a question asked of one collection, where only one set gets built.
|
|
516
|
+
|
|
517
|
+
Kept apart from `_classified` because the arithmetic differs, and differs in the other direction than
|
|
518
|
+
it first looks: what a set replaces here is a growing list of seen elements, which is quadratic from
|
|
519
|
+
the very first elements. A size threshold made this *slower* at thirty elements, so there is none:
|
|
520
|
+
the only question is whether the elements may be hashed at all.
|
|
521
|
+
"""
|
|
522
|
+
return _hash_safe(values)
|