-
-
Notifications
You must be signed in to change notification settings - Fork 49
/
utilclstest.py
317 lines (272 loc) · 13.3 KB
/
utilclstest.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
#!/usr/bin/env python3
# --------------------( LICENSE )--------------------
# Copyright (c) 2014-2021 Beartype authors.
# See "LICENSE" for further details.
'''
Project-wide **unmemoized class tester** (i.e., unmemoized and thus efficient
callable testing various properties of arbitrary classes) utilities.
This private submodule is *not* intended for importation by downstream callers.
'''
# ....................{ IMPORTS }....................
from beartype.roar._roarexc import (
BeartypeDecorHintPep3119Exception,
_BeartypeUtilTypeException,
)
from beartype._data.cls.datacls import TYPES_BUILTIN_FAKE
from beartype._data.mod.datamod import BUILTINS_MODULE_NAME
from typing import Type
# ....................{ VALIDATORS }....................
def die_unless_type(
# Mandatory parameters.
cls: object,
# Optional parameters.
exception_cls: Type[Exception] = _BeartypeUtilTypeException,
) -> None:
'''
Raise an exception unless the passed object is a class.
Parameters
----------
cls : object
Object to be validated.
exception_cls : Type[Exception]
Type of exception to be raised. Defaults to
:exc:`_BeartypeUtilTypeException`.
Raises
----------
exception_cls
If this object is *not* a class.
'''
# If this object is *NOT* a class, raise an exception.
if not isinstance(cls, type):
assert isinstance(exception_cls, type), (
'f{repr(exception_cls)} not exception class.')
raise exception_cls(f'{repr(cls)} not class.')
# ....................{ VALIDATORS ~ class }....................
#FIXME: Unit test us up, please.
def die_unless_type_isinstanceable(
# Mandatory parameters.
cls: type,
# Optional parameters.
cls_label: str = 'Annotated',
exception_cls: Type[Exception] = BeartypeDecorHintPep3119Exception,
) -> None:
'''
Raise an exception unless the passed object is an **isinstanceable class**
(i.e., class whose metaclass does *not* define an ``__instancecheck__()``
dunder method that raises an exception).
Classes that are *not* isinstanceable include most PEP-compliant type
hints, notably:
* **Generic aliases** (i.e., subscriptable classes overriding the
``__class_getitem__()`` class dunder method standardized by :pep:`560`
subscripted by an arbitrary object) under Python >= 3.9, whose
metaclasses define an ``__instancecheck__()`` dunder method to
unconditionally raise an exception. Generic aliases include:
* :pep:`484`-compliant **subscripted generics.**
* :pep:`585`-compliant type hints.
* User-defined classes whose metaclasses define an ``__instancecheck__()``
dunder method to unconditionally raise an exception, including:
* :pep:`544`-compliant protocols *not* decorated by the
:func:`typing.runtime_checkable` decorator.
Motivation
----------
When a class whose metaclass defines an ``__instancecheck__()`` dunder
method is passed as the second parameter to the :func:`isinstance` builtin,
that builtin defers to that method rather than testing whether the first
parameter passed to that builtin is an instance of that class. If that
method raises an exception, that builtin raises the same exception,
preventing callers from deciding whether arbitrary objects are instances
of that class. For brevity, we refer to that class as "non-isinstanceable."
Most classes are isinstanceable, because deciding whether arbitrary objects
are instances of those classes is a core prerequisite for object-oriented
programming. Most classes that are also PEP-compliant type hints, however,
are *not* isinstanceable, because they're *never* intended to be
instantiated into objects (and typically prohibit instantiation in various
ways); they're only intended to be referenced as type hints annotating
callables, an arguably crude form of callable markup.
:mod:`beartype`-decorated callables typically check the types of arbitrary
objects at runtime by passing those objects and types as the first and
second parameters to the :func:`isinstance` builtin. If those types are
non-isinstanceable, those type-checks will typically raise
non-human-readable exceptions (e.g., ``"TypeError: isinstance() argument 2
cannot be a parameterized generic"`` for `PEP 585`_-compliant type hints).
This is non-ideal both because those exceptions are non-human-readable
*and* because those exceptions are raised at call rather than decoration
time, where users expect the :mod:`beartype.beartype` decorator to raise
exceptions for erroneous type hints.
Thus the existence of this function, which the :mod:`beartype.beartype`
decorator calls to validate the usability of type hints that are classes
*before* checking objects against those classes at call time.
Parameters
----------
cls : type
Class to be validated.
cls_label : str
Human-readable label prefixing the representation of this class in the
exception message raised by this function. Defaults to ``"Annotated"``.
exception_cls : Type[Exception]
Type of exception to be raised. Defaults to
:exc:`BeartypeDecorHintPep3119Exception`.
Raises
----------
BeartypeDecorHintPep3119Exception
If this hint is *not* an isinstanceable class.
'''
# If this hint is *NOT* a class, raise an exception.
die_unless_type(cls=cls, exception_cls=exception_cls)
# Else, this hint is a class.
# If this class is *NOT* isinstanceable, raise an exception. For
# efficiency, this test is split into two passes (in order of decreasing
# efficiency):
#
# 1. Test whether this class is isinstanceable with the memoized
# is_type_isinstanceable() tester. This is crucial, as this test can
# *ONLY* be implemented via inefficient EAFP-style exception handling.
# 2. If that tester reports this class to be non-isinstanceable, raise a
# human-readable exception chained onto the non-human-readable exception
# raised by explicitly passing that class as the second parameter to the
# isinstance() builtin.
if not is_type_isinstanceable(cls):
assert isinstance(exception_cls, type), (
'f{repr(exception_cls)} not exception class.')
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# CAUTION: Synchronize with the is_type_isinstanceable() tester.
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
try:
isinstance(None, cls) # type: ignore[arg-type]
except Exception as exception:
#FIXME: Uncomment after we uncover why doing so triggers an
#infinite circular exception chain when "hint" is a "GenericAlias".
# # Human-readable exception message to be raised as either...
# exception_message = (
# # If this class is a PEP 544-compliant protocol, a message
# # documenting this exact issue and how to resolve it;
# (
# f'{hint_label} PEP 544 protocol {hint} '
# f'uncheckable at runtime (i.e., '
# f'not decorated by @typing.runtime_checkable).'
# )
# if is_hint_pep544_protocol(hint) else
# # Else, a fallback message documenting this general issue.
# (
# f'{hint_label} type {hint} uncheckable at runtime (i.e., '
# f'not passable as second parameter to isinstance() '
# f'due to raising "{exception}" from metaclass '
# f'__instancecheck__() method).'
# )
# )
# Human-readable exception message to be raised.
exception_message = (
f'{cls_label} {repr(cls)} uncheckable at runtime (i.e., '
f'not passable as second parameter to isinstance() '
f'due to raising "{exception}" from metaclass '
f'__instancecheck__() method).'
)
# Raise this high-level exception with this human-readable message
# chained onto this low-level exception with a typically
# non-human-readable message.
raise exception_cls(exception_message) from exception
# ....................{ TESTERS ~ builtin }....................
def is_type_builtin(cls: type) -> bool:
'''
``True`` only if the passed class is **builtin** (i.e., globally accessible
C-based type requiring *no* explicit importation).
This tester is intentionally *not* memoized (e.g., by the
:func:`callable_cached` decorator), as the implementation trivially reduces
to an efficient one-liner.
Parameters
----------
cls : type
Class to be inspected.
Returns
----------
bool
``True`` only if this class is builtin.
Raises
----------
_BeartypeUtilTypeException
If this object is *not* a class.
'''
# Avoid circular import dependencies.
from beartype._util.mod.utilmodule import (
get_object_type_module_name_or_none)
# If this object is *NOT* a type, raise an exception.
die_unless_type(cls)
# Else, this object is a type.
# If this type is a fake builtin (i.e., type that is *NOT* builtin but
# which erroneously masquerades as being builtin), this type is *NOT* a
# builtin. In this case, silently reject this type.
if cls in TYPES_BUILTIN_FAKE:
return False
# Else, this type is *NOT* a fake builtin.
# Fully-qualified name of the module defining this type if this type is
# defined by a module *OR* "None" otherwise (i.e., if this type is
# dynamically defined in-memory).
cls_module_name = get_object_type_module_name_or_none(cls)
# This return true only if this name is that of the "builtins" module
# declaring all builtin types.
return cls_module_name == BUILTINS_MODULE_NAME
# ....................{ TESTERS ~ isinstanceable }....................
def is_type_isinstanceable(cls: object) -> bool:
'''
``True`` only if the passed type cls is an **isinstanceable class** (i.e.,
class whose metaclass does *not* define an ``__instancecheck__()`` dunder
method that raises an exception).
This tester is intentionally *not* memoized (e.g., by the
:func:`callable_cached` decorator). Although the implementation does *not*
trivially reduce to an efficient one-liner, the inefficient branch of this
implementation *only* applies to erroneous edge cases resulting in raised
exceptions and is thus largely ignorable.
Caveats
----------
**This tester may return false positives in unlikely edge cases.**
Internally, this tester tests whether this class is isinstanceable by
detecting whether passing the ``None`` singleton and this class to the
:func:`isinstance` builtin raises an exception. If that call raises *no*
exception, this class is probably but *not* necessarily isinstanceable.
Since the metaclass of this class could define an ``__instancecheck__()``
dunder method to conditionally raise exceptions except when passed the
``None`` singleton, there exists *no* means of ascertaining whether a class
is fully isinstanceable in the general case. Since most classes that are
*not* isinstanceable are unconditionally isinstanceable (i.e., the
metaclasses of those classes define an ``__instancecheck__()`` dunder
method to unconditionally raise exceptions), this distinction is generally
meaningless in the real world. This test thus generally suffices.
Parameters
----------
cls : object
Object to be tested.
Returns
----------
bool
``True`` only if this object is an isinstanceable class.
See Also
----------
:func:`die_unless_type_isinstanceable`
Further details.
'''
# If this object is *NOT* a class, return false.
if not isinstance(cls, type):
return False
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# CAUTION: Synchronize with die_unless_type_isinstanceable().
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# Attempt to pass this class as the second parameter to the isinstance()
# builtin to decide whether or not this class is safely usable as a
# standard class or not.
#
# Note that this leverages an EAFP (i.e., "It is easier to ask forgiveness
# than permission") approach and thus imposes a minor performance penalty,
# but that there exists *NO* faster alternative applicable to arbitrary
# user-defined classes, whose metaclasses may define an __instancecheck__()
# dunder method to raise exceptions and thus prohibit being passed as the
# second parameter to the isinstance() builtin, the primary means employed
# by @beartype wrapper functions to check arbitrary types.
try:
isinstance(None, cls)
# If the prior function call raised *NO* exception, this class is
# probably but *NOT* necessarily isinstanceable.
return True
# If the prior function call raised an exception, this class is *NOT*
# isinstanceable. In this case, return false.
except:
return False