Skip to content

Commit

Permalink
Import hook decoration position x 3.
Browse files Browse the repository at this point in the history
This commit is the last in a commit chain enabling end users to
explicitly configure **import hook decoration positions** (i.e.,
competing locations to which the `@beartype` decorator will be
implicitly injected into existing chains of one or more decorators
decorating classes and callables defined by modules imported under
`beartype.claw` import hooks, each with concomitant tradeoffs with
respect to decorator interoperability and quality assurance), resolving
feature request #374 kindly indirectly submitted by bestest @beartype
users @danielward27 and @patrick-kidger courtesy parent feature request
#368. Specifically, this commit exhaustively tests this functionality as
shockingly working. Would @leycec lie!? (*Win some, winsome yards of vineyards!*)
  • Loading branch information
leycec committed May 17, 2024
1 parent 1d0bd23 commit ba3bb42
Show file tree
Hide file tree
Showing 11 changed files with 274 additions and 45 deletions.
6 changes: 1 addition & 5 deletions beartype/_conf/confcls.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,7 @@
TypeWarning,
)
from beartype._data.func.datafuncarg import ARG_VALUE_UNPASSED
from beartype._util.cache.utilcachecall import callable_cached
from beartype._util.utilobject import (
SENTINEL,
get_object_type_basename,
)
from beartype._util.utilobject import get_object_type_basename
from threading import Lock

