diff --git a/beartype/_conf/confcls.py b/beartype/_conf/confcls.py index 2f7ea0ef..738495ee 100644 --- a/beartype/_conf/confcls.py +++ b/beartype/_conf/confcls.py @@ -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 }.................... diff --git a/beartype/_conf/confenum.py b/beartype/_conf/confenum.py index 92d2f94e..b712b7b2 100644 --- a/beartype/_conf/confenum.py +++ b/beartype/_conf/confenum.py @@ -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 @@ -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 @@ -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. ''' diff --git a/beartype_test/a00_unit/a90_claw/a90_hook/test_claw_intraprocess.py b/beartype_test/a00_unit/a90_claw/a90_hook/test_claw_intraprocess.py index 7f76f447..caa0d67a 100644 --- a/beartype_test/a00_unit/a90_claw/a90_hook/test_claw_intraprocess.py +++ b/beartype_test/a00_unit/a90_claw/a90_hook/test_claw_intraprocess.py @@ -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 @@ -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) @@ -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() @@ -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 diff --git a/beartype_test/a00_unit/data/claw/intraprocess/hookable_package/conf/__init__.py b/beartype_test/a00_unit/data/claw/intraprocess/hookable_package/conf/__init__.py new file mode 100644 index 00000000..3a966988 --- /dev/null +++ b/beartype_test/a00_unit/data/claw/intraprocess/hookable_package/conf/__init__.py @@ -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) diff --git a/beartype_test/a00_unit/data/claw/intraprocess/hookable_package/conf/data_claw_conf_decoration_position_funcs_first.py b/beartype_test/a00_unit/data/claw/intraprocess/hookable_package/conf/data_claw_conf_decoration_position_funcs_first.py new file mode 100644 index 00000000..54cec469 --- /dev/null +++ b/beartype_test/a00_unit/data/claw/intraprocess/hookable_package/conf/data_claw_conf_decoration_position_funcs_first.py @@ -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* diff --git a/beartype_test/a00_unit/data/claw/intraprocess/hookable_package/conf/data_claw_conf_decoration_position_types_first.py b/beartype_test/a00_unit/data/claw/intraprocess/hookable_package/conf/data_claw_conf_decoration_position_types_first.py new file mode 100644 index 00000000..ababccd1 --- /dev/null +++ b/beartype_test/a00_unit/data/claw/intraprocess/hookable_package/conf/data_claw_conf_decoration_position_types_first.py @@ -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;" diff --git a/beartype_test/a00_unit/data/claw/intraprocess/hookable_package/pep/__init__.py b/beartype_test/a00_unit/data/claw/intraprocess/hookable_package/pep/__init__.py index 02a81ac7..1ae8a255 100644 --- a/beartype_test/a00_unit/data/claw/intraprocess/hookable_package/pep/__init__.py +++ b/beartype_test/a00_unit/data/claw/intraprocess/hookable_package/pep/__init__.py @@ -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). ''' diff --git a/beartype_test/a00_unit/data/claw/intraprocess/hookable_package/pep/pep526/__init__.py b/beartype_test/a00_unit/data/claw/intraprocess/hookable_package/pep/pep526/__init__.py index 4df65739..9bde1097 100644 --- a/beartype_test/a00_unit/data/claw/intraprocess/hookable_package/pep/pep526/__init__.py +++ b/beartype_test/a00_unit/data/claw/intraprocess/hookable_package/pep/pep526/__init__.py @@ -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 }.................... @@ -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. diff --git a/beartype_test/a00_unit/data/claw/intraprocess/hookable_package/pep/pep526/data_claw_pep526_ignore.py b/beartype_test/a00_unit/data/claw/intraprocess/hookable_package/pep/pep526/data_claw_pep526_ignore.py new file mode 100644 index 00000000..f4f02683 --- /dev/null +++ b/beartype_test/a00_unit/data/claw/intraprocess/hookable_package/pep/pep526/data_claw_pep526_ignore.py @@ -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,' diff --git a/beartype_test/a00_unit/data/claw/intraprocess/hookable_package/pep/pep526/data_claw_pep526_raise.py b/beartype_test/a00_unit/data/claw/intraprocess/hookable_package/pep/pep526/data_claw_pep526_raise.py index 638dddc9..4c75eabb 100644 --- a/beartype_test/a00_unit/data/claw/intraprocess/hookable_package/pep/pep526/data_claw_pep526_raise.py +++ b/beartype_test/a00_unit/data/claw/intraprocess/hookable_package/pep/pep526/data_claw_pep526_raise.py @@ -4,11 +4,11 @@ # See "LICENSE" for further details. ''' -Project-wide **beartype import hookable** :pep:`526` **exception submodule** -(i.e., data module containing *only* :pep:`526`-compliant annotated variable -assignments raising :class:`beartype.roar.BeartypeDoorHintViolation` violations, -mimicking real-world usage of the :func:`beartype.claw.beartype_package` import -hook from an external caller). +**Beartype import hookable** :pep:`526` **exception submodule** (i.e., data +module containing *only* :pep:`526`-compliant annotated variable assignments +raising :class:`beartype.roar.BeartypeDoorHintViolation` violations, mimicking +real-world usage of the :func:`beartype.claw.beartype_package` import hook from +external callers). ''' # ....................{ IMPORTS }.................... diff --git a/beartype_test/a00_unit/data/claw/intraprocess/hookable_package/pep/pep526/data_claw_pep526_warn.py b/beartype_test/a00_unit/data/claw/intraprocess/hookable_package/pep/pep526/data_claw_pep526_warn.py index 033ed679..91c06f71 100644 --- a/beartype_test/a00_unit/data/claw/intraprocess/hookable_package/pep/pep526/data_claw_pep526_warn.py +++ b/beartype_test/a00_unit/data/claw/intraprocess/hookable_package/pep/pep526/data_claw_pep526_warn.py @@ -4,11 +4,11 @@ # See "LICENSE" for further details. ''' -Project-wide **beartype import hookable** :pep:`526` **warning submodule** -(i.e., data module containing *only* :pep:`526`-compliant annotated variable -assignments emitting :exc:`beartype.roar.BeartypeValeLambdaWarning` violations, -mimicking real-world usage of the :func:`beartype.claw.beartype_package` import -hook from an external caller). +**Beartype import hookable** :pep:`526` **warning submodule** (i.e., data module +containing *only* :pep:`526`-compliant annotated variable assignments emitting +:exc:`beartype.roar.BeartypeValeLambdaWarning` violations, mimicking real-world +usage of the :func:`beartype.claw.beartype_package` import hook from external +callers). ''' # ....................{ IMPORTS }....................