Skip to content

Commit

Permalink
Import hook decoration position x 1.
Browse files Browse the repository at this point in the history
This commit is the first 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), en-route
to resolving feature request #374 kindly indirectly submitted by bestest
@beartype users @danielward27 and @patrick-kidger courtesy parent
feature request #368. Specifically, this commit:

* Defines a new public `beartype.BeartypeDecorationPosition`
  configuration enumeration, currently containing this pair of members:
  * `BeartypeDecorationPosition.FIRST`, the **first (i.e., bottom-most)
    decorator position**. Passing this member configures
    :mod:`beartype.claw` import hooks to inject the
    :func:`beartype.beartype` decorator as the first (i.e., bottom-most)
    decorator in relevant decorator chains. Examples below or it didn't
    happen.
  * `BeartypeDecorationPosition.LAST`, the **last (i.e., top-most)
    decorator position**. As the default, this position need *not* be
    explicitly passed
* Defines a pair of new optional parameters with which to instantiate
  **beartype configurations** (i.e., `beartype.BeartypeConf` objects):
  * `claw_decoration_position_funcs`, the **import hook callable
    decorator position** (i.e., location to which the `@beartype`
    decorator will be implicitly injected into existing chains of one or
    more decorators decorating functions and methods defined by modules
    imported under :mod:`beartype.claw` import hooks). Defaults to
    `BeartypeDecorationPosition.LAST`.

    Modifying this configures import hooks to inject `@beartype` as the
    first (rather than last) decorator for callables. If your codebase
    requires this, consider submitting an issue to the @beartype issue
    tracker. Ideally, the `@beartype` decorator should be
    order-invariant with respect to decorator chaining and thus support
    decoration in *any* position – including the default last position:
    e.g.,

    ```python
    # Registering this import hook...
    from beartype import BeartypeConf, BeartypeDecorationPosition
    from beartype.claw import beartype_this_package
    beartype_this_package(conf=BeartypeConf(
        claw_decoration_position_funcs=(
            BeartypeDecorationPosition.FIRST)))

    # ...transforms chains of function decorators like this...
    from functools import cache
    @cache
    def chad_func() -> int:
        return 42

    # ...into chains of function decorators like this.
    from beartype import beartype
    from functools import cache
    @cache
    @beartype  # <-- @beartype decorates first rather than last! \\o/
    def chad_func() -> int:
        return 42
    ```
  * `claw_decoration_position_types`, the **import hook class decorator
    position** (i.e., location to which the `@beartype` decorator will
    be implicitly injected into existing chains of one or more
    decorators decorating classes defined by modules imported under
    `beartype.claw` import hooks). Defaults to
    `BeartypeDecorationPosition.LAST`.

    Modifying this configures import hooks to inject `@beartype` as the
    first (rather than last) decorator for classes. As above, if your
    codebase requires this, consider submitting an issue to the
    @beartype issue tracker: e.g.,

    ```python
    # Registering this import hook...
    from beartype import BeartypeConf, BeartypeDecorationPosition
    from beartype.claw import beartype_this_package
    beartype_this_package(conf=BeartypeConf(
        claw_decoration_position_types=BeartypeDecorationPosition.FIRST))

    # ...transforms chains of class decorators like this...
    from dataclasses import dataclass
    @DataClass
    class ClassyData(object):
        integral_datum: int

    # ...into chains of class decorators like this.
    from beartype import beartype
    from dataclasses import dataclass
    @DataClass
    @beartype  # <-- @beartype decorates first rather than last! \\o/
    class ClassyData(object):
        integral_datum: int
    ```

Naturally, not much is tested and even less is likely to work. (*Boomy looms of doomy rooms!*)
  • Loading branch information
