Skip to content

Commit

Permalink
Forward reference issubclass() proxying x 1.
Browse files Browse the repository at this point in the history
This commit is the first in a commit chain generalizing our support for
subscripted forward references (e.g., `"type[MuhClass]"`) to proxy both
`isinstance()` *and* `issubclass()` type-checks, en-route to resolving
issue #289 kindly submitted by Google X extraordinaire @patrick-kidger
(Patrick Kidger). Previously, subscripted forward references erroneously
proxied only `isinstance()` type-checks; this omission prevented these
references from correctly resolving subscriptions of the PEP
585-compliant `type[...]` builtin by a forward reference to a class that
has yet to be defined. Specifically, this commit implements (but has yet
to exhaustively test) this generalization. Praise be to the Kidger.
(*Masterful blaster in Electric City full of fulsome elasticity!*)
  • Loading branch information
leycec committed Oct 6, 2023
1 parent d2ed487 commit e4ca4ca
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 27 deletions.
124 changes: 111 additions & 13 deletions beartype/_check/forward/_fwdref.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from beartype._check.forward.fwdtype import bear_typistry
from beartype._util.cache.utilcachecall import callable_cached
from beartype._util.cls.utilclsmake import make_type
from beartype._util.cls.utilclstest import is_type_subclass

# ....................{ METACLASSES }....................
#FIXME: Unit test us up, please.
Expand Down Expand Up @@ -104,7 +105,7 @@ def __getattr__( # type: ignore[misc]
hint_name.endswith('__')
):
raise AttributeError(
f'Forward reference proxy {repr(cls)} dunder attribute '
f'Forward reference proxy dunder attribute '
f'"{cls.__name__}.{hint_name}" not found.'
)
# Else, this unqualified basename is *NOT* that of a non-existent dunder
Expand All @@ -126,8 +127,8 @@ def __instancecheck__( # type: ignore[misc]
:data:`True` only if the passed object is an instance of the external
class referenced by the passed **forward reference subclass** (i.e.,
:class:`._BeartypeForwardRefABC` subclass whose metaclass is this
metaclass and whose :attr:`._BeartypeForwardRefABC.__beartype_name__` class
variable is the fully-qualified name of that external class).
metaclass and whose :attr:`._BeartypeForwardRefABC.__beartype_name__`
class variable is the fully-qualified name of that external class).
Parameters
----------
Expand All @@ -146,7 +147,39 @@ class referenced by this forward reference subclass.

# Return true only if this forward reference subclass insists that this
# object satisfies the external class referenced by this subclass.
return cls.is_instance(obj)
return cls.__beartype_is_instance__(obj)


def __subclasscheck__( # type: ignore[misc]
cls: Type['_BeartypeForwardRefABC'], # pyright: ignore[reportGeneralTypeIssues]
obj: object,
) -> bool:
'''
:data:`True` only if the passed object is a subclass of the external
class referenced by the passed **forward reference subclass** (i.e.,
:class:`._BeartypeForwardRefABC` subclass whose metaclass is this
metaclass and whose :attr:`._BeartypeForwardRefABC.__beartype_name__`
class variable is the fully-qualified name of that external class).
Parameters
----------
cls : Type[_BeartypeForwardRefABC]
Forward reference subclass to test this object against.
obj : object
Arbitrary object to be tested as a subclass of the external class
referenced by this forward reference subclass.
Returns
----------
bool
:data:`True` only if this object is a subclass of the external class
referenced by this forward reference subclass.
'''

# Return true only if this forward reference subclass insists that this
# object is an instance of the external class referenced by this
# subclass.
return cls.__beartype_is_subclass__(obj)


