Skip to content

Commit

Permalink
Equinox x 1.
Browse files Browse the repository at this point in the history
This commit is the first in a commit chain officially adding support for
@patrick-kidger (Patrick Kidger)'s third-party Equinox JAX-driven ML
framework, en-route to resolving issue patrick-kidger/equinox#584 kindly
submitted by friendly magical ML unicorn @EtaoinWu (Yue Wu).
Specifically, this commit *very* carefully crafts general-purpose
support for dynamically unwrapping non-standard function wrappers
implemented by third-party packages -- including Equinox. Naturally,
nothing is tested; everything is suspect. Trust no one, @beartype!
(*Some piano etude on a window pane is no winsome pain!*)
  • Loading branch information
leycec committed Nov 18, 2023
1 parent 7ab1216 commit 58219ba
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 22 deletions.
63 changes: 44 additions & 19 deletions beartype/_data/cls/datacls.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,50 @@
NotImplementedType,
)

# ....................{ BEARTYPEABLE }....................
# Types of *ALL* objects that may be decorated by @beartype, intentionally
# listed in descending order of real-world prevalence for negligible efficiency
# gains when performing isinstance()-based tests against this tuple. These
# include the types of *ALL*...
TYPES_BEARTYPEABLE = (
# Pure-Python unbound functions and methods.
FunctionType,
# Pure-Python classes.
ClassType,
# C-based builtin method descriptors wrapping pure-Python unbound methods,
# including class methods, static methods, and property methods.
MethodDecoratorBuiltinTypes,
)
'''
Tuple set of all **beartypeable types** (i.e., types of all objects that may be
decorated by the :func:`beartype.beartype` decorator).
'''


TYPES_UNBEARTYPEABLE = frozenset((
object,
type,
))
'''
Frozen set of all **non-beartypeable types** (i.e., types of all objects that
are *not* safely decoratable by the :func:`beartype.beartype` decorator despite
otherwise being beartypeable types).
Notably, this includes:
* The :class:`object` and :class:`type` superclasses, which
:func:`beartype.beartype` decorator should *obviously* never attempt to
decorate. Doing so:
* Is needlessly inefficient. Like all C-based types, these superclasses are
*not* annotated. Decorating these types thus reduces to an expensive noop.
Since numerous classes (e.g., *all* concrete subclasses of the standard
:class:`enum.Enum` superclass) contain class attributes whose values are
either :class:`object` or :class:`type`, this edge case arises frequently.
* Invites catastrophic issues, including **INFINITE FRIGGIN' RECURSION.** See
also the :func:`beartype._decor._decortype.beartype_type` function.
'''