leycec committed May 15, 2024
1 parent f4fb42c commit 72e3eb1
Show file tree
Hide file tree
Showing 13 changed files with 402 additions and 124 deletions.
1 change: 1 addition & 0 deletions beartype/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
from beartype._conf.confcls import (
BeartypeConf as BeartypeConf)
from beartype._conf.confenum import (
BeartypeDecorationPosition as BeartypeDecorationPosition,
BeartypeStrategy as BeartypeStrategy,
BeartypeViolationVerbosity as BeartypeViolationVerbosity,
)
Expand Down
2 changes: 1 addition & 1 deletion beartype/_check/code/codemake.py
Original file line number Diff line number Diff line change
Expand Up @@ -2152,7 +2152,7 @@ def _enqueue_hint_child(pith_child_expr: str) -> str:
# * The higher-level die_unless_hint_nonpep() validator is
# intentionally *NOT* called here, as doing so would permit both:
# * PEP-noncompliant forward references, which could admittedly be
# disabled by passing "is_forwardref_ignorable=False" to that call.
# disabled by passing "is_forwardref_valid=False" to that call.
# * PEP-noncompliant tuple unions, which currently *CANNOT* be
# disabled by passing such an option to that call.
else:
Expand Down
2 changes: 1 addition & 1 deletion beartype/_check/forward/reference/fwdrefmeta.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ def __type_beartype__(cls: BeartypeForwardRef) -> type: # type: ignore[misc]
# referee that proxy refers to. Although an unlikely edge
# case, unlikely edge cases are like million-to-one chances
# in a Pratchett novel: you just know it's coming up.
is_forwardref_ignorable=False,
is_forwardref_valid=False,
)
# Else, this referee is isinstanceable.
# If doing so raised *ANY* exception whatsoever...
Expand Down
195 changes: 164 additions & 31 deletions beartype/_conf/confcls.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,14 @@

