Skip to content

Commit

Permalink
@classmethod C-based type decoration.
Browse files Browse the repository at this point in the history
This commit generalizes the `@beartype` decorator to support decoration
of user-defined types declaring class methods by directly calling the
builtin `@classmethod` decorator as a function passed a C-based callable
type (e.g., `classmethod(types.GenericAlias)`), resolving issue #358
kindly submitted by "Computer Graphics and Visualization" sufferer
@sylvorg, who graciously sacrificed his entire undergraduate GPA for the
gradual betterment of @beartype. Your journey of woe and hardship will
*not* be forgotten, @sylvorg! Specifically, this commit enables
`@beartype` to support the standard idiom for user-defined subscriptable
type hint factories under Python >= 3.9:

```python
from abc import ABCMeta
from beartype import beartype
from types import GenericAlias

@beartype
class MuhTypeHintFactory(metaclass=ABCMeta):
    # This exact one liner appears verbatim throughout the
    # standard library (as well as third-party packages).
    __class_getitem__ = classmethod(GenericAlias)
```

Previously, `@beartype` raised unreadable exceptions when confronted
with such commonplace yet dastardly types. "Dastards!", @leycec says.
(*Faster rasterized pasta-rice on ice!*)
  • Loading branch information
leycec committed Apr 10, 2024
1 parent 5bdbe24 commit ce6cfbe
Show file tree
Hide file tree
Showing 5 changed files with 247 additions and 150 deletions.
41 changes: 39 additions & 2 deletions beartype/_decor/_decornontype.py
Expand Up @@ -413,8 +413,45 @@ def beartype_descriptor_decorator_builtin(
#
# If this descriptor is a class method...
elif descriptor_type is MethodDecoratorClassType:
# Pure-Python unbound function type-checking this class method.
# Possibly C-based callable wrappee object decorated by this descriptor.
#
# Note that this wrappee is typically but *NOT* necessarily a
# pure-Python unbound function. This descriptor explicitly permits the
# decorated object to be a callable C-based type (i.e., defining the
# __call__() dunder method), which numerous standard and third-party
# pure-Python classes then leverage to augment those classes into
# subscriptable type hint factories via a simple one-liner: e.g.,
# from abc import ABCMeta
# from beartype import beartype
# from types import GenericAlias
#
# @beartype
# class MuhTypeHintFactory(metaclass=ABCMeta):
# # This exact one liner appears verbatim throughout the
# # standard library (as well as third-party packages).
# __class_getitem__ = classmethod(GenericAlias)
#
# Ergo, the name "__func__" of this dunder attribute is disingenuous.
# This descriptor does *NOT* merely decorate functions; this descriptor
# permissively decorates all callable objects.
descriptor_wrappee = descriptor.__func__ # type: ignore[union-attr]

# If this wrappee is *NOT* a pure-Python unbound function, this wrappee
# is C-based and/or a type. In either case, avoid type-checking this
# wrappee by silently preserving this descriptor as is. Why? If this
# wrappee is:
# * C-based, this wrappee *CANNOT* be decorated with type-checking.
# * A type, this wrappee *COULD* be effectively decorated with
# type-checking by decorating its __call__() dunder method. However,
# this type may *NOT* have been intended to be decorated by @beartype.
# Indeed, this type may *NOT* even reside within the same package.
# That the current class references this type is an insufficient
# reason to transitively decorate external types without user consent.
if not is_func_python(descriptor_wrappee):
return descriptor
# Else, this wrappee is a pure-Python unbound function.

# Pure-Python unbound function type-checking this class method.
# Note that:
# * Python 3.8, 3.9, and 3.10 explicitly permit the @classmethod
# decorator to be chained into the @property decorator: e.g.,
Expand Down Expand Up @@ -451,7 +488,7 @@ def beartype_descriptor_decorator_builtin(
# * The low-level beartype_func() decorator (which requires the passed
# object to be callable, which the descriptor created and returned
# by the @property decorator is *NOT*).
func_checked = beartype_nontype(descriptor.__func__, **kwargs) # type: ignore[union-attr]
func_checked = beartype_nontype(descriptor_wrappee, **kwargs)

# Return a new class method descriptor decorating the pure-Python
# unbound function wrapped by this descriptor with type-checking,
Expand Down
1 change: 0 additions & 1 deletion beartype/_decor/_decortype.py
Expand Up @@ -131,7 +131,6 @@ class variable or method annotated by this hint *or* :data:`None`).
# Else, this decorator has yet to decorate this class.

# ....................{ LOCALS }....................

# Replace the passed class stack with a new class stack appending this
# decorated class to the top of this stack, reflecting the fact that this
# decorated class is now the most deeply lexically nested class for the
Expand Down
Expand Up @@ -4,10 +4,11 @@
# See "LICENSE" for further details.

'''
**Beartype decorator wrapper function** unit tests.
**Beartype decorator God-mode** unit tests.
This submodule unit tests high-level functionality of type-checking wrapper
functions dynamically generated by the :func:`beartype.beartype` decorator.
This submodule unit tests fragile aspects of the :func:`beartype.beartype`
decorator that warrant **God-mode privileges** (i.e., a complete lack of
caution, common sense, and goodwill to your dev box).
'''

# ....................{ IMPORTS }....................
Expand Down
157 changes: 157 additions & 0 deletions beartype_test/a00_unit/a70_decor/test_decornontype.py
@@ -0,0 +1,157 @@
#!/usr/bin/env python3
# --------------------( LICENSE )--------------------
# Copyright (c) 2014-2024 Beartype authors.
# See "LICENSE" for further details.

'''
**Beartype decorator non-class decoration** unit tests.
This submodule unit tests high-level functionality of the
:func:`beartype.beartype` decorator with respect to decorating **non-classes**
(e.g., unbound functions) irrespective of lower-level type hinting concerns
(e.g., PEP-compliance and -noncompliance).
'''

# ....................{ IMPORTS }....................
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# WARNING: To raise human-readable test errors, avoid importing from
# package-specific submodules at module scope.
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
from beartype_test._util.mark.pytskip import (
skip_if_python_version_greater_than_or_equal_to,
skip_if_python_version_less_than,
)

# ....................{ TESTS ~ wrapper }....................
def test_decor_nontype_wrapper_isomorphic() -> None:
'''
Test the :func:`beartype.beartype` decorator on **isomorphic wrappers**
(i.e., callables decorated by the standard :func:`functools.wraps` decorator
for wrapping pure-Python callables with additional functionality defined by
higher-level decorators such that those wrappers isomorphically preserve
both the number and types of all passed parameters and returns by accepting
only a variadic positional argument and a variadic keyword argument).
'''

# ....................{ IMPORTS }....................
# Defer test-specific imports.
from beartype import beartype
from beartype.roar import BeartypeCallHintParamViolation
from collections.abc import Callable
from functools import wraps
from pytest import raises

# ....................{ WRAPPERS }....................
def hang_their_mute_thoughts(on_the_mute_walls_around: str) -> int:
'''
Arbitrary **undecorated wrappee** (i.e., lower-level callable wrapped by
the higher-level :func:`hang_their_mute_thoughts` wrapper intentionally
*not* decorated by the :func:`.beartype` decorator).
'''

return len(on_the_mute_walls_around)


@beartype
@wraps(hang_their_mute_thoughts)
def he_lingered(*args, **kwargs):
'''
Arbitrary **decorated isomorphic non-closure wrapper** (i.e., isomorphic
wrapper defined as a function rather than closure, decorated by the
:func:`.beartype` decorator).
'''

return hang_their_mute_thoughts(*args, **kwargs)


@beartype
def of_the_worlds_youth(func: Callable) -> Callable:
'''
Arbitrary **decorated isomorphic non-closure wrapper decorator** (i.e.,
decorator function creating and returning an isomorphic wrapper defined
as a closure, all decorated by the :func:`.beartype` decorator).
'''

@beartype
@wraps(func)
def through_the_long_burning_day(*args, **kwargs):
'''
Arbitrary **decorated isomorphic closure wrapper** (i.e., isomorphic
wrapper defined as a closure, decorated by the :func:`.beartype`
decorator).
'''

return func(*args, **kwargs)

# Return this wrapper.
return through_the_long_burning_day


# Isomorphic closure wrapper created and returned by the above decorator.
when_the_moon = of_the_worlds_youth(hang_their_mute_thoughts)

# ....................{ PASS }....................
# Assert that these wrappers passed valid parameters return the expected
# values.
assert he_lingered('He lingered, poring on memorials') == 32
assert when_the_moon(
'Gazed on those speechless shapes, nor, when the moon') == 52

# ....................{ FAIL }....................
# Assert that these wrappers passed invalid parameters raise the expected
# exceptions.
with raises(BeartypeCallHintParamViolation):
he_lingered(b"Of the world's youth, through the long burning day")
with raises(BeartypeCallHintParamViolation):
when_the_moon(b"Filled the mysterious halls with floating shades")


def test_decor_nontype_wrapper_type() -> None:
'''
Test the :func:`beartype.beartype` decorator on **type wrappers**
(i.e., types decorated by the standard :func:`functools.wraps` decorator
for wrapping arbitrary types with additional functionality defined by
higher-level decorators, despite the fact that wrapping types does *not*
necessarily make as much coherent sense as one would think it does).
'''

# ....................{ IMPORTS }....................
# Defer test-specific imports.
from beartype import beartype
from beartype.typing import Any
from functools import wraps

# ....................{ WRAPPERS }....................
@beartype
@wraps(list)
def that_echoes_not_my_thoughts(*args: Any, **kwargs: Any):
'''
Arbitrary **decorated type non-closure wrapper** (i.e., wrapper defined
as a function wrapped by an arbitrary type decorated by the
:func:`.beartype` decorator).
'''

return list(*args, **kwargs)

# ....................{ ASSERTS }....................
# Assert that this wrapper passed valid parameters returns the expected
# value.
assert that_echoes_not_my_thoughts(('A', 'gloomy', 'smile',)) == [
'A', 'gloomy', 'smile']

# ....................{ TESTS ~ fail : wrappee }....................
def test_decor_nontype_type_fail() -> None:
'''
Test unsuccessful usage of the :func:`beartype.beartype` decorator for an
**invalid wrappee** (i.e., object *not* decoratable by this decorator).
'''

# Defer test-specific imports.
from beartype import beartype
from beartype.roar import BeartypeDecorWrappeeException
from pytest import raises

# Assert that decorating an uncallable object raises the expected
# exception.
with raises(BeartypeDecorWrappeeException):
beartype(('Book of the Astronomican', 'Slaves to Darkness',))

0 comments on commit ce6cfbe

Please sign in to comment.