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,884 @@
|
|
|
1
|
+
"""Ordering as one decision: can these two be ordered, and if so, how do they compare.
|
|
2
|
+
|
|
3
|
+
The comparison itself is an operator and needs no core. What did need one is the rule *around* it,
|
|
4
|
+
because it was written twice and in two different shapes. The builder listed types (`complex` has no
|
|
5
|
+
ordering, a `datetime` wants a `datetime`, a number wants a number) and then tried the operator; the
|
|
6
|
+
matcher tried the operator and answered False on `TypeError`. Two spellings of one rule is exactly how
|
|
7
|
+
`bytes` drifted apart in the text matchers, and it is a matter of time rather than of luck.
|
|
8
|
+
|
|
9
|
+
`compare` gives both callers the same three answers: ordered one way, ordered the other, or not
|
|
10
|
+
orderable at all. What each does with "not orderable" stays theirs, and stays different on purpose: a
|
|
11
|
+
builder refuses the call, because a wrong subject there is a mistake in the test, while a matcher
|
|
12
|
+
answers "no match", because it feeds `==` and the combinators where raising would be wrong.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import collections
|
|
18
|
+
import collections.abc
|
|
19
|
+
import decimal
|
|
20
|
+
import fractions
|
|
21
|
+
import inspect
|
|
22
|
+
import itertools
|
|
23
|
+
import numbers
|
|
24
|
+
import operator
|
|
25
|
+
import sys
|
|
26
|
+
import types
|
|
27
|
+
from datetime import date, datetime, time, timedelta
|
|
28
|
+
from typing import TYPE_CHECKING, Any
|
|
29
|
+
|
|
30
|
+
from ._require import argument, pure_decimal_code, raised_inside, raised_on_purpose, refuse
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from collections.abc import Callable, Iterable, Sequence
|
|
34
|
+
|
|
35
|
+
__tracebackhide__ = raised_on_purpose
|
|
36
|
+
|
|
37
|
+
# ordering exists for real numbers and not for complex ones, whatever `numbers.Number` says
|
|
38
|
+
_UNORDERED = frozenset({complex})
|
|
39
|
+
# types whose ordering needs no rule at all: identical on both sides, total, and not kind-bound
|
|
40
|
+
_PLAIN = frozenset({int, float, str, bytes})
|
|
41
|
+
_DIRECT = {"lt": operator.lt, "le": operator.le, "gt": operator.gt, "ge": operator.ge}
|
|
42
|
+
_SEARCHED_BY_EQUALITY = frozenset({list, tuple, collections.deque, type({}.values())})
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class UnorderableError(Exception):
|
|
46
|
+
"""The pair cannot be ordered, with the reason the caller needs to explain it.
|
|
47
|
+
|
|
48
|
+
``kind`` is one of ``"value"`` (this value has no ordering at all), ``"kind"`` (both are ordered,
|
|
49
|
+
but not against each other's type) or ``"pair"`` (the operator itself refused them).
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self, kind: str, *, wanted: type | None = None) -> None:
|
|
53
|
+
super().__init__(kind)
|
|
54
|
+
self.kind = kind
|
|
55
|
+
self.wanted = wanted
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def nan_operand(value: Any) -> bool:
|
|
59
|
+
"""A `float`, `Decimal` or `numpy` float NaN, unordered against everything, the `Decimal` one by signalling.
|
|
60
|
+
|
|
61
|
+
Asked by type rather than through `math.isnan`, which would call `__float__` on somebody else's
|
|
62
|
+
number and raises on a signalling `Decimal` instead of answering.
|
|
63
|
+
|
|
64
|
+
Through the base type's own operator and method rather than the value's, the rule `_is_infinite`
|
|
65
|
+
already follows: this answer decides whether an `InvalidOperation` is a verdict or a bug in the
|
|
66
|
+
value, so a subclass overriding `is_nan` could have its own signal absorbed, and one overriding
|
|
67
|
+
`__ne__` could call itself unordered against everything. A `numpy` float other than `float64` is no
|
|
68
|
+
`float`, and is read through its own type's `as_integer_ratio` as the exact order reads it: missed, it
|
|
69
|
+
let a `Decimal`'s signal out and passed `is_sorted` over a list a `float` NaN fails. Subclasses
|
|
70
|
+
included, and only off a class built by `type` itself, since a metaclass of anybody's answers reads. An
|
|
71
|
+
integer, built in or registered, is never one, and is not read at all.
|
|
72
|
+
|
|
73
|
+
A class that holds no number this reads is kept once found, as `integral_kind` keeps its own, and one given
|
|
74
|
+
another `as_integer_ratio` after it was asked is not followed. Asked afresh, a text or a ``None`` cost 1.3 us,
|
|
75
|
+
and a failure asks this of both sides of every row it reports.
|
|
76
|
+
"""
|
|
77
|
+
if isinstance(value, float):
|
|
78
|
+
return bool(float.__ne__(value, value))
|
|
79
|
+
if isinstance(value, decimal.Decimal):
|
|
80
|
+
return decimal.Decimal.is_nan(value)
|
|
81
|
+
kind = type(value)
|
|
82
|
+
if kind is int or type(kind) is not type or kind in _UNREAD_KINDS or kind in _INTEGRAL_KINDS or integral_kind(kind):
|
|
83
|
+
return False
|
|
84
|
+
ratio = _known_number_method(kind, "as_integer_ratio", types.MethodDescriptorType)
|
|
85
|
+
if ratio is None:
|
|
86
|
+
if len(_UNREAD_KINDS) < 256:
|
|
87
|
+
_UNREAD_KINDS.add(kind)
|
|
88
|
+
return False
|
|
89
|
+
return _read_by(value, ratio) == "nan"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def integral_kind(kind: type) -> bool:
|
|
93
|
+
"""Whether a class built by `type` itself is a `numbers.Integral`, kept once found: a class never stops being one.
|
|
94
|
+
|
|
95
|
+
Asked of a `numpy` integer at every NaN and infinity question, where the ABC check cost an `int64` tie 11%. At
|
|
96
|
+
most 256 are kept, so classes made on the fly cannot grow it without bound, and an ABC registry cleared by hand
|
|
97
|
+
through its private API is not followed.
|
|
98
|
+
"""
|
|
99
|
+
if type(kind) is not type:
|
|
100
|
+
return False
|
|
101
|
+
if kind in _INTEGRAL_KINDS:
|
|
102
|
+
return True
|
|
103
|
+
if issubclass(kind, numbers.Integral):
|
|
104
|
+
if len(_INTEGRAL_KINDS) < 256:
|
|
105
|
+
_INTEGRAL_KINDS.add(kind)
|
|
106
|
+
return True
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
_INTEGRAL_KINDS: set[type] = set()
|
|
111
|
+
"""What `integral_kind` has found, only classes built by `type` itself, so hashed as `type` hashes."""
|
|
112
|
+
|
|
113
|
+
_UNREAD_KINDS: set[type] = set()
|
|
114
|
+
"""What `nan_operand` has found to hold no number it reads, kept and bounded as `_INTEGRAL_KINDS` is."""
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _kind_of(value: Any) -> type | None:
|
|
118
|
+
"""The date-and-time kind a value orders within, subclasses included, or ``None`` for anything else.
|
|
119
|
+
|
|
120
|
+
By kind rather than by exact type: `pandas.Timestamp` and `freezegun`'s clock are `datetime`
|
|
121
|
+
subclasses, and refusing them against a `datetime` boundary failed an assertion that holds.
|
|
122
|
+
"""
|
|
123
|
+
for kind in (datetime, date, timedelta, time): # `datetime` ahead of `date`, which it subclasses
|
|
124
|
+
if isinstance(value, kind):
|
|
125
|
+
return kind
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _equal(actual: Any, expected: Any) -> bool:
|
|
130
|
+
"""``==``, with a NaN on either side answering ``False`` rather than signalling."""
|
|
131
|
+
if nan_operand(actual) or nan_operand(expected):
|
|
132
|
+
return False
|
|
133
|
+
return equals(actual, expected)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def equals(actual: Any, expected: Any) -> bool:
|
|
137
|
+
"""``actual == expected`` as a verdict, where Python's own ``==`` would raise instead of answering.
|
|
138
|
+
|
|
139
|
+
A signalling `Decimal` NaN signals at ``==``, a `numpy` float overflows converting a Python int past its
|
|
140
|
+
range and a `Decimal` refuses a `numpy` integer, in a list or a mapping as much as on its own. A `numpy`
|
|
141
|
+
scalar against a list or a tuple answers an array, element by element (`broadcasts`), and is unequal to it
|
|
142
|
+
as a number is. Asked as written first, so every other pair keeps the answer and the cost it always had.
|
|
143
|
+
"""
|
|
144
|
+
try:
|
|
145
|
+
equal = actual == expected
|
|
146
|
+
except (decimal.InvalidOperation, OverflowError, TypeError, ValueError) as refusal:
|
|
147
|
+
return equal_past(actual, expected, refusal)
|
|
148
|
+
if type(equal) is not bool and type(actual) is not type(expected) and broadcasts(actual, expected, answer=equal):
|
|
149
|
+
return False
|
|
150
|
+
return bool(equal)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
_SEQUENCES = (list, tuple)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def numpy_duration(value: object) -> bool:
|
|
157
|
+
"""Whether *value* is a `numpy.timedelta64`, a duration `numpy` registers as an integer all the same."""
|
|
158
|
+
numpy = sys.modules.get("numpy")
|
|
159
|
+
return numpy is not None and type(value) is numpy.timedelta64
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
_UNANSWERED: Any = object()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def broadcasts(actual: Any, expected: Any, *, ordering: bool = False, answer: object = _UNANSWERED) -> bool:
|
|
166
|
+
"""Whether `numpy` answered ``actual == expected``, or ``actual < expected`` with *ordering*, element by element.
|
|
167
|
+
|
|
168
|
+
One is a `numpy` scalar and the other a list or a tuple, which the scalar turns into an array. It answers an
|
|
169
|
+
array, true where one element was equal, or raises on its truth where there were several, or on building the
|
|
170
|
+
array of a ragged list. A number is simply unequal to a list and unordered against it. Only where the scalar's
|
|
171
|
+
operator in that place is one `numpy` wrote, its own on the left and its reflected one on the right, and where
|
|
172
|
+
the *answer* is at hand, only for the array itself: a subclass that wrote the operator keeps the answer it
|
|
173
|
+
gives, and a sequence's own operator that declined the scalar leaves the answer to `numpy`.
|
|
174
|
+
"""
|
|
175
|
+
numpy = sys.modules.get("numpy")
|
|
176
|
+
if numpy is None or (answer is not _UNANSWERED and type(answer) is not numpy.ndarray):
|
|
177
|
+
return False
|
|
178
|
+
first, second = type(actual), type(expected)
|
|
179
|
+
if issubclass(first, numpy.generic) and issubclass(second, _SEQUENCES):
|
|
180
|
+
return _written_by_numpy(first, "__lt__" if ordering else "__eq__")
|
|
181
|
+
if issubclass(second, numpy.generic) and issubclass(first, _SEQUENCES):
|
|
182
|
+
return _written_by_numpy(second, "__gt__" if ordering else "__eq__")
|
|
183
|
+
return False
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _written_by_numpy(kind: type, name: str) -> bool:
|
|
187
|
+
"""Whether *kind*'s operator *name* is one `numpy` wrote, read off the class without running it."""
|
|
188
|
+
owner = getattr(inspect.getattr_static(kind, name, None), "__objclass__", None)
|
|
189
|
+
return getattr(owner, "__module__", None) == "numpy"
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def may_broadcast(value: Any) -> bool:
|
|
193
|
+
"""Whether *value* can be one side of a pair `broadcasts` names, while `numpy` is loaded."""
|
|
194
|
+
numpy = sys.modules.get("numpy")
|
|
195
|
+
return numpy is not None and issubclass(type(value), (numpy.generic, *_SEQUENCES))
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def mixes_broadcasting(values: Any, others: Any = ()) -> bool:
|
|
199
|
+
"""Whether a `numpy` scalar and a list or a tuple meet across *values* and *others*, or within *values*.
|
|
200
|
+
|
|
201
|
+
One linear pass over the types, for a walk that compares every pair and is quadratic already.
|
|
202
|
+
"""
|
|
203
|
+
numpy = sys.modules.get("numpy")
|
|
204
|
+
if numpy is None:
|
|
205
|
+
return False
|
|
206
|
+
try:
|
|
207
|
+
kinds: Any = {*map(type, values), *map(type, others)}
|
|
208
|
+
except TypeError: # a metaclass may refuse to hash its classes
|
|
209
|
+
kinds = [*map(type, values), *map(type, others)]
|
|
210
|
+
return any(issubclass(kind, numpy.generic) for kind in kinds) and any(
|
|
211
|
+
issubclass(kind, _SEQUENCES) for kind in kinds
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def member(item: Any, container: Any, verify: bool = True) -> bool:
|
|
216
|
+
"""``item in container`` as a verdict, each element asked as `equals` asks it once ``in`` raised.
|
|
217
|
+
|
|
218
|
+
A set or a mapping cannot hold a signalling NaN, which refuses to hash, alone or inside a tuple, so it holds
|
|
219
|
+
none: answered where the lookup ends in a built-in set's or dict's own (`_searches_by_hash`), which refuses
|
|
220
|
+
nothing but an unhashable key, and the item would hash but for the NaNs in it. A built-in list, tuple, deque
|
|
221
|
+
or dict's values, or a set or mapping the item hashes for, whose comparison refused a pair, is searched
|
|
222
|
+
element by element too. Any other error raised inside the container's or an element's own code is handed
|
|
223
|
+
on. An iterator is searched on from where the raising ``in`` stopped, which is enough: every element before
|
|
224
|
+
it compared unequal, or ``in`` would have answered, and the one that raised met a NaN or an int no float can
|
|
225
|
+
equal. An item that may broadcast (`may_broadcast`) is searched element by element in a container that
|
|
226
|
+
walks by ``==``, before ``in`` could take the truth of an array; *verify* false is a caller that knows no such
|
|
227
|
+
item comes. A set or a mapping meets one only through a hash collision, whose ambiguous truth is answered
|
|
228
|
+
by the same walk.
|
|
229
|
+
"""
|
|
230
|
+
if verify and type(item) not in _PLAIN and may_broadcast(item) and _walks_by_equality(container):
|
|
231
|
+
return any(element is item or equals(element, item) for element in container)
|
|
232
|
+
try:
|
|
233
|
+
return item in container
|
|
234
|
+
except MEMBERSHIP_REFUSALS as refusal:
|
|
235
|
+
found = member_past(item, container, refusal)
|
|
236
|
+
if found is None:
|
|
237
|
+
raise
|
|
238
|
+
return found
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
MEMBERSHIP_REFUSALS = (decimal.InvalidOperation, OverflowError, ValueError, DeprecationWarning, TypeError)
|
|
242
|
+
"""What ``in`` raises that `member_past` may answer."""
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def member_past(item: Any, container: Any, refusal: Exception) -> bool | None:
|
|
246
|
+
"""`member`'s verdict once ``item in container`` raised *refusal*, or ``None`` to hand the refusal on as raised.
|
|
247
|
+
|
|
248
|
+
Answered from the refusal already caught, never by asking ``in`` again: an element whose ``==`` answers
|
|
249
|
+
differently the second time would have turned the refusal into a verdict.
|
|
250
|
+
"""
|
|
251
|
+
if isinstance(refusal, (decimal.InvalidOperation, OverflowError)):
|
|
252
|
+
if raised_inside(refusal):
|
|
253
|
+
return None
|
|
254
|
+
elif isinstance(refusal, (ValueError, DeprecationWarning)):
|
|
255
|
+
# a hash collision met a `numpy` scalar with a tuple, and `in` took the truth of their array
|
|
256
|
+
if raised_inside(refusal) or not may_broadcast(item) or not _searched_again(container):
|
|
257
|
+
return None
|
|
258
|
+
else:
|
|
259
|
+
# a dict's items view hashes the key of a pair alone, and asks about the value only once the key is there
|
|
260
|
+
pair = type(container) is type({}.items()) and isinstance(item, tuple) and len(item) == 2
|
|
261
|
+
key = item[0] if pair else item
|
|
262
|
+
hashed = _searches_by_hash(container)
|
|
263
|
+
if hashed and _unhashable_for_a_nan_alone(key):
|
|
264
|
+
return False
|
|
265
|
+
if raised_inside(refusal) or not (_walks_by_equality(container) or (hashed and _hashes(key))):
|
|
266
|
+
return None
|
|
267
|
+
return any(element is item or equals(element, item) for element in container)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _walks_by_equality(container: Any) -> bool:
|
|
271
|
+
"""Whether ``in`` walks *container* asking ``==``: a built-in list, tuple, deque or dict's values.
|
|
272
|
+
|
|
273
|
+
A subclass counts where it keeps both the search and the iteration of the one it derives from, read off the
|
|
274
|
+
class without running either.
|
|
275
|
+
"""
|
|
276
|
+
kind = type(container)
|
|
277
|
+
if kind in _SEARCHED_BY_EQUALITY:
|
|
278
|
+
return True
|
|
279
|
+
return any(
|
|
280
|
+
issubclass(kind, base)
|
|
281
|
+
and inspect.getattr_static(kind, "__contains__") is inspect.getattr_static(base, "__contains__")
|
|
282
|
+
and inspect.getattr_static(kind, "__iter__") is inspect.getattr_static(base, "__iter__")
|
|
283
|
+
for base in (list, tuple, collections.deque)
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _searched_again(container: Any) -> bool:
|
|
288
|
+
"""Whether *container* can be searched a second time: a built-in sequence, set or mapping, never an iterator."""
|
|
289
|
+
return _walks_by_equality(container) or _searches_by_hash(container)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
REFUSALS = (TypeError, decimal.InvalidOperation, OverflowError, ValueError)
|
|
293
|
+
"""What ``==`` raises for a pair it refuses to compare: a `Decimal` against a `numpy` integer, a signalling NaN, a
|
|
294
|
+
`numpy` float against a Python int past its range, and the truth of the array a `numpy` integer answers against a
|
|
295
|
+
tuple key its hash collided with."""
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def held_key(keys: Any, key: Any) -> tuple[bool, Any]:
|
|
299
|
+
"""Whether *keys* holds *key*, as `member` asks it, and the very key it holds there.
|
|
300
|
+
|
|
301
|
+
Where ``in`` answers, that is *key* itself. Where it refused a comparison after an equal hash, as a `Decimal`
|
|
302
|
+
refuses a `numpy` integer, the key held is the one `equals` finds, which a lookup by that very object reaches
|
|
303
|
+
through identity and never compares again. Only a set or a mapping whose lookup ends in a built-in one
|
|
304
|
+
(`_searches_by_hash`) is searched that way; anybody else's refusal is handed on.
|
|
305
|
+
"""
|
|
306
|
+
try:
|
|
307
|
+
return key in keys, key
|
|
308
|
+
except REFUSALS:
|
|
309
|
+
if not _searches_by_hash(keys):
|
|
310
|
+
raise
|
|
311
|
+
if not member(key, keys):
|
|
312
|
+
return False, key
|
|
313
|
+
return True, next(held for held in keys if held is key or equals(held, key))
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def lookup(mapping: Any, key: Any, refusal: BaseException | None = None) -> tuple[bool, Any]:
|
|
317
|
+
"""Whether *mapping* holds *key*, and ``mapping[key]`` where it does, the key found as `held_key` finds it.
|
|
318
|
+
|
|
319
|
+
*refusal* is what the caller's own lookup raised, handed on where it came from code of the mapping's, the
|
|
320
|
+
key's or a value's own rather than from the comparison of two keys.
|
|
321
|
+
"""
|
|
322
|
+
if refusal is not None and raised_inside(refusal):
|
|
323
|
+
raise refusal
|
|
324
|
+
found, held = held_key(mapping, key)
|
|
325
|
+
return found, mapping[held] if found else None
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _hashes(key: Any) -> bool:
|
|
329
|
+
"""Whether *key* hashes, so a lookup that refused it refused a comparison rather than the key."""
|
|
330
|
+
try:
|
|
331
|
+
hash(key)
|
|
332
|
+
except TypeError:
|
|
333
|
+
return False
|
|
334
|
+
return True
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _unhashable_for_a_nan_alone(item: Any) -> bool:
|
|
338
|
+
"""Whether *item* is a `Decimal` NaN, or a tuple holding one that would hash if its NaNs did."""
|
|
339
|
+
if nan_operand(item):
|
|
340
|
+
return True
|
|
341
|
+
if not isinstance(item, tuple):
|
|
342
|
+
return False
|
|
343
|
+
held = False
|
|
344
|
+
for element in item:
|
|
345
|
+
if _unhashable_for_a_nan_alone(element):
|
|
346
|
+
held = True
|
|
347
|
+
continue
|
|
348
|
+
try:
|
|
349
|
+
hash(element)
|
|
350
|
+
except TypeError:
|
|
351
|
+
return False
|
|
352
|
+
return held
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _searches_by_hash(container: Any) -> bool:
|
|
356
|
+
"""Whether ``in`` over *container* ends in a built-in set's or dict's own lookup, all the way down.
|
|
357
|
+
|
|
358
|
+
A built-in set or dict, or a view of one, directly. A `UserDict` whose `data` is a built-in dict, and the
|
|
359
|
+
keys view over such a mapping, by following the path, read without running a descriptor of anybody's.
|
|
360
|
+
"""
|
|
361
|
+
searched = inspect.getattr_static(type(container), "__contains__", None)
|
|
362
|
+
built_in = (set, frozenset, dict, type({}.keys()), type({}.items()))
|
|
363
|
+
if any(searched is inspect.getattr_static(kind, "__contains__") for kind in built_in):
|
|
364
|
+
return True
|
|
365
|
+
if searched is collections.UserDict.__contains__:
|
|
366
|
+
return type(inspect.getattr_static(container, "data", None)) is dict
|
|
367
|
+
if searched is collections.abc.KeysView.__contains__:
|
|
368
|
+
# the slot `MappingView` declares, read by its own descriptor, and only where no subclass has shadowed it
|
|
369
|
+
slot = inspect.getattr_static(collections.abc.MappingView, "_mapping")
|
|
370
|
+
return inspect.getattr_static(type(container), "_mapping") is slot and _searches_by_hash(
|
|
371
|
+
slot.__get__(container)
|
|
372
|
+
)
|
|
373
|
+
return False
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def equal_past(actual: Any, expected: Any, refusal: Exception) -> bool:
|
|
377
|
+
"""Whether a pair whose own ``==`` raised *refusal* is equal, or *refusal* again where nothing can answer.
|
|
378
|
+
|
|
379
|
+
A NaN equals nothing, which answers a signal wherever one of the pair is a NaN, as the operands decide it for
|
|
380
|
+
ordering. Two lists, two tuples or two mappings compared by the built-in ``==`` are equal element by
|
|
381
|
+
element, each asked as `equals` asks it. Two numbers whose operator overflowed or refused the other are equal
|
|
382
|
+
where their exact values are. An error raised inside a comparison of the value's own is handed on first,
|
|
383
|
+
whatever else the pair holds, and so is a refusal nothing here answers. A `ValueError` is otherwise answered
|
|
384
|
+
only for a pair `broadcasts` names, as unequal.
|
|
385
|
+
"""
|
|
386
|
+
if raised_inside(refusal):
|
|
387
|
+
raise refusal
|
|
388
|
+
if isinstance(refusal, decimal.InvalidOperation) and (nan_operand(actual) or nan_operand(expected)):
|
|
389
|
+
return False
|
|
390
|
+
walked = _equal_by_element(actual, expected)
|
|
391
|
+
if walked is not None:
|
|
392
|
+
return walked
|
|
393
|
+
if isinstance(refusal, ValueError):
|
|
394
|
+
if broadcasts(actual, expected):
|
|
395
|
+
return False
|
|
396
|
+
raise refusal
|
|
397
|
+
left, right = _exact_real(actual), _exact_real(expected)
|
|
398
|
+
if not isinstance(refusal, decimal.InvalidOperation) and left is not None and right is not None:
|
|
399
|
+
return not isinstance(left, str) and left == right
|
|
400
|
+
# a scalar against a container broadcast by `numpy`: the NaN that signalled equals nothing it met
|
|
401
|
+
if isinstance(refusal, decimal.InvalidOperation) and (_holds_nan(actual) or _holds_nan(expected)):
|
|
402
|
+
return False
|
|
403
|
+
raise refusal
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _holds_nan(value: Any, seen: frozenset[int] = frozenset()) -> bool:
|
|
407
|
+
"""Whether *value* is a `Decimal` NaN or a list, tuple or dict holding one at any depth."""
|
|
408
|
+
if id(value) in seen:
|
|
409
|
+
return False
|
|
410
|
+
within = seen | {id(value)}
|
|
411
|
+
if isinstance(value, dict):
|
|
412
|
+
return any(_holds_nan(held, within) for held in value.values())
|
|
413
|
+
if isinstance(value, (list, tuple)):
|
|
414
|
+
return any(_holds_nan(held, within) for held in value)
|
|
415
|
+
return nan_operand(value)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _equal_by_element(actual: Any, expected: Any) -> bool | None:
|
|
419
|
+
"""Built-in ``==`` over two lists, tuples, sets or mappings, asked of each element, else ``None``.
|
|
420
|
+
|
|
421
|
+
A set equals another of its size that holds each of its elements, and a mapping one of its size that holds
|
|
422
|
+
each of its keys, found as `member` finds it, under an equal value.
|
|
423
|
+
"""
|
|
424
|
+
for kind in (list, tuple):
|
|
425
|
+
if isinstance(actual, kind) and isinstance(expected, kind):
|
|
426
|
+
if type(actual).__eq__ is not kind.__eq__ or type(expected).__eq__ is not kind.__eq__:
|
|
427
|
+
return None
|
|
428
|
+
pairs = zip(actual, expected, strict=False)
|
|
429
|
+
return len(actual) == len(expected) and all(left is right or equals(left, right) for left, right in pairs)
|
|
430
|
+
if isinstance(actual, (set, frozenset)) and isinstance(expected, (set, frozenset)):
|
|
431
|
+
if not {type(actual).__eq__, type(expected).__eq__} <= {set.__eq__, frozenset.__eq__}:
|
|
432
|
+
return None
|
|
433
|
+
return len(actual) == len(expected) and all(member(element, expected) for element in actual)
|
|
434
|
+
if isinstance(actual, dict) and isinstance(expected, dict):
|
|
435
|
+
return _mappings_equal(actual, expected)
|
|
436
|
+
return None
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _mappings_equal(actual: dict, expected: dict) -> bool | None:
|
|
440
|
+
"""Two dicts, as ``==`` compares them: two `OrderedDict` values item by item in order, else key by key."""
|
|
441
|
+
kinds = {type(actual).__eq__, type(expected).__eq__}
|
|
442
|
+
if not kinds <= {dict.__eq__, collections.OrderedDict.__eq__}:
|
|
443
|
+
return None
|
|
444
|
+
if kinds == {collections.OrderedDict.__eq__}:
|
|
445
|
+
return equals(list(actual.items()), list(expected.items()))
|
|
446
|
+
return len(actual) == len(expected) and all(_held_alike(key, held, expected) for key, held in actual.items())
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _held_alike(key: Any, held: Any, mapping: Any) -> bool:
|
|
450
|
+
"""Whether *mapping* holds *key* under a value equal to *held*."""
|
|
451
|
+
found, other = lookup(mapping, key)
|
|
452
|
+
return found and (other is held or equals(held, other))
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _order_past(actual: Any, expected: Any, refusal: Exception) -> int | None:
|
|
456
|
+
"""How a pair orders once its own operator raised *refusal*: ``-1``, ``0`` or ``1``, ``None`` for a NaN.
|
|
457
|
+
|
|
458
|
+
An `OverflowError` is a `numpy` float converting a Python int past its range, and a `TypeError` the operator
|
|
459
|
+
refusing the pair, as a `Decimal` refuses a `numpy` integer. Where both stand for exact values the pair
|
|
460
|
+
orders by them, an infinity above every finite value and a NaN against nothing. A `TypeError` between any
|
|
461
|
+
other two leaves the pair with no order. Raised inside a comparison of the value's own, either is a bug in
|
|
462
|
+
the value and is handed on, unless `rational_overflow` names it, and so is an overflow from a value with no
|
|
463
|
+
exact value to order by. A `ValueError` otherwise leaves only a pair `broadcasts` names either way with no
|
|
464
|
+
order, and is handed on.
|
|
465
|
+
"""
|
|
466
|
+
if raised_inside(refusal) and not rational_overflow(refusal):
|
|
467
|
+
raise refusal
|
|
468
|
+
if _lexicographic(actual, expected):
|
|
469
|
+
return _order_by_element(actual, expected)
|
|
470
|
+
if isinstance(refusal, ValueError):
|
|
471
|
+
if broadcasts(actual, expected, ordering=True) or broadcasts(expected, actual, ordering=True):
|
|
472
|
+
raise UnorderableError("pair") from None
|
|
473
|
+
raise refusal
|
|
474
|
+
left, right = _exact_real(actual), _exact_real(expected)
|
|
475
|
+
if left is None or right is None:
|
|
476
|
+
if isinstance(refusal, TypeError):
|
|
477
|
+
raise UnorderableError("pair") from None
|
|
478
|
+
raise refusal
|
|
479
|
+
if isinstance(left, str) or isinstance(right, str):
|
|
480
|
+
return None
|
|
481
|
+
return (left > right) - (left < right)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _own_code(owner: type, name: str) -> types.CodeType | None:
|
|
485
|
+
"""The code of the function *owner* itself defines as *name*, or ``None`` where it defines no Python one."""
|
|
486
|
+
return getattr(vars(owner).get(name), "__code__", None)
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
_FRACTION_ARITHMETIC = frozenset(
|
|
490
|
+
filter(None, (_own_code(fractions.Fraction, "__sub__"), _own_code(fractions.Fraction, "__rsub__")))
|
|
491
|
+
)
|
|
492
|
+
_FRACTION_CONVERSION = frozenset(
|
|
493
|
+
filter(None, (_own_code(numbers.Rational, "__float__"), _own_code(fractions.Fraction, "__float__")))
|
|
494
|
+
)
|
|
495
|
+
_FRACTION_ORDERING = frozenset(
|
|
496
|
+
filter(None, (_own_code(fractions.Fraction, name) for name in ("__lt__", "__le__", "__gt__", "__ge__")))
|
|
497
|
+
)
|
|
498
|
+
_FRACTION_COMPARISON = _own_code(fractions.Fraction, "_richcmp")
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def rational_overflow(error: BaseException) -> bool:
|
|
502
|
+
"""Whether *error* is `Fraction`'s own code overflowing, which the exact values of the pair remove.
|
|
503
|
+
|
|
504
|
+
Two places in `Fraction` meet the other operand in Python code, so `raised_inside` read their overflow as a
|
|
505
|
+
bug in the value: its arithmetic converting itself to ``float`` through `numbers.Rational.__float__`, and its
|
|
506
|
+
ordering multiplying its denominator by a fixed-width `numpy` integer. The call decides instead: the
|
|
507
|
+
operator called is `Fraction`'s own, and the overflow was raised in the code that operator runs, for a
|
|
508
|
+
`Fraction`. Checked by code object at both ends, since a module's name or namespace can be claimed by a
|
|
509
|
+
function of anybody's, the inner code alone can be reached from a value's own method, and both can be
|
|
510
|
+
borrowed by a class of anybody's, which is why the value has to be a `Fraction` as well, and an exact one,
|
|
511
|
+
since a subclass can hand either code fields of its own. The code objects are read once, and an
|
|
512
|
+
interpreter whose `Fraction` runs either in C names nothing.
|
|
513
|
+
"""
|
|
514
|
+
called = error.__traceback__.tb_next if error.__traceback__ is not None else None
|
|
515
|
+
if not isinstance(error, OverflowError) or called is None:
|
|
516
|
+
return False
|
|
517
|
+
raised = called
|
|
518
|
+
while raised.tb_next is not None:
|
|
519
|
+
raised = raised.tb_next
|
|
520
|
+
if type(raised.tb_frame.f_locals.get("self")) is not fractions.Fraction:
|
|
521
|
+
return False
|
|
522
|
+
if called.tb_frame.f_code in _FRACTION_ARITHMETIC:
|
|
523
|
+
return raised.tb_frame.f_code in _FRACTION_CONVERSION
|
|
524
|
+
return called.tb_frame.f_code in _FRACTION_ORDERING and raised.tb_frame.f_code is _FRACTION_COMPARISON
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def _lexicographic(actual: Any, expected: Any) -> bool:
|
|
528
|
+
"""Whether the pair is two lists or two tuples ordered by the built-in ``<``, element by element."""
|
|
529
|
+
return any(
|
|
530
|
+
isinstance(actual, kind)
|
|
531
|
+
and isinstance(expected, kind)
|
|
532
|
+
and type(actual).__lt__ is kind.__lt__
|
|
533
|
+
and type(expected).__lt__ is kind.__lt__
|
|
534
|
+
for kind in (list, tuple)
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def _order_by_element(actual: Any, expected: Any) -> int | None:
|
|
539
|
+
"""Two sequences in the built-in order: the first pair `equals` calls unequal decides, else the length.
|
|
540
|
+
|
|
541
|
+
That pair ordered by `compare`, and ``None`` where it orders neither way, as a NaN does.
|
|
542
|
+
"""
|
|
543
|
+
for left, right in zip(actual, expected, strict=False):
|
|
544
|
+
if left is right or equals(left, right):
|
|
545
|
+
continue
|
|
546
|
+
return compare(left, right) or None
|
|
547
|
+
return (len(actual) > len(expected)) - (len(actual) < len(expected))
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def _exact_real(value: Any) -> tuple[int, fractions.Fraction] | str | None:
|
|
551
|
+
"""*value* as ``(rank, exact)`` for ordering past an overflow, ``"nan"``, or ``None`` with no exact value.
|
|
552
|
+
|
|
553
|
+
The rank puts an infinity above or below every finite value, whose exact value is compared otherwise. An
|
|
554
|
+
integer is read by `int`'s own `__index__` or a `numpy` integer's, never by a conversion of the value's own,
|
|
555
|
+
which a registered `numbers.Integral` can make answer anything. A `Fraction` is its own exact value, and a
|
|
556
|
+
subclass of one is read as any other number. Any other number is read through
|
|
557
|
+
`as_integer_ratio`, which never rounds and refuses an infinity and a NaN by the error it raises, rather than
|
|
558
|
+
through a conversion to ``float``. Only as `float`, `Decimal` or a `numpy` float wrote it in C,
|
|
559
|
+
whose errors mean exactly that, and called as the type holds it, as is its ``>`` for an infinity's sign:
|
|
560
|
+
any other, on the class or reached through the instance, can raise either error for a finite value, and
|
|
561
|
+
would order it as an infinity or leave it unordered.
|
|
562
|
+
"""
|
|
563
|
+
if issubclass(type(value), int) or issubclass(type(value), numbers.Integral):
|
|
564
|
+
whole = exact_int(value)
|
|
565
|
+
return None if whole is None else (0, fractions.Fraction(whole))
|
|
566
|
+
if type(value) is fractions.Fraction:
|
|
567
|
+
return 0, value
|
|
568
|
+
ratio = _known_number_method(type(value), "as_integer_ratio", types.MethodDescriptorType)
|
|
569
|
+
return None if ratio is None else _read_by(value, ratio)
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def _read_by(value: Any, ratio: Any) -> tuple[int, fractions.Fraction] | str | None:
|
|
573
|
+
"""What `_exact_real` answers for *value* through *ratio*, the `as_integer_ratio` its class holds."""
|
|
574
|
+
try:
|
|
575
|
+
numerator, denominator = ratio(value)
|
|
576
|
+
except OverflowError:
|
|
577
|
+
greater = _known_number_method(type(value), "__gt__", types.WrapperDescriptorType)
|
|
578
|
+
if greater is None:
|
|
579
|
+
return None
|
|
580
|
+
return (1 if greater(value, 0) else -1), fractions.Fraction(0)
|
|
581
|
+
except ValueError:
|
|
582
|
+
return "nan"
|
|
583
|
+
return 0, fractions.Fraction(int(numerator), int(denominator))
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def exact_int(value: Any) -> int | None:
|
|
587
|
+
"""*value* as the int it holds, read by `int`'s own `__index__` or a `numpy` integer's, else ``None``.
|
|
588
|
+
|
|
589
|
+
Never by a conversion of the value's own, which a registered `numbers.Integral` can make answer anything.
|
|
590
|
+
"""
|
|
591
|
+
kind = type(value)
|
|
592
|
+
if issubclass(kind, int):
|
|
593
|
+
return int.__index__(value)
|
|
594
|
+
if issubclass(kind, numbers.Integral):
|
|
595
|
+
index = _known_number_method(kind, "__index__", types.WrapperDescriptorType)
|
|
596
|
+
return None if index is None else index(value)
|
|
597
|
+
return None
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def unwrapped(value: Any) -> Any:
|
|
601
|
+
"""*value*, or the `int` a `numpy` integer holds: fixed in width, its arithmetic wraps where an `int`'s grows.
|
|
602
|
+
|
|
603
|
+
``numpy.uint8(3) - 250`` is ``9`` and a warning, so a distance or a product taken through one is no measure.
|
|
604
|
+
Only a scalar class `numpy` itself made is read. A subclass of one may have written operators of its own, so
|
|
605
|
+
it is asked through them and keeps its width, and a `numpy` duration, an integer to the ABC, has no
|
|
606
|
+
`__index__`. The class is asked once while the records have room: asked at every call, a passing
|
|
607
|
+
`is_close_to` on two `int64` cost 90% more.
|
|
608
|
+
"""
|
|
609
|
+
kind = type(value)
|
|
610
|
+
if kind is int or kind is float or type(kind) is not type or kind in _NOT_FIXED:
|
|
611
|
+
return value
|
|
612
|
+
index = _WHOLE_OF.get(kind)
|
|
613
|
+
if index is None:
|
|
614
|
+
fixed = not issubclass(kind, int) and _made_ahead(kind) and integral_kind(kind)
|
|
615
|
+
index = _known_number_method(kind, "__index__", types.WrapperDescriptorType) if fixed else None
|
|
616
|
+
if index is None:
|
|
617
|
+
if len(_NOT_FIXED) < 256:
|
|
618
|
+
_NOT_FIXED.add(kind)
|
|
619
|
+
return value
|
|
620
|
+
if len(_WHOLE_OF) < 256:
|
|
621
|
+
_WHOLE_OF[kind] = index
|
|
622
|
+
return index(value)
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def python_number(value: Any) -> Any:
|
|
626
|
+
"""*value*, or the Python number a `numpy` number holds: the `int` of an integer, else the `float` of a float.
|
|
627
|
+
|
|
628
|
+
A float is read only where the `float` is the same number, which a long double past a double's precision is not.
|
|
629
|
+
"""
|
|
630
|
+
whole = unwrapped(value)
|
|
631
|
+
return whole if type(whole) is int else _widened(whole)
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def _widened(value: Any) -> Any:
|
|
635
|
+
"""*value*, or the `float` a `numpy` float holds where that loses nothing, as a long double past a double would."""
|
|
636
|
+
kind = type(value)
|
|
637
|
+
if kind is float or type(kind) is not type:
|
|
638
|
+
return value
|
|
639
|
+
convert = _float_of(kind)
|
|
640
|
+
if convert is None:
|
|
641
|
+
return value
|
|
642
|
+
wide = convert(value)
|
|
643
|
+
return wide if wide == value else value
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def numpy_float(value: Any) -> bool:
|
|
647
|
+
"""Whether *value* is a float scalar of a class `numpy` itself made, whatever it holds, a NaN too."""
|
|
648
|
+
kind = type(value)
|
|
649
|
+
return type(kind) is type and _float_of(kind) is not None
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def _float_of(kind: type) -> Any:
|
|
653
|
+
"""The `__float__` of a float scalar class `numpy` itself made, else ``None``, kept while the record has room."""
|
|
654
|
+
convert = _FLOAT_OF.get(kind, _UNANSWERED)
|
|
655
|
+
if convert is _UNANSWERED:
|
|
656
|
+
real = _made_ahead(kind) and issubclass(kind, numbers.Real) and not integral_kind(kind)
|
|
657
|
+
convert = _known_number_method(kind, "__float__", types.WrapperDescriptorType) if real else None
|
|
658
|
+
if len(_FLOAT_OF) < 256:
|
|
659
|
+
_FLOAT_OF[kind] = convert
|
|
660
|
+
return convert
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def python_numbers(first: Any, second: Any) -> tuple[Any, Any]:
|
|
664
|
+
"""The pair, each `numpy` number read as the Python number it holds where one of the two is a `numpy` integer.
|
|
665
|
+
|
|
666
|
+
Beside an `int`, `numpy` rounds a Python integer to the float it meets: ``16777217 > numpy.float32(16777216)``
|
|
667
|
+
is false to it, where the `int64` the integer was read from ordered exactly. So the float is read as well,
|
|
668
|
+
where a `float` holds the same number, and the pair is measured as Python measures an `int` against a
|
|
669
|
+
`float`. With no `numpy` integer in it the pair keeps the arithmetic it had. Taken apart for a pair: over
|
|
670
|
+
any number of operands an `int64` against an `int` paid 55% for its ordering.
|
|
671
|
+
"""
|
|
672
|
+
one, other = unwrapped(first), unwrapped(second)
|
|
673
|
+
if one is first and other is second:
|
|
674
|
+
return first, second
|
|
675
|
+
return (one if type(one) is int else _widened(one)), (other if type(other) is int else _widened(other))
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
def _made_ahead(kind: type) -> bool:
|
|
679
|
+
"""Whether *kind* is no class made at run time, which bit 9 of its flags marks and a subclass in Python is."""
|
|
680
|
+
return not _flags(kind) & 512
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
_WHOLE_OF: dict[type, Any] = {}
|
|
684
|
+
"""The `__index__` of each `numpy` integer class `unwrapped` has met, bounded as its neighbours are."""
|
|
685
|
+
_NOT_FIXED: set[type] = {int, float, decimal.Decimal, fractions.Fraction}
|
|
686
|
+
"""The classes `unwrapped` found to be no `numpy` integer. A set: asked of a dict, two close `float64` paid 9%."""
|
|
687
|
+
_FLOAT_OF: dict[type, Any] = {int: None, float: None, decimal.Decimal: None, fractions.Fraction: None}
|
|
688
|
+
"""What `_float_of` found for a class, a `numpy` float's `__float__` or ``None``, bounded likewise."""
|
|
689
|
+
_flags = type.__dict__["__flags__"].__get__
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def whole_number(value: Any) -> int | None:
|
|
693
|
+
"""*value* as the int it stands for where it is an integer, which a bool is not, else ``None``."""
|
|
694
|
+
if type(value) is int:
|
|
695
|
+
return value
|
|
696
|
+
return None if isinstance(value, bool) else exact_int(value)
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
def require_integer(value: object, name: str | None = None) -> int:
|
|
700
|
+
"""*value* as the int it stands for (`whole_number`), or refused as no integer: as the argument *name*, else val.
|
|
701
|
+
|
|
702
|
+
A caller on an assertion's own path asks ``value if type(value) is int else require_integer(value, name)``:
|
|
703
|
+
asked through the call with its subject built up front, a plain `int` cost `is_length` 37%.
|
|
704
|
+
"""
|
|
705
|
+
whole = whole_number(value)
|
|
706
|
+
if whole is None:
|
|
707
|
+
refuse(value, "an integer", subject="val" if name is None else argument(name))
|
|
708
|
+
return whole
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
def _known_number_method(owner: type, name: str, kind: type) -> Any | None:
|
|
712
|
+
"""*owner*'s *name* as the type holds it, if `float`, `Decimal` or a `numpy` number wrote it in C, else ``None``.
|
|
713
|
+
|
|
714
|
+
Or as `decimal` wrote it in Python, where CPython was built without its C accelerator (`pure_decimal_code`).
|
|
715
|
+
"""
|
|
716
|
+
method = inspect.getattr_static(owner, name, None)
|
|
717
|
+
if isinstance(method, types.FunctionType) and method.__code__ in pure_decimal_code():
|
|
718
|
+
stock = inspect.getattr_static(decimal.Decimal, name, None)
|
|
719
|
+
return method if method is stock and issubclass(owner, decimal.Decimal) else None
|
|
720
|
+
maker = getattr(method, "__objclass__", None)
|
|
721
|
+
known = maker in (float, decimal.Decimal) or getattr(maker, "__module__", None) == "numpy"
|
|
722
|
+
# borrowed from another number type, the method refuses the instance with a `TypeError` of its own
|
|
723
|
+
owned = isinstance(maker, type) and issubclass(owner, maker)
|
|
724
|
+
return method if isinstance(method, kind) and known and owned else None
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def compare(actual: Any, expected: Any) -> int:
|
|
728
|
+
"""``-1``/``0``/``1`` for *actual* against *expected*, or `UnorderableError` when they cannot be ordered.
|
|
729
|
+
|
|
730
|
+
A `TypeError` raised *inside* somebody's own `__lt__` travels out untouched: that is a bug in the
|
|
731
|
+
value, not an unorderable pair, and answering it either way would send the reader to the wrong file.
|
|
732
|
+
"""
|
|
733
|
+
actual_type = type(actual)
|
|
734
|
+
# the ordinary case first: in a per-element loop, so the frozenset lookup and two `isinstance` were paid each time.
|
|
735
|
+
# An int against a float orders exactly and never signals, and the walk below spent two ABC checks on it
|
|
736
|
+
if (actual_type is type(expected) and actual_type in _PLAIN) or (
|
|
737
|
+
(actual_type is int or actual_type is float) and (type(expected) is int or type(expected) is float)
|
|
738
|
+
):
|
|
739
|
+
return (actual > expected) - (actual < expected)
|
|
740
|
+
if actual_type in _UNORDERED:
|
|
741
|
+
raise UnorderableError("value")
|
|
742
|
+
actual_kind: Any = _kind_of(actual)
|
|
743
|
+
# a subclass that wrote its own `<` is asked instead: the rule is about the stock one being wrong across kinds
|
|
744
|
+
if actual_kind is not None and type(actual).__lt__ is actual_kind.__lt__ and _kind_of(expected) is not actual_kind:
|
|
745
|
+
raise UnorderableError("kind", wanted=actual_kind)
|
|
746
|
+
if (
|
|
747
|
+
actual_kind is None
|
|
748
|
+
and isinstance(actual, numbers.Number)
|
|
749
|
+
and (not isinstance(expected, numbers.Number) or type(expected) in _UNORDERED)
|
|
750
|
+
):
|
|
751
|
+
raise UnorderableError("kind", wanted=numbers.Number)
|
|
752
|
+
# deliberately dynamic: what may be ordered is decided above, and a checker reading the union sees no `<`
|
|
753
|
+
left: Any = actual
|
|
754
|
+
right: Any = expected
|
|
755
|
+
expected_type = type(expected)
|
|
756
|
+
# asked of the class kept, since a call for every pair cost a `Decimal` against an `int` 12%
|
|
757
|
+
if actual_type is not expected_type and (
|
|
758
|
+
(type(actual_type) is type and actual_type not in _NOT_FIXED)
|
|
759
|
+
or (type(expected_type) is type and expected_type not in _NOT_FIXED)
|
|
760
|
+
):
|
|
761
|
+
left, right = python_numbers(actual, expected)
|
|
762
|
+
try:
|
|
763
|
+
less = left < right
|
|
764
|
+
mixed = actual_type is not type(expected)
|
|
765
|
+
# a pair of one type is never a scalar against a list: asked for it anyway, an `int64` tie cost 19% more
|
|
766
|
+
if mixed and type(less) is not bool and broadcasts(left, right, ordering=True, answer=less):
|
|
767
|
+
raise UnorderableError("pair")
|
|
768
|
+
if less:
|
|
769
|
+
return -1
|
|
770
|
+
greater = right < left
|
|
771
|
+
if mixed and type(greater) is not bool and broadcasts(right, left, ordering=True, answer=greater):
|
|
772
|
+
raise UnorderableError("pair")
|
|
773
|
+
if greater:
|
|
774
|
+
return 1
|
|
775
|
+
except (TypeError, OverflowError, ValueError) as refusal:
|
|
776
|
+
order = _order_past(actual, expected, refusal)
|
|
777
|
+
return 0 if order is None else order
|
|
778
|
+
except decimal.InvalidOperation as signal:
|
|
779
|
+
return _order_past_signal(actual, expected, signal)
|
|
780
|
+
return 0 # neither less nor greater, which is what a float NaN answers and `holds` keeps from reading equal
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
def _order_past_signal(actual: Any, expected: Any, signal: decimal.InvalidOperation) -> int:
|
|
784
|
+
"""How a pair orders once a `Decimal` NaN signalled at ``<``: a NaN operand neither way, two sequences by element.
|
|
785
|
+
|
|
786
|
+
Asked after the comparison rather than before: checked first, `'a'` against a NaN read as a verdict where the
|
|
787
|
+
same pair without one is refused. The operands decide and not the traceback: measured, a signal comes from
|
|
788
|
+
one frame under the C accelerator and from four under `_pydecimal`, so `raised_inside` answered the
|
|
789
|
+
interpreter build. A `numpy` scalar against a sequence holding a NaN is no order, as a number against a list.
|
|
790
|
+
Any other signal is handed on.
|
|
791
|
+
"""
|
|
792
|
+
if nan_operand(actual) or nan_operand(expected):
|
|
793
|
+
return 0
|
|
794
|
+
if broadcasts(actual, expected, ordering=True) or broadcasts(expected, actual, ordering=True):
|
|
795
|
+
raise UnorderableError("pair") from None
|
|
796
|
+
if not _lexicographic(actual, expected):
|
|
797
|
+
raise signal
|
|
798
|
+
order = _order_by_element(actual, expected)
|
|
799
|
+
return 0 if order is None else order
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
def holds(actual: Any, expected: Any, relation: str) -> bool:
|
|
803
|
+
"""Whether *relation* (``lt``/``le``/``gt``/``ge``) holds between the two.
|
|
804
|
+
|
|
805
|
+
`NaN` is unordered against everything including itself, and `compare` answers ``0`` for it because
|
|
806
|
+
neither side is less than the other. That would make `le`/`ge` true, which is the one place where
|
|
807
|
+
"not less, not greater" does not mean "equal", so equality is asked separately.
|
|
808
|
+
"""
|
|
809
|
+
actual_type = type(actual)
|
|
810
|
+
if (actual_type is type(expected) and actual_type in _PLAIN) or (
|
|
811
|
+
(actual_type is int or actual_type is float) and (type(expected) is int or type(expected) is float)
|
|
812
|
+
):
|
|
813
|
+
# the shortcut `compare` takes, one call earlier: building the answer through a dict of four keys cost most
|
|
814
|
+
return _DIRECT[relation](actual, expected)
|
|
815
|
+
order = compare(actual, expected)
|
|
816
|
+
if order == 0 and relation in ("le", "ge") and not _equal(actual, expected):
|
|
817
|
+
return False
|
|
818
|
+
return {"lt": order < 0, "le": order <= 0, "gt": order > 0, "ge": order >= 0}[relation]
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
def first_out_of_order(
|
|
822
|
+
items: Iterable[Any], *, key: Callable[[Any], Any], reverse: bool = False
|
|
823
|
+
) -> tuple[int, Any, Any] | None:
|
|
824
|
+
"""The first adjacent pair that breaks the order, as ``(index, earlier, later)``, or ``None``.
|
|
825
|
+
|
|
826
|
+
Returned rather than reported, so both spellings get what they need from one walk: the assertion
|
|
827
|
+
names the pair and where it sits, and a matcher only looks at whether there was one.
|
|
828
|
+
"""
|
|
829
|
+
previous: Any = None
|
|
830
|
+
previous_key: Any = None
|
|
831
|
+
for index, current in enumerate(items):
|
|
832
|
+
current_key = key(current)
|
|
833
|
+
# through `compare`, not `<`: a raise here reads to the origin check as a plain type mismatch.
|
|
834
|
+
# The key is carried rather than recomputed, which doubled the calls to the caller's `key`
|
|
835
|
+
if index > 0 and _out_of_order(current_key, previous_key, reverse=reverse):
|
|
836
|
+
return index - 1, previous, current
|
|
837
|
+
previous = current
|
|
838
|
+
previous_key = current_key
|
|
839
|
+
return None
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
def plainly_sortable(items: object) -> bool:
|
|
843
|
+
"""Whether *items* is an exact list or tuple of one exact `int`, `str` or `bytes` type: `<` orders it totally, in C.
|
|
844
|
+
|
|
845
|
+
Such a sequence holds no NaN and runs no code of anybody's, so `first_plainly_out_of_order` compares its pairs
|
|
846
|
+
in one C pass instead of pair by pair through `compare`, which took 106 us over 1000 ints. Under five items
|
|
847
|
+
the pass costs more to set up than the walk: 344 against 186 ns at two.
|
|
848
|
+
"""
|
|
849
|
+
if (type(items) is not list and type(items) is not tuple) or len(items) < 5:
|
|
850
|
+
return False
|
|
851
|
+
kind = type(items[0])
|
|
852
|
+
# by identity, stopping at the first other type: hashing a type asks its metaclass, which may refuse
|
|
853
|
+
if kind is not int and kind is not str and kind is not bytes:
|
|
854
|
+
return False
|
|
855
|
+
return all(map(operator.is_, map(type, items), itertools.repeat(kind)))
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
def first_plainly_out_of_order(items: Sequence[Any], *, reverse: bool) -> tuple[int, Any, Any] | None:
|
|
859
|
+
"""`first_out_of_order` over the items themselves, for a sequence `plainly_sortable` accepted."""
|
|
860
|
+
breaks = operator.lt if reverse else operator.gt
|
|
861
|
+
pairs = map(breaks, items, itertools.islice(items, 1, None))
|
|
862
|
+
index = next(itertools.compress(itertools.count(), pairs), None)
|
|
863
|
+
return None if index is None else (index, items[index], items[index + 1])
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
def _out_of_order(later: Any, earlier: Any, *, reverse: bool) -> bool:
|
|
867
|
+
"""Whether *later* breaks the order after *earlier*, a pair holding a NaN vouching for no order at all.
|
|
868
|
+
|
|
869
|
+
The NaN is asked only where the pair did not order, a tie or no order, since a NaN orders against
|
|
870
|
+
nothing: asked of every key, a `numpy` float's NaN check cost a sort of ints 18%.
|
|
871
|
+
"""
|
|
872
|
+
try:
|
|
873
|
+
order = compare(later, earlier)
|
|
874
|
+
except UnorderableError:
|
|
875
|
+
if nan_operand(later) or nan_operand(earlier):
|
|
876
|
+
return True
|
|
877
|
+
raise
|
|
878
|
+
if order == 0:
|
|
879
|
+
# a tie of two exact ints or strings holds no NaN: asked anyway, a sort of equal ints cost 19%
|
|
880
|
+
never_nan = (int, str, bytes)
|
|
881
|
+
if type(later) in never_nan and type(earlier) in never_nan:
|
|
882
|
+
return False
|
|
883
|
+
return nan_operand(later) or nan_operand(earlier)
|
|
884
|
+
return order > 0 if reverse else order < 0
|