# ....................{ IMPORTS }....................
from beartype.roar._roarwarn import (
_BeartypeConfReduceDecoratorExceptionToWarningDefault,
)
_BeartypeConfReduceDecoratorExceptionToWarningDefault)
from beartype.typing import (
TYPE_CHECKING,
Dict,
Optional,
)
from beartype._conf.confenum import (
BeartypeDecorationPosition,
BeartypeStrategy,
BeartypeViolationVerbosity,
)
Expand Down Expand Up @@ -73,6 +73,17 @@ class BeartypeConf(object):
Attributes
----------
_claw_decoration_position_funcs : BeartypeDecorationPosition
**Import hook callable decorator position** (i.e., location to which the
:func:`beartype.beartype` decorator will be implicitly injected into
existing chains of one or more decorators decorating functions and
methods defined by modules imported under :mod:`beartype.claw` import
hooks).
_claw_decoration_position_types : BeartypeDecorationPosition
**Import hook class decorator position** (i.e., location to which the
:func:`beartype.beartype` decorator will be implicitly injected into
existing chains of one or more decorators decorating classes defined by
modules imported under :mod:`beartype.claw` import hooks).
_claw_is_pep526 : bool
:data:`True` only if type-checking **annotated variable assignments**
(i.e., :pep:`526`-compliant assignments to local, global, class, and
Expand Down Expand Up @@ -143,7 +154,7 @@ class BeartypeConf(object):
member) with which to implement all type-checks in the wrapper function
dynamically generated by the :func:`beartype.beartype` decorator for
the decorated callable.
violation_door_type : TypeException
_violation_door_type : TypeException
**DOOR violation type** (i.e., type of exception raised by the
:func:`beartype.door.die_if_unbearable` type-checker when the object
passed to that type-checker violates the type hint passed to that
Expand Down Expand Up @@ -194,6 +205,8 @@ class BeartypeConf(object):
# cache dunder methods. Slotting has been shown to reduce read and write
# costs by approximately ~10%, which is non-trivial.
__slots__ = (
'_claw_decoration_position_funcs',
'_claw_decoration_position_types',
'_claw_is_pep526',
'_conf_args',
'_conf_kwargs',
Expand All @@ -219,6 +232,8 @@ class BeartypeConf(object):
# Squelch false negatives from mypy. This is absurd. This is mypy. See:
# https://github.com/python/mypy/issues/5941
if TYPE_CHECKING:
_claw_decoration_position_funcs: BeartypeDecorationPosition
_claw_decoration_position_types: BeartypeDecorationPosition
_claw_is_pep526: bool
_conf_args: tuple
_conf_kwargs: DictStrToAny
Expand Down Expand Up @@ -258,6 +273,10 @@ def __new__(

# Uncomment us when implementing O(n) type-checking, please.
# check_time_max_multiplier: Union[int, None] = 1000,
claw_decoration_position_funcs: BeartypeDecorationPosition = (
BeartypeDecorationPosition.LAST),
claw_decoration_position_types: BeartypeDecorationPosition = (
BeartypeDecorationPosition.LAST),
claw_is_pep526: bool = True,
hint_overrides: BeartypeHintOverrides = BEARTYPE_HINT_OVERRIDES_EMPTY,
is_color: BoolTristateUnpassable = ARG_VALUE_UNPASSED,
Expand Down Expand Up @@ -342,29 +361,103 @@ def __new__(
.. code-block:: python
b * check_time_max_multiplier >= T
claw_decoration_position_funcs : BeartypeDecorationPosition, optional
**Import hook callable decorator position** (i.e., location to which
the :func:`beartype.beartype` decorator will be implicitly injected
into existing chains of one or more decorators decorating functions
and methods defined by modules imported under :mod:`beartype.claw`
import hooks). Defaults to :attr:`BeartypeDecorationPosition.LAST`.
Modifying this configures import hooks to inject
:func:`beartype.beartype` as the first (rather than last) decorator
for callables. If your codebase requires this, consider submitting
an issue to the :mod:`beartype` issue tracker. Ideally, the
:func:`beartype.beartype` decorator should be order-invariant with
respect to decorator chaining and thus support decoration in *any*
position – including the default last position: e.g.,
.. code-block:: python
# Registering this import hook...
from beartype import BeartypeConf, BeartypeDecorationPosition
from beartype.claw import beartype_this_package
beartype_this_package(conf=BeartypeConf(
claw_decoration_position_funcs=(
BeartypeDecorationPosition.FIRST)))
# ...transforms chains of function decorators like this...
from functools import cache
@cache
def chad_func() -> int:
return 42
# ...into chains of function decorators like this.
from beartype import beartype
from functools import cache
@cache
@beartype # <-- @beartype decorates first rather than last! \\o/
def chad_func() -> int:
return 42
claw_decoration_position_types : BeartypeDecorationPosition, optional
**Import hook class decorator position** (i.e., location to which
the :func:`beartype.beartype` decorator will be implicitly injected
into existing chains of one or more decorators decorating classes
defined by modules imported under :mod:`beartype.claw` import
hooks). Defaults to :attr:`BeartypeDecorationPosition.LAST`.
Modifying this configures import hooks to inject
:func:`beartype.beartype` as the first (rather than last) decorator
for classes. If your codebase requires this, consider submitting an
issue to the :mod:`beartype` issue tracker. Ideally, the
:func:`beartype.beartype` decorator should be order-invariant with
respect to decorator chaining and thus support decoration in *any*
position – including the default last position: e.g.,
.. code-block:: python
# Registering this import hook...
from beartype import BeartypeConf, BeartypeDecorationPosition
from beartype.claw import beartype_this_package
beartype_this_package(conf=BeartypeConf(
claw_decoration_position_types=(
BeartypeDecorationPosition.FIRST)))
# ...transforms chains of class decorators like this...
from dataclasses import dataclass
@dataclass
class ClassyData(object):
integral_datum: int
# ...into chains of class decorators like this.
from beartype import beartype
from dataclasses import dataclass
@dataclass
@beartype # <-- @beartype decorates first rather than last! \\o/
class ClassyData(object):
integral_datum: int
claw_is_pep526 : bool, optional
:data:`True` only if implicitly type-checking **annotated variable
assignments** (i.e., :pep:`526`-compliant assignments to local,
global, class, and instance variables annotated by type hints) when
importing modules under import hooks published by the
:mod:`beartype.claw` subpackage by injecting calls to the
:func:`beartype.door.die_if_unbearable` function immediately *after*
those assignments in those modules. Enabling this boolean:
global, class, or instance variables annotated by type hints) when
importing modules under :mod:`beartype.claw` import hooks by
injecting calls to the :func:`beartype.door.die_if_unbearable`
function immediately *after* those assignments in those modules.
Enabling this boolean:
* Effectively augments :mod:`beartype` into a full-blown **hybrid
runtime-static type-checker** (i.e., performing both standard
runtime type-checking *and* non-standard static type-checking at
runtime).
* Adds negligible runtime overhead to all annotated variable
* Adds mostly negligible runtime overhead to all annotated variable
assignments in all modules imported under those import hooks.
Although the *individual* cost of this overhead for any given
assignment is negligible, the *aggregate* cost across all such
assignments could be non-negligible in worst-case use cases.
Ideally, this boolean should only be disabled for a small subset of
performance-sensitive modules *after* profiling those modules to
suffer performance regressions under import hooks published by the
:mod:`beartype.claw` subpackage. Defaults to :data:`True`.
suffer performance regressions under :mod:`beartype.claw` import
hooks. Defaults to :data:`True`.
hint_overrides : BeartypeHintOverrides
**Type hint overrides** (i.e., frozen dictionary mapping from
arbitrary source to target type hints), enabling callers to lie to
Expand Down Expand Up @@ -621,6 +714,8 @@ def lies(all_lies: list[int | float]) -> int | float:

# Efficiently hashable tuple of these parameters in arbitrary order.
conf_args = (
claw_decoration_position_funcs,
claw_decoration_position_types,
claw_is_pep526,
hint_overrides,
is_color,
Expand Down Expand Up @@ -649,6 +744,8 @@ def lies(all_lies: list[int | float]) -> int | float:
# defined *AFTER* this method first attempts to efficiently reduce
# to a noop by returning a previously instantiated configuration.
conf_kwargs = dict(
claw_decoration_position_funcs=claw_decoration_position_funcs,
claw_decoration_position_types=claw_decoration_position_types,
claw_is_pep526=claw_is_pep526,
hint_overrides=hint_overrides,
is_color=is_color,
Expand Down Expand Up @@ -718,6 +815,10 @@ def lies(all_lies: list[int | float]) -> int | float:
# parameters from the "conf_kwargs" dictionary possibly modified by
# the above call to the default_conf_kwargs() function rather than
# the original passed values of these parameters.
self._claw_decoration_position_funcs = conf_kwargs[ # pyright: ignore
'claw_decoration_position_funcs']
self._claw_decoration_position_types = conf_kwargs[ # pyright: ignore
'claw_decoration_position_types']
self._claw_is_pep526 = conf_kwargs['claw_is_pep526'] # pyright: ignore
self._hint_overrides = conf_kwargs['hint_overrides'] # pyright: ignore
self._is_color = conf_kwargs['is_color'] # pyright: ignore
Expand Down Expand Up @@ -862,26 +963,6 @@ def warning_cls_on_decorator_exception(self) -> (
return self._warning_cls_on_decorator_exception

# ..................{ PROPERTIES ~ options : bool }..................
# Read-only public properties with which this configuration was originally
# instantiated (as keyword-only parameters).

@property
def claw_is_pep526(self) -> bool:
'''
:data:`True` only if type-checking **annotated variable assignments**
(i.e., :pep:`526`-compliant assignments to local, global, class, and
instance variables annotated by type hints) when importing modules
under import hooks published by the :mod:`beartype.claw` subpackage.
See Also
--------
:meth:`__new__`
Further details.
'''

return self._claw_is_pep526


@property
def is_color(self) -> Optional[bool]:
'''
Expand Down Expand Up @@ -936,6 +1017,58 @@ def is_pep484_tower(self) -> bool:

return self._is_pep484_tower

# ..................{ PROPERTIES ~ options : claw }..................
@property
def claw_decoration_position_funcs(self) -> BeartypeDecorationPosition:
'''
**Import hook callable decorator position** (i.e., location to which the
:func:`beartype.beartype` decorator will be implicitly injected into
existing chains of one or more decorators decorating functions and
methods defined by modules imported under :mod:`beartype.claw` import
hooks).
See Also
--------
:meth:`__new__`
Further details.
'''

return self._claw_decoration_position_funcs


@property
def claw_decoration_position_types(self) -> BeartypeDecorationPosition:
'''
**Import hook class decorator position** (i.e., location to which the
:func:`beartype.beartype` decorator will be implicitly injected into
existing chains of one or more decorators decorating classes defined by
modules imported under :mod:`beartype.claw` import hooks).
See Also
--------
:meth:`__new__`
Further details.
'''

return self._claw_decoration_position_types


@property
def claw_is_pep526(self) -> bool:
'''
:data:`True` only if type-checking **annotated variable assignments**
(i.e., :pep:`526`-compliant assignments to local, global, class, and
instance variables annotated by type hints) when importing modules
under import hooks published by the :mod:`beartype.claw` subpackage.
See Also
--------
:meth:`__new__`
Further details.
'''

return self._claw_is_pep526

# ..................{ PROPERTIES ~ options : violation }..................
@property
def violation_door_type(self) -> TypeException:
Expand Down
Loading

0 comments on commit 72e3eb1

Please sign in to comment.