# ....................{ SETS }....................
TYPES_BUILTIN_FAKE = frozenset((
AsyncCoroutineCType,
Expand Down Expand Up @@ -117,22 +161,3 @@
StackOverflow answer introducing an alternate non-portable technique for
obtaining the PyCapsule type itself.
'''

# ....................{ TUPLES }....................
# Types of *ALL* objects that may be decorated by @beartype, intentionally
# listed in descending order of real-world prevalence for negligible efficiency
# gains when performing isinstance()-based tests against this tuple. These
# include the types of *ALL*...
TYPES_BEARTYPEABLE = (
# Pure-Python unbound functions and methods.
FunctionType,
# Pure-Python classes.
ClassType,
# C-based builtin method descriptors wrapping pure-Python unbound methods,
# including class methods, static methods, and property methods.
MethodDecoratorBuiltinTypes,
)
'''
Tuple set of all **beartypeable types** (i.e., types of all objects that may be
decorated by the :func:`beartype.beartype` decorator).
'''
91 changes: 88 additions & 3 deletions beartype/_decor/_decortype.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@
from beartype._cave._cavemap import NoneTypeOr
from beartype._check.convert.convcoerce import clear_coerce_hint_caches
from beartype._conf.confcls import BeartypeConf
from beartype._data.cls.datacls import TYPES_BEARTYPEABLE
from beartype._data.cls.datacls import (
TYPES_BEARTYPEABLE,
TYPES_UNBEARTYPEABLE,
)
from beartype._data.hint.datahinttyping import (
BeartypeableT,
TypeStack,
Expand Down Expand Up @@ -136,6 +139,8 @@ class variable or method annotated by this hint *or* :data:`None`).
# dynamically defined in-memory outside of any module structure).
module_name = get_object_module_name_or_none(cls)

#FIXME: Consider relegating this logic to a new private
#_beartype_type_reloaded() function for maintainability, please.
# If this class is defined by a module...
if module_name:
# Unqualified basename of this class.
Expand Down Expand Up @@ -234,11 +239,91 @@ class variable or method annotated by this hint *or* :data:`None`).
)

# ....................{ DECORATION }....................
#FIXME: Consider relegating this logic to a new private
#_beartype_type_attrs() function for maintainability, please.
# For the unqualified name and value of each direct (i.e., *NOT* indirectly
# inherited) attribute of this class...
for attr_name, attr_value in cls.__dict__.items(): # pyright: ignore[reportGeneralTypeIssues]
# If this attribute is beartypeable...
if isinstance(attr_value, TYPES_BEARTYPEABLE):
# True only if this attribute is directly beartypeable (e.g., is either
# a function, class, or builtin method descriptor).
is_attr_beartypeable = isinstance(attr_value, TYPES_BEARTYPEABLE)

# If this attribute is *NOT* directly beartypeable (e.g., is neither a
# function, class, nor builtin method descriptor), this attribute
# *COULD* still be indirectly beartypeable. How? By being a non-standard
# object implemented by some third-party package wrapping a standard
# object that *is* directly beartypeable. Although the original use case
# was non-standard function wrappers implemented by the third-party
# Equinox package, this logic transparently generalizes to *ALL*
# third-party packages. Consequently, *ALL* third-party packages
# defining non-standard objects wrapping standard objects should
# endeavour to support @beartype by reproducing the general-purpose
# solution that Equinox adopted.
#
# Notably, third-party packages should ideally add "support for such
# monkey-patching, by adding a __setattr__() that checks for functions
# and wraps them into one of Equinox's function-wrappers." See also:
# https://github.com/patrick-kidger/equinox/issues/584#issuecomment-1806260288
#
# Specifically, if...
if not (
# This attribute is neither directly beartypeable *NOR*...
is_attr_beartypeable or
# A dunder attribute (i.e., prefixed and suffixed by "__")...
#
# Note that dunder attributes *MUST* explicitly be excluded. Why?
# Because attempting to decorate the dynamic values of arbitrary
# dunder attributes with @beartype is intrinsically dangerous and
# frequently induces *INFINITE FRIGGIN' RECURSION* on standard
# types, including:
# * "enum.Enum" subclasses, which define a private "_member_type_"
# attribute whose value is the "object" superclass, which
# @beartype then attempts to decorate. However, the "object"
# superclass defines the "__class__" dunder attribute whose value
# is the "type" superclass, which @beartype then attempts to
# decorate. However, the "type" superclass defines the "__base__"
# dunder attribute whose value is the "object" superclass, which
# @beartype then attempts to decorate. Anarchy ensues.
(
attr_name.startswith('__') and
attr_name.endswith ('__')
)
):
# Uncomment to debug this insanity. *sigh*
# attr_value_old = attr_value

# Override the previously retrieved static value of this attribute
# (i.e., the direct value of this attribute *WITHOUT* regard to
# dynamic descriptor lookup, which in the case of a standard
# descriptor builtin like @classmethod is that C-based @classmethod
# descriptor itself) with the dynamic value of this attribute
# (i.e., the indirect value of this attribute *WITH* regard to
# dynamic descriptor lookup, which in the case of a standard
# descriptor builtin like @classmethod is the pure-Python function
# wrapped by that C-based @classmethod descriptor) if this attribute
# supports the descriptor protocol *OR* reduce to a noop otherwise.
attr_value = getattr(cls, attr_name)

# True only if this attribute is directly beartypeable.
is_attr_beartypeable = isinstance(attr_value, TYPES_BEARTYPEABLE)
# Else, this attribute is directly beartypeable.

# If this attribute is...
if (
# Now directly beartypeable *AND*...
is_attr_beartypeable and
# It is *NOT* the case that...
not (
# This attribute is a class *AND*...
isinstance(attr_value, type) and
# This class is non-beartypeable, in which case @beartype should
# avoid attempting to @beartype this class despite technically
# being capable of doing so. Why? Because doing so frequently
# induces *INFINITE FRIGGIN' RECURSION.* See the above
# "enum.Enum" discussion for a pragmatic example.
attr_value in TYPES_UNBEARTYPEABLE
)
):
# This attribute decorated with type-checking configured by this
# configuration if *NOT* already decorated.
attr_value_beartyped = beartype_object(
Expand Down
2 changes: 2 additions & 0 deletions doc/src/_links.rst
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,8 @@
https://www.sympy.org
.. _TensorFlow:
https://www.tensorflow.org
.. _equinox:
https://github.com/patrick-kidger/equinox
.. _nptyping:
https://github.com/ramonhagenaars/nptyping
.. _numerary:
Expand Down
2 changes: 2 additions & 0 deletions doc/src/pep.rst
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,8 @@ you into stunned disbelief that somebody typed all this. [#rsi]_
+------------------------+-----------------------------------------------------------+--------------------------+---------------------------+
| :mod:`enum` | :obj:`~enum.Enum` | **0.16.0**\ \ *current* | *none* |
+------------------------+-----------------------------------------------------------+--------------------------+---------------------------+
| equinox_ | *all* || **0.17.0**\ \ *current* |
+------------------------+-----------------------------------------------------------+--------------------------+---------------------------+
| | :obj:`~enum.StrEnum` | **0.16.0**\ \ *current* | *none* |
+------------------------+-----------------------------------------------------------+--------------------------+---------------------------+
| :mod:`functools` | :obj:`~functools.lru_cache` || **0.15.0**\ \ *current* |
Expand Down

0 comments on commit 58219ba

Please sign in to comment.