# ....................{ CLASSES }....................
Expand Down
59 changes: 38 additions & 21 deletions beartype/_conf/confenum.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,25 @@ class BeartypeDecorationPosition(Enum):
Note that this position is *not* the default (and thus need be
explicitly passed anywhere). Why? Various hand-wavy reasons, the most
compelling of which is that this position ignores explicitly configured
:func:`beartype.beartype` decorations (e.g.,
``@beartype(conf=BeartypeConf(...))``). When this position is used,
implicit :func:`beartype.beartype` decorations injected by
:mod:`beartype.claw` import hooks assume precedence over explicit
:func:`beartype.beartype` decorations manually specified by users. Since
that is almost never what anyone wants, this is *not* the default: e.g.,
compelling of which is that this position ignores standard decorators
and **thus violates PEP standards.** Notably, this position ignores:
* The :pep:`484`-compliant :func:`typing.no_type_check` decorator, which
then erroneously instructs :func:`beartye.beartype` to type-check
classes and callables that should *not* be type-checked.
* The :pep:`557`-compliant :func:`dataclasses.dataclass` decorator,
which then prevents :func:`beartye.beartype` from type-checking
dataclasses.
* Explicitly configured :func:`beartype.beartype` decorators (e.g.,
``@beartype(conf=BeartypeConf(...))``), which then instructs
:func:`beartye.beartype` to type-check classes and callables under
differing configurations.
When this position is used, implicit :func:`beartype.beartype`
decorators injected by :mod:`beartype.claw` import hooks assume
precedence over *all* other decorators (including those listed above),
with predictably catastrophic results. Since this is almost never what
anyone wants, this is *not* the default: e.g.,
.. code-block:: python
Expand All @@ -68,30 +80,35 @@ class ClassyData(object):
integral_datum: int
# ...into chains of class decorators like this.
from beartype import beartype
from dataclasses import dataclass
@beartype(conf=BeartypeConf(is_debug=True))
@dataclass
@beartype # <-- @beartype decorates first rather than last! \\o/
@beartype(conf=BeartypeConf(is_debug=True)) # <-- *IGNORED*
@dataclass # <-- *IGNORED* by the @beartype decorator injected below
@beartype # <-- @beartype now ignores all of the above decorators!!
class ClassyData(object):
integral_datum: int
In the above example, the default :func:`beartype.beartype` decorator
injected by the :func:`beartype.claw.beartype_this_package` silently
overwrites the non-default
``@beartype(conf=BeartypeConf(is_debug=True))`` decorator manually
configured by the author of that third-party package. Consequently,
caveats apply to usage of this position:
fails to type-check the :func:`dataclasses.dataclass` decorator and then
overwrites the ``@beartype(conf=BeartypeConf(is_debug=True))`` decorator
manually configured by the author of that third-party package.
Consequently, caveats apply to usage of this position:
* This position should only be applied to codebases that avoid
explicitly decorating *any* classes or callables with the
:func:`beartype.beartype` decorator.
explicitly decorating classes and/or callables with standard
decorators, including:
* The :pep:`484`-compliant :func:`typing.no_type_check` decorator.
* The :pep:`557`-compliant :func:`dataclasses.dataclass` decorator.
* The :func:`beartype.beartype` decorator itself.
* Equivalently, this position should only be applied to codebases that
implicitly decorate *all* classes and callables with
:mod:`beartype.claw` import hooks.
* Equivalently, if a codebase explicitly decorates even a single class
or callable with the :func:`beartype.beartype` decorator, this
position *cannot* be used.
or callable with the :func:`typing.no_type_check`,
:func:`dataclasses.dataclass`, or :func:`beartype.beartype`
decorators, this position should *not* be used.
* Consequently, this position should *not* be applied to other packages
outside your direct control.
* In particular, this position should *not* be applied to all packages
Expand Down Expand Up @@ -144,8 +161,8 @@ def chad_func() -> int:
return 42
In the above example, the default :func:`beartype.beartype` decorator
injected by the :func:`beartype.claw.beartype_this_package` is silently
ignored in favour of the non-default
injected by the :func:`beartype.claw.beartype_this_package` import hook
is silently ignored in favour of the non-default
``@beartype(conf=BeartypeConf(is_debug=True))`` decorator manually
configured by the author of that third-party package.
'''
Expand Down
19 changes: 15 additions & 4 deletions beartype_test/a00_unit/a90_claw/a90_hook/test_claw_intraprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,10 @@ def test_claw_intraprocess_beartype_package() -> None:
# Import all submodules of the package hooked above, exercising that these
# submodules are subject to that import hook.
from beartype_test.a00_unit.data.claw.intraprocess.hookable_package import (
kind, pep)
conf,
kind,
pep,
)

# Import an arbitrary submodule *NOT* subject to those import hooks.
from beartype_test.a00_unit.data.claw.intraprocess import unhookable_module
Expand Down Expand Up @@ -148,7 +151,10 @@ def test_claw_intraprocess_beartype_packages() -> None:
# Import all submodules of the package hooked above, exercising that these
# submodules are subject to that import hook.
from beartype_test.a00_unit.data.claw.intraprocess.hookable_package import (
kind, pep)
conf,
kind,
pep,
)

# Assert that repeating the same import hook as above silently succeeds.
beartype_packages(PACKAGE_NAMES)
Expand Down Expand Up @@ -225,7 +231,10 @@ def test_claw_intraprocess_beartype_all() -> None:
# Import *ALL* "beartype.claw"-specific data submodules, exercising that
# these submodules are subject to that import hook.
from beartype_test.a00_unit.data.claw.intraprocess.hookable_package import (
kind, pep)
conf,
kind,
pep,
)

# Assert that repeating the same import hook as above silently succeeds.
beartype_all()
Expand Down Expand Up @@ -281,7 +290,9 @@ def test_claw_intraprocess_beartyping() -> None:
# testing whether or not this context manager raises exceptions under a
# different (and thus conflicting) beartype configuration.
from beartype_test.a00_unit.data.claw.intraprocess.hookable_package import (
pep)
conf,
pep,
)

# Assert that nesting a similar context manager under a non-default
# configuration nonetheless semantically equivalent to the default
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/usr/bin/env python3
# --------------------( LICENSE )--------------------
# Copyright (c) 2014-2024 Beartype authors.
# See "LICENSE" for further details.

'''
**Beartype import hook configuration subpackage initialization submodule**
(i.e., data module mimicking real-world usage of various :func:`beartype.claw`
import hooks configured by different beartype configurations).
'''

# ....................{ IMPORTS }....................
from beartype import (
BeartypeConf,
BeartypeDecorationPosition,
)
from beartype.claw import beartype_package

# Subject this single module to a beartype import hook configured to inject the
# @beartype decorator first before (i.e., below) all other class decorators.
beartype_package(
package_name=(
'beartype_test.a00_unit.data.claw.intraprocess.hookable_package.conf.'
'data_claw_conf_decoration_position_funcs_first'
),
conf=BeartypeConf(
claw_decoration_position_funcs=BeartypeDecorationPosition.FIRST),
)
from beartype_test.a00_unit.data.claw.intraprocess.hookable_package.conf import (
data_claw_conf_decoration_position_funcs_first)

# Subject this single module to a beartype import hook configured to inject the
# @beartype decorator first before (i.e., below) all other class decorators.
beartype_package(
package_name=(
'beartype_test.a00_unit.data.claw.intraprocess.hookable_package.conf.'
'data_claw_conf_decoration_position_types_first'
),
conf=BeartypeConf(
claw_decoration_position_types=BeartypeDecorationPosition.FIRST),
)
from beartype_test.a00_unit.data.claw.intraprocess.hookable_package.conf import (
data_claw_conf_decoration_position_types_first)
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/usr/bin/env python3
# --------------------( LICENSE )--------------------
# Copyright (c) 2014-2024 Beartype authors.
# See "LICENSE" for further details.

'''
**Beartype import hookable configuration first callable decorator position
submodule** (i.e., data module defining callables decorated by chains of one or
more decorators into which the :mod:`beartype.beartype` decorator will be
injected as the first decorator, mimicking real-world usage of the
:func:`beartype.claw.beartype_package` import hook from external callers).
'''

# ....................{ IMPORTS }....................
from beartype.roar import BeartypeCallHintReturnViolation
from beartype.typing import no_type_check
from beartype._util.py.utilpyversion import IS_PYTHON_AT_LEAST_3_11
from pytest import raises

# ....................{ FUNCTIONS }....................
# Validate that the import hook presumably registered by the caller implicitly
# injects the @beartype decorator as the first decorator in *ALL* chains of
# callable decorations.

@no_type_check
def night_followed() -> int:
'''
Arbitrary function trivially violating its return type hint.
'''

return 'Night followed, clad with stars. On every side'

# Assert that the import hook registered by the caller ignored the
# @no_type_check decorator decorating the callable defined above by forcefully
# injecting the @beartype decorator as the first decorator on that callable.
with raises(BeartypeCallHintReturnViolation):
night_followed()

# ....................{ CLASSES }....................
# Validate that the import hook presumably registered by the caller implicitly
# injects the @beartype decorator as the last decorator in *ALL* chains of
# class decorations.

# If the active Python interpreter targets Python >= 3.11, the standard
# @no_type_check decorator correctly applies itself to classes. In this case...
if IS_PYTHON_AT_LEAST_3_11:
@no_type_check
class MoreHorribly(object):
'''
Arbitrary class decorated by one or more decorators, which the import
hook registered by the caller will then respect by continuing to inject
the :func:`beartype.beartype` decorator as the last decorator.
'''

@staticmethod
def the_multitudinous_streams() -> int:
'''
Arbitrary static method trivially violating its return type hint.
'''

return 'More horribly the multitudinous streams'

# Assert that the import hook registered by the caller respected the
# @no_type_check decorator decorating the class defined above by continuing
# to inject the @beartype decorator as the last decorator on that class.
assert MoreHorribly.the_multitudinous_streams() == (
'More horribly the multitudinous streams')
# Else, the active Python interpreter targets Python <= 3.10. In this case, the
# standard @no_type_check decorator fails to correctly apply itself to classes.
# In this case, simply avoid performing the prior test for simplicity. *shrug*
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#!/usr/bin/env python3
# --------------------( LICENSE )--------------------
# Copyright (c) 2014-2024 Beartype authors.
# See "LICENSE" for further details.

'''
**Beartype import hookable configuration first class decorator position
submodule** (i.e., data module defining classes decorated by chains of one or
more decorators into which the :mod:`beartype.beartype` decorator will be
injected as the first decorator, mimicking real-world usage of the
:func:`beartype.claw.beartype_package` import hook from external callers).
'''

# ....................{ IMPORTS }....................
from beartype.roar import BeartypeCallHintReturnViolation
from beartype.typing import no_type_check
from pytest import raises

# ....................{ CLASSES }....................
# Validate that the import hook presumably registered by the caller implicitly
# injects the @beartype decorator as the first decorator in *ALL* chains of
# class decorations.

@no_type_check
class EntwinedInDuskierWreaths(object):
'''
Arbitrary class decorated by one or more decorators, which the import hook
registered by the caller will then ignore by forcefully injecting the
:func:`beartype.beartype` decorator as the first decorator.
'''

@staticmethod
def her_braided_locks() -> int:
'''
Arbitrary static method trivially violating its return type hint.
'''

return 'Entwined in duskier wreaths her braided locks'

# Assert that the import hook registered by the caller ignored the
# @no_type_check decorator decorating the class defined above by forcefully
# injecting the @beartype decorator as the first decorator on that class.
with raises(BeartypeCallHintReturnViolation):
EntwinedInDuskierWreaths.her_braided_locks()

# ....................{ FUNCTIONS }....................
# Validate that the import hook presumably registered by the caller implicitly
# injects the @beartype decorator as the last decorator in *ALL* chains of
# callable decorations.

@no_type_check
def over_the_fair_front() -> int:
'''
Arbitrary function trivially violating its return type hint.
'''

return "O'er the fair front and radiant eyes of day;"

# Assert that the import hook registered by the caller respected the
# @no_type_check decorator decorating the function defined above by continuing
# to inject the @beartype decorator as the last decorator on that function.
assert over_the_fair_front() == "O'er the fair front and radiant eyes of day;"
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# See "LICENSE" for further details.

'''
Project-wide **beartype import hook Python enhancement proposal (PEP) subpackage
**Beartype import hook Python enhancement proposal (PEP) subpackage
initialization submodule** (i.e., data module mimicking real-world usage of
various :func:`beartype.claw` import hooks on various PEPs).
'''
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@
# See "LICENSE" for further details.

'''
Project-wide **beartype import hook** :pep:`526` **subpackage initialization
submodule** (i.e., data module mimicking real-world usage of various
:func:`beartype.claw` import hooks on :pep:`526`-compliant annotated variable
assignments).
**Beartype import hook** :pep:`526` **subpackage initialization submodule**
(i.e., data module mimicking real-world usage of various :func:`beartype.claw`
import hooks on :pep:`526`-compliant annotated variable assignments).
'''

# ....................{ IMPORTS }....................
Expand All @@ -17,6 +16,15 @@
from beartype_test.a00_unit.data.claw.intraprocess.hookable_package.pep.pep526 import (
data_claw_pep526_raise)

# Subject this single module to a beartype import hook configured to silently
# ignore (rather than type-check) all PEP 526-compliant annotated assignments.
beartype_package(
'beartype_test.a00_unit.data.claw.intraprocess.hookable_package.pep.pep526.data_claw_pep526_ignore',
conf=BeartypeConf(claw_is_pep526=False),
)
from beartype_test.a00_unit.data.claw.intraprocess.hookable_package.pep.pep526 import (
data_claw_pep526_ignore)

# Subject this single module to a beartype import hook configured to emit
# non-fatal warnings of an arbitrary beartype-specific warning type unlikely to
# arise via accidental change rather than raise fatal exceptions.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env python3
# --------------------( LICENSE )--------------------
# Copyright (c) 2014-2024 Beartype authors.
# See "LICENSE" for further details.

'''
**Beartype import hookable** :pep:`526` **ignorance submodule** (i.e., data
module containing *only* :pep:`526`-compliant annotated variable assignments
ignored rather than type-checked, mimicking real-world usage of the
:func:`beartype.claw.beartype_package` import hook from external callers).
'''

# ....................{ PEP 526 }....................
# Validate that the import hook presumably installed by the caller avoids
# implicitly appending all PEP 526-compliant annotated assignment statements in
# this submodule with calls to beartype's statement-level
# beartype.door.die_if_unbearable() exception-raiser.

# Assert that a PEP 526-compliant annotated assignment statement assigning an
# object violating the type hint annotating that statement raises *NO*
# exception.
twilight_ascending_slowly: int = 'Twilight, ascending slowly from the east,'

0 comments on commit ba3bb42

Please sign in to comment.