Skip to content

Commit

Permalink
beartype.peps.resolve_pep563() C-based x 2.
Browse files Browse the repository at this point in the history
This commit is the last in a commit chain generalizing our public
`beartype.peps.resolve_pep563()` function to explicitly support
low-level C-based decorator objects (e.g., `classmethod`, `property`,
`staticmethod`), resolving feature request #283 kindly submitted by
non-equilibrium quantum master @PhilipVinc (Filippo Vicentini).
Previously, `resolve_pep563()` raised non-human-readable
`AttributeError` exceptions when passed low-level C-based decorator
objects. Huge shout-outs to Plum maestro @wesselb for his ingenious
last-minute game-changing save, which resurrected this feature request
from an early grave as well as resolved a currently broken unit test in
Plum's test suite. Bear Team: when we combine our powers, even GitHub
nods in acknowledgement. (*Forceful fulsome force majeure!*)
  • Loading branch information
leycec committed Sep 21, 2023
1 parent f705456 commit 5170a2b
Show file tree
Hide file tree
Showing 2 changed files with 23 additions and 27 deletions.
37 changes: 15 additions & 22 deletions beartype/peps/_pep563.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# See "LICENSE" for further details.

'''
Beartype :pep:`563` **support** (i.e., low-level functions resolving stringified
Beartype :pep:`563` support (i.e., public callables resolving stringified
:pep:`563`-compliant type hints implicitly postponed by the active Python
interpreter via a ``from __future__ import annotations`` statement at the head
of the external user-defined module currently being introspected).
Expand All @@ -20,7 +20,6 @@
#Python interpreter targets Python >= 3.10 *AND* the passed callable is nested.

# ....................{ IMPORTS }....................
import __future__
from beartype.roar import BeartypePep563Exception
from beartype._check.checkcall import make_beartype_call
from beartype._check.forward.fwdhint import resolve_hint
Expand All @@ -32,7 +31,6 @@
from beartype._util.cache.pool.utilcachepoolobjecttyped import (
release_object_typed)
from beartype._util.func.utilfuncget import get_func_annotations
from beartype._util.utilobject import get_object_name
from collections.abc import Callable

# ....................{ RESOLVERS }....................
Expand Down Expand Up @@ -301,37 +299,32 @@ def get_str(self) -> my_str:
# object of some sort (e.g., class, property, or static method descriptor):
# AttributeError: 'method' object has no attribute '__annotations__'
#
# Ideally, the above call to the get_func_annotations() getter would have
# already detected this to be the case and raised an exception. Tragically,
# C-based decorator objects define a read-only "__annotations__" dunder
# attribute that proxies an original writeable "__annotations__" dunder
# attribute of the pure-Python callables they originally decorated. Ergo,
# detecting this edge case is non-trivial and most most easily deferred to
# this late time. While non-ideal, simplicity >>>> idealism in this case.
except AttributeError as exception:
# Fully-qualified name of this C-based decorator object.
func_name = get_object_name(func)

# Raise a human-readable exception embedding this name.
raise BeartypePep563Exception(
f'C-based decorator object {repr(func)} not pure-Python callable. '
f'Consider passing the pure-Python callable underlying this '
f'C-based decorator object instead (e.g., "{func_name}.__func__").'
) from exception
except AttributeError:
# For the name of each annotated parameter and return of that callable
# and the destringified type hint annotating this parameter or return,
# overwrite the stringified type hint originally annotating this
# parameter or return with this destringified type hint.
#
# Note that:
# * The above assignment is an efficient O(1) operation and thus
# intentionally performed first.
# * This iteration-based assignment is an inefficient O(n) operation
# (where "n" is the number of annotated parameters and returns of that
# callable) and thus intentionally performed last here.
for arg_name, arg_hint in arg_name_to_hint.items():
func.__annotations__[arg_name] = arg_hint

# print(
# f'{func.__name__}() PEP 563-postponed annotations resolved:'
# f'\n\t------[ POSTPONED ]------\n\t{func_hints_postponed}'
# f'\n\t------[ RESOLVED ]------\n\t{func_hints_resolved}'
# )

# ....................{ PRIVATE ~ constants }....................
_FUTURE_ANNOTATIONS = __future__.annotations
'''
:obj:`__future__.annotations` object, globalized as a private constant of this
submodule to negligibly optimize repeated lookups of this object.
'''

# ....................{ PRIVATE ~ resolvers }....................
#FIXME: We currently no longer require this. See above for further commentary.
# from beartype.roar import BeartypeDecorHintPepException
Expand Down
13 changes: 8 additions & 5 deletions beartype_test/a00_unit/a40_api/peps/test_pep563.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,29 +50,32 @@ def test_resolve_pep563() -> None:
# callables.
with raises(BeartypeCallHintForwardRefException):
their_starry_domes(numberless_and_immeasurable_halls)
with raises(BeartypeCallHintForwardRefException):
and_thrones_radiant_with_chrysolite.until_the_doves(
numberless_and_immeasurable_halls)
with raises(BeartypeCallHintForwardRefException):
and_thrones_radiant_with_chrysolite.crystal_column(
'Nor had that scene of ampler majesty')

# Resolve all PEP 563-postponed type hints annotating these callables.
resolve_pep563(their_starry_domes)
resolve_pep563(FrequentWith.until_the_doves)
resolve_pep563(FrequentWith.crystal_column)

# Assert that this function successfully accepts and returns this instance.
assert their_starry_domes(numberless_and_immeasurable_halls) is (
numberless_and_immeasurable_halls)

# Assert that this method successfully accepts and returns this string.
assert FrequentWith.until_the_doves(numberless_and_immeasurable_halls) is (
numberless_and_immeasurable_halls)

# .....................{ FAIL }....................
# Assert that this resolver raises the expected exception when passed an
# uncallable object.
with raises(BeartypePep563Exception):
resolve_pep563('Mont Blanc yet gleams on high:—the power is there,')

# Assert that this resolver raises the expected exception when passed a
# C-based descriptor.
with raises(BeartypePep563Exception):
resolve_pep563(FrequentWith.until_the_doves)

# Assert that this method unsuccessfully raises the excepted exception, due
# to being annotated by a missing forward reference.
with raises(BeartypeCallHintForwardRefException):
Expand Down

0 comments on commit 5170a2b

Please sign in to comment.