Skip to content

Commit

Permalink
beartype.claw + methods + PEP 562.
Browse files Browse the repository at this point in the history
This commit resolves a critical oversight with respect to
`beartype.claw`-fueled beartype import hooks, resolving issue #293
kindly submitted by Ontarian AI superstar MaximilienLC (Maximilien Le
Cleï). Previously, `beartype.claw` failed to type-check PEP
562-compliant annotated variable assignments in methods. Now,
`beartype.claw` does so: e.g.,

```python
class SoMuchClass(object):
    def so_much_method() -> None:
        # "beartype.claw" now raises an exception on this violation. Yah!
        so_much_variable: int = 'You no longer fool @beartype, variable."
```

(*Impressive impression in a massive session!*)
  • Loading branch information
leycec committed Oct 19, 2023
1 parent dbdb858 commit 0a96480
Show file tree
Hide file tree
Showing 5 changed files with 47 additions and 27 deletions.
47 changes: 27 additions & 20 deletions beartype/claw/_ast/clawastmain.py
Original file line number Diff line number Diff line change
Expand Up @@ -519,26 +519,33 @@ def visit_FunctionDef(self, node: NodeCallable) -> Optional[NodeCallable]:
This same callable node.
'''

# If this callable node has one or more parent nodes previously visited
# by this node transformer *AND* the immediate parent node of this
# callable node is a class node, then this callable node encapsulates a
# method rather than a function. In this case, the visit_ClassDef()
# method defined above has already explicitly decorated the class
# defining this method by the @beartype decorator, which then implicitly
# decorates both this and all other methods of that class by that
# decorator. For safety and efficiency, avoid needlessly re-decorating
# this method by the same decorator by simply preserving and returning
# this node as is.
if self._is_node_parent_class:
return node
# Else, this callable node is either the root node of the current AST
# *OR* has a parent node that is not a class node. In either case, this
# callable node necessarily encapsulates a function (rather than a
# method), which yet to be decorated. Do so now! So say we all.
#
# If the currently visited callable is annotated by one or more type
# hints and thus *NOT* ignorable with respect to beartype decoration...
elif is_node_callable_typed(node):
# If...
if (
# * This callable node has one or more parent nodes previously
# visited by this node transformer *AND* the immediate parent node
# of this callable node is a class node, then this callable node
# encapsulates a method rather than a function. In this case, the
# visit_ClassDef() method defined above has already explicitly
# decorated the class defining this method by the @beartype
# decorator, which then implicitly decorates both this and all
# other methods of that class by that decorator. For safety and
# efficiency, avoid needlessly re-decorating this method by the
# same decorator by preserving and returning this node as is.
# * That is *NOT* the case, then this callable node is either the
# root node of the current AST *OR* has a parent node that is not
# a class node. In either case, this callable node necessarily
# encapsulates a function (rather than a method), which yet to be
# decorated. Do so now! So say we all.
#
# This logic corresponds to the above "That is *NOT* the case" case
# (i.e., this callable node necessarily encapsulates a function).
# Look. Just accept that we have a tenuous grasp on reality at best.
not self._is_node_parent_class and
# ...and the currently visited callable is annotated by one or more
# type hints and thus *NOT* ignorable with respect to beartype
# decoration...
is_node_callable_typed(node)
):
# Add a new child decoration node to this parent callable node
# decorating this callable by @beartype under this configuration.
decorate_node(node=node, conf=self._conf_beartype)
Expand Down
4 changes: 2 additions & 2 deletions beartype/typing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,8 +250,8 @@
# If the active Python interpreter targets Python >= 3.12...
if _IS_PYTHON_AT_LEAST_3_12:
from typing import ( # type: ignore[attr-defined]
TypeAliasType as TypeAliasType,
override as override,
TypeAliasType as TypeAliasType, # pyright: ignore[reportGeneralTypeIssues]
override as override, # pyright: ignore[reportGeneralTypeIssues]
)

# ....................{ PEP ~ 544 }....................
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
# See "LICENSE" for further details.

'''
Project-wide **beartype import hookable class submodule** (i.e., data
module containing *only* annotated classes, mimicking real-world usage of the
Project-wide **beartype import hookable class submodule** (i.e., data module
containing *only* annotated classes, mimicking real-world usage of the
:func:`beartype.claw.beartype_package` import hook from an external caller).
'''

Expand All @@ -14,7 +14,10 @@
BeartypeConf,
beartype,
)
from beartype.roar import BeartypeCallHintParamViolation
from beartype.roar import (
BeartypeCallHintParamViolation,
BeartypeDoorHintViolation,
)
from beartype.typing import (
List,
Union,
Expand Down Expand Up @@ -50,6 +53,15 @@ def if_no_bright_bird(
method violates the type hints annotating this method.
'''

# Assert that assigning an annotated local variable a valid value
# raises *NO* exception.
his_wandering_step: int = len('More graceful than her own')

# Assert that assigning an annotated local variable an invalid value
# raises the expected exception.
with raises(BeartypeDoorHintViolation):
his_wandering_step: int = 'Obedient to high thoughts, has visited'

# Look, @beartype. Just do it!
return insect_or_gentle_beast

Expand Down
1 change: 1 addition & 0 deletions pyright
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ pushd "${script_dirname}" >/dev/null
# Statically type-check this project's codebase with all passed arguments.
command pyright beartype "${@}"
# command pyright --pythonversion 3.8 beartype "${@}"
# command pyright --pythonversion 3.11 beartype "${@}"

# 0-based exit code reported by the prior command.
exit_code=$?
Expand Down
4 changes: 2 additions & 2 deletions pytest
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ set -e
# * "-X dev", enabling the Python Development Mode (PDM). See also commentary
# for the ${PYTHONDEVMODE} shell variable in the "tox.ini" file.
# PYTHON_ARGS=( command python3 -X dev )
PYTHON_ARGS=( command python3.8 -X dev )
# PYTHON_ARGS=( command python3.8 -X dev )
# PYTHON_ARGS=( command python3.9 -X dev )
# PYTHON_ARGS=( command python3.10 -X dev )
# PYTHON_ARGS=( command python3.11 -X dev )
PYTHON_ARGS=( command python3.11 -X dev )
# PYTHON_ARGS=( command python3.12 -X dev )
# PYTHON_ARGS=( command pypy3.7 -X dev )

Expand Down

0 comments on commit 0a96480

Please sign in to comment.