@@ -72,11 +72,33 @@ def nan_operand(value: Any) -> bool:
7272 if isinstance (value , decimal .Decimal ):
7373 return decimal .Decimal .is_nan (value )
7474 kind = type (value )
75- if kind is int or type (kind ) is not type or issubclass ( kind , numbers . Integral ):
75+ if kind is int or type (kind ) is not type or kind in _INTEGRAL_KINDS or integral_kind ( kind ):
7676 return False
7777 return _exact_real (value ) == "nan"
7878
7979
80+ def integral_kind (kind : type ) -> bool :
81+ """Whether a class built by `type` itself is a `numbers.Integral`, kept once found: a class never stops being one.
82+
83+ Asked of a `numpy` integer at every NaN and infinity question, where the ABC check cost an `int64` tie 11%. At
84+ most 256 are kept, so classes made on the fly cannot grow it without bound, and an ABC registry cleared by hand
85+ through its private API is not followed.
86+ """
87+ if type (kind ) is not type :
88+ return False
89+ if kind in _INTEGRAL_KINDS :
90+ return True
91+ if issubclass (kind , numbers .Integral ):
92+ if len (_INTEGRAL_KINDS ) < 256 :
93+ _INTEGRAL_KINDS .add (kind )
94+ return True
95+ return False
96+
97+
98+ _INTEGRAL_KINDS : set [type ] = set ()
99+ """What `integral_kind` has found, only classes built by `type` itself, so hashed as `type` hashes."""
100+
101+
80102def _kind_of (value : Any ) -> type | None :
81103 """The date-and-time kind a value orders within, subclasses included, or ``None`` for anything else.
82104
@@ -108,7 +130,7 @@ def equals(actual: Any, expected: Any) -> bool:
108130 equal = actual == expected
109131 except (decimal .InvalidOperation , OverflowError , TypeError , ValueError ) as refusal :
110132 return equal_past (actual , expected , refusal )
111- if type (equal ) is not bool and broadcasts (actual , expected , answer = equal ):
133+ if type (equal ) is not bool and type ( actual ) is not type ( expected ) and broadcasts (actual , expected , answer = equal ):
112134 return False
113135 return bool (equal )
114136
@@ -194,25 +216,40 @@ def member(item: Any, container: Any, verify: bool = True) -> bool:
194216 return any (element is item or equals (element , item ) for element in container )
195217 try :
196218 return item in container
197- except (decimal .InvalidOperation , OverflowError ) as refusal :
198- if raised_inside (refusal ):
219+ except MEMBERSHIP_REFUSALS as refusal :
220+ found = member_past (item , container , refusal )
221+ if found is None :
199222 raise
200- return any (element is item or equals (element , item ) for element in container )
201- except (ValueError , DeprecationWarning ) as ambiguous :
223+ return found
224+
225+
226+ MEMBERSHIP_REFUSALS = (decimal .InvalidOperation , OverflowError , ValueError , DeprecationWarning , TypeError )
227+ """What ``in`` raises that `member_past` may answer."""
228+
229+
230+ def member_past (item : Any , container : Any , refusal : Exception ) -> bool | None :
231+ """`member`'s verdict once ``item in container`` raised *refusal*, or ``None`` to hand the refusal on as raised.
232+
233+ Answered from the refusal already caught, never by asking ``in`` again: an element whose ``==`` answers
234+ differently the second time would have turned the refusal into a verdict.
235+ """
236+ if isinstance (refusal , (decimal .InvalidOperation , OverflowError )):
237+ if raised_inside (refusal ):
238+ return None
239+ elif isinstance (refusal , (ValueError , DeprecationWarning )):
202240 # a hash collision met a `numpy` scalar with a tuple, and `in` took the truth of their array
203- if raised_inside (ambiguous ) or not may_broadcast (item ) or not _searched_again (container ):
204- raise
205- return any (element is item or equals (element , item ) for element in container )
206- except TypeError as refusal :
241+ if raised_inside (refusal ) or not may_broadcast (item ) or not _searched_again (container ):
242+ return None
243+ else :
207244 # a dict's items view hashes the key of a pair alone, and asks about the value only once the key is there
208245 pair = type (container ) is type ({}.items ()) and isinstance (item , tuple ) and len (item ) == 2
209246 key = item [0 ] if pair else item
210247 hashed = _searches_by_hash (container )
211248 if hashed and _unhashable_for_a_nan_alone (key ):
212249 return False
213250 if raised_inside (refusal ) or not (_walks_by_equality (container ) or (hashed and _hashes (key ))):
214- raise
215- return any (element is item or equals (element , item ) for element in container )
251+ return None
252+ return any (element is item or equals (element , item ) for element in container )
216253
217254
218255def _walks_by_equality (container : Any ) -> bool :
@@ -598,12 +635,14 @@ def compare(actual: Any, expected: Any) -> int:
598635 right : Any = expected
599636 try :
600637 less = left < right
601- if type (less ) is not bool and broadcasts (left , right , ordering = True , answer = less ):
638+ mixed = actual_type is not type (expected )
639+ # a pair of one type is never a scalar against a list: asked for it anyway, an `int64` tie cost 19% more
640+ if mixed and type (less ) is not bool and broadcasts (left , right , ordering = True , answer = less ):
602641 raise UnorderableError ("pair" )
603642 if less :
604643 return - 1
605644 greater = right < left
606- if type (greater ) is not bool and broadcasts (right , left , ordering = True , answer = greater ):
645+ if mixed and type (greater ) is not bool and broadcasts (right , left , ordering = True , answer = greater ):
607646 raise UnorderableError ("pair" )
608647 if greater :
609648 return 1
0 commit comments