def __repr__( # type: ignore[misc]
Expand All @@ -165,11 +198,26 @@ def __repr__( # type: ignore[misc]

# If this is a subscripted forward reference subclass, append additional
# metadata representing this subscription.
if issubclass(cls, _BeartypeForwardRefIndexedABC):
#
# Ideally, we would test whether this is a subclass of the
# "_BeartypeForwardRefIndexedABC" superclass as follows:
# if issubclass(cls, _BeartypeForwardRefIndexedABC):
#
# Sadly, doing so invokes the __subclasscheck__() dunder method defined
# above, which invokes the
# _BeartypeForwardRefABC.__beartype_is_subclass__() method defined
# above, which tests the type referred to by this subclass rather than
# this subclass itself. In short, this is why you play with madness.
try:
cls_repr += (
f', __beartype_args__={repr(cls.__beartype_args__)}'
f', __beartype_kwargs__={repr(cls.__beartype_kwargs__)}'
)
# If doing so fails with the expected "AttributeError", then this is
# *NOT* a subscripted forward reference subclass. In this avoid,
# silently ignore this common case. *sigh*
except AttributeError:
pass

# Close this representation.
cls_repr += ')'
Expand Down Expand Up @@ -249,9 +297,9 @@ def __new__(cls, *args, **kwargs) -> NoReturn:
f'{repr(cls)} not instantiatable.'
)

# ....................{ TESTERS }....................
# ....................{ PRIVATE ~ testers }....................
@classmethod
def is_instance(cls, obj: object) -> bool:
def __beartype_is_instance__(cls, obj: object) -> bool:
'''
:data:`True` only if the passed object is an instance of the external
class referred to by this forward reference.
Expand All @@ -262,12 +310,66 @@ class referred to by this forward reference.
Arbitrary object to be tested.
Returns
----------
-------
bool
:data:`True` only if this object is an instance of the external
class referred to by this forward reference subclass.
'''

# Resolve the external class referred to by this forward reference and
# permanently store that class in the "__beartype_type__" variable.
cls.__beartype_resolve_type__()

# Return true only if this object is an instance of the external class
# referenced by this forward reference.
return isinstance(obj, cls.__beartype_type__) # type: ignore[arg-type]


@classmethod
def __beartype_is_subclass__(cls, obj: object) -> bool:
'''
:data:`True` only if the passed object is a subclass of the external
class referred to by this forward reference.
Parameters
----------
obj : object
Arbitrary object to be tested.
Returns
-------
bool
:data:`True` only if this object is a subclass of the external class
referred to by this forward reference subclass.
'''

# Resolve the external class referred to by this forward reference and
# permanently store that class in the "__beartype_type__" variable.
cls.__beartype_resolve_type__()

# Return true only if this object is a subclass of the external class
# referenced by this forward reference.
return is_type_subclass(obj, cls.__beartype_type__) # type: ignore[arg-type]

# ....................{ PRIVATE ~ resolvers }....................
#FIXME: [SPEED] Optimize this by refactoring this into a cached class
#property defined on the metaclass of the superclass instead. Since doing so
#is a bit non-trivial and nobody particularly cares, the current naive
#approach certainly suffices for now. *sigh*
@classmethod
def __beartype_resolve_type__(cls) -> None:
'''
**Resolve** (i.e., dynamically lookup) the external class referred to by
this forward reference and permanently store that class in the
:attr:`__beartype_type__` class variable for subsequent lookup.
Caveats
-------
This method should *always* be called before accessing the
:attr:`__beartype_type__` class variable, which should *always* be
assumed to be :data:`None` before calling this method.
'''

# If the external class referenced by this forward reference has yet to
# be resolved, do so now.
if cls.__beartype_type__ is None:
Expand All @@ -292,11 +394,7 @@ class referred to by this forward reference subclass.
#
# In either case, that class is now resolved.

# Return true only if this object is an instance of the external class
# referenced by this forward reference.
return isinstance(obj, cls.__beartype_type__)


# ....................{ SUPERCLASSES ~ index }....................
#FIXME: Unit test us up, please.
class _BeartypeForwardRefIndexedABC(_BeartypeForwardRefABC):
'''
Expand Down
2 changes: 1 addition & 1 deletion beartype/_check/forward/fwdhint.py
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,7 @@ def resolve_hint(
# Initialize this forward scope to the set of all builtin attributes
# (e.g., "str", "Exception"). Although the eval() builtin does, of
# course, implicitly evaluate this stringified type hint against all
# builtin attributes, it does only *AFTER* invoking the
# builtin attributes, it does so only *AFTER* invoking the
# BeartypeForwardScope.__missing__() dunder method with each such
# builtin attribute referenced in this hint. Since handling that
# eccentricity would be less efficient and trivial than simply
Expand Down
26 changes: 13 additions & 13 deletions doc/src/api_claw.rst
Original file line number Diff line number Diff line change
Expand Up @@ -190,11 +190,11 @@ GitHub and Reddit for that.

Other beartype import hooks – like :func:`.beartype_packages` or
:func:`.beartyping` – can be (mis)used to dangerously type-check *other*
third-party packages outside your control that have probably *never* been
third-party packages outside your control that have probably never been
stress-tested with beartype. Those packages could raise type-checking violations
at runtime that you have no control over. If they don't now, they could later.
Forward compatibility is out the window. ``git blame`` has things to say
about that.
Forward compatibility is out the window. ``git blame`` has things to say about
that.

If :func:`.beartype_this_package` fails, there is no hope for your package. Even
though it might be beartype's fault, beartype will still blame you for its
Expand All @@ -205,11 +205,11 @@ Import Hooks API

Beartype import hooks come in two flavours:

* :ref:`Global import hooks <api_claw:global>`, whose effects globally apply to
*all* subsequently imported packages and modules matching various patterns.
* :ref:`Local import hooks <api_claw:local>`, whose effects are locally isolated
to specific packages and modules imported inside specific blocks of code.
*All* subsequently imported packages and modules remain unaffected.
* :ref:`Global import hooks <api_claw:global>`, whose effects encompass *all*
subsequently imported packages and modules matching various patterns.
* :ref:`Local import hooks <api_claw:local>`, whose effects are isolated to only
specific packages and modules imported inside specific blocks of code. Any
subsequently imported packages or modules remain unaffected.

.. _api_claw:global:

Expand All @@ -219,7 +219,7 @@ Global Import Hooks
Global beartype import hooks are... well, *global*. Their claws extend to a
horizontal slice of your full stack. These hooks globally type-check *all*
annotated callables, classes, and variable assignments in *all* subsequently
imported packages and modules (matching various patterns).
imported packages and modules matching various patterns.

With great globality comes great responsibility.

Expand Down Expand Up @@ -251,16 +251,16 @@ With great globality comes great responsibility.
from beartype import BeartypeConf # <-- boilerplate
from beartype.claw import beartype_this_package # <-- boilerplate: the revenge
beartype_this_package(conf=BeartypeConf(is_color=False)) # <-- you hate rainbows
beartype_this_package(conf=BeartypeConf(is_color=False)) # <-- y u hate rainbows?
This hook is typically called as the first statement in the ``__init__``
submodule of some caller-defined (sub)package. If this hook is called from:

* Your top-level ``{your_package}.__init__`` submodule, this hook type-checks
your entire package. This includes *all* submodules and subpackages of your
package.
your entire package. This includes *all* submodules and subpackages across
your entire package.
* Some mid-level ``{your_package}.{your_subpackage}.__init__`` submodule,
this hook type-checks only that subpackage. This includes *only* submodules
this hook type-checks only that subpackage. This includes *all* submodules
and subsubpackages of that subpackage.

As the term "import hook" implies, this hook only applies to subsequent
Expand Down

0 comments on commit e4ca4ca

Please sign in to comment.