Skip to content

Commit

Permalink
beartype.claw + methods + PEP 562 x 2.
Browse files Browse the repository at this point in the history
This commit is the (hopefully) last in a commit chain addressing 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(self) -> None:
        # "beartype.claw" now raises an exception on this violation. Yah!
        so_much_local: int = 'You no longer fool @beartype, local."

        # "beartype.claw" also raises an exception on this violation. Go!
        self.so_much_var: int = 'You too are known to @beartype, variable."
```

(*Pentatonic tonic in tectonic techno!*)
  • Loading branch information
leycec committed Oct 20, 2023
1 parent 0a96480 commit 84613f2
Show file tree
Hide file tree
Showing 8 changed files with 108 additions and 37 deletions.
4 changes: 2 additions & 2 deletions beartype/_util/ast/utilastmunge.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ def copy_node_metadata(
f'{repr(node_trg_cur)} not AST node.')

# Copy all source code metadata from this source to target node.
node_trg_cur.lineno = node_src.lineno
node_trg_cur.col_offset = node_src.col_offset
node_trg_cur.lineno = node_src.lineno
node_trg_cur.col_offset = node_src.col_offset
node_trg_cur.end_lineno = node_src.end_lineno # type: ignore[attr-defined]
node_trg_cur.end_col_offset = node_src.end_col_offset # type: ignore[attr-defined]
91 changes: 72 additions & 19 deletions beartype/claw/_ast/clawastmain.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
from ast import (
AST,
AnnAssign,
Attribute,
Call,
ClassDef,
# Constant,
Expand Down Expand Up @@ -579,15 +580,16 @@ def visit_AnnAssign(self, node: AnnAssign) -> NodeVisitResult:
* ``node.target``, a child node describing the target attribute assigned
to by this assignment, guaranteed to be an instance of either:
* :class:`ast.Name`, in which case this assignment is denoted as
"simple" via the ``node.simple`` instance variable. This is the
common case in which the attribute being assigned to is *NOT*
embedded in parentheses and thus denotes a simple attribute name
rather than a full-blown Python expression.
* :class:`ast.Name`, in which case this is a **simple assignment**
(i.e., to a local or global variable). This is the common case in
which the attribute being assigned to is *NOT* embedded in
parentheses and thus denotes a simple attribute name rather than a
full-blown Python expression.
* :class:`ast.Attribute`, in which case this is an **object
assignment** (i.e., to an instance or class variable of an object).
* :class:`ast.Subscript`, in which case this assignment is to the item
subscripted by an index of a container rather than to that container
itself.
* :class:`ast.Attribute`. **WE HAVE NO IDEA.** Look. We just don't.
* ``node.simple``, an integer :superscript:`sigh` that is either:
Expand Down Expand Up @@ -643,6 +645,9 @@ def visit_AnnAssign(self, node: AnnAssign) -> NodeVisitResult:
phenomenal inspiration!
'''

# Recursively transform *ALL* child nodes of this parent callable node.
self.generic_visit(node)

# If either...
if (
# This beartype configuration disables type-checking of PEP
Expand All @@ -651,11 +656,13 @@ def visit_AnnAssign(self, node: AnnAssign) -> NodeVisitResult:
# This beartype configuration enables type-checking of PEP
# 526-compliant annotated variable assignments *BUT*...

#FIXME: Excise us up, please.
#FIXME: Can and/or should we also support "node.target" child nodes
#that are instances of "ast.Attribute" and "ast.Subscript"?
# This assignment is *NOT* simple (in which case this assignment is
# *NOT* assigning to an attribute name) *OR*...
not node.simple or
# # This assignment is *NOT* simple (in which case this assignment is
# # *NOT* assigning to an attribute name) *OR*...
# not node.simple or

# This assignment is simple *BUT*...
#
# This assignment is *NOT* actually an assignment but simply an
Expand Down Expand Up @@ -709,19 +716,60 @@ def visit_AnnAssign(self, node: AnnAssign) -> NodeVisitResult:
# 526-compliant annotated variable assignments.
# * This assignment is simple and assigning to an attribute name.

# Validate this expectation.
assert isinstance(node.target, Name), (
f'Non-simple AST annotated assignment node {repr(node)} '
f'target {repr(node.target)} not {repr(Name)} instance.')

# Child node referencing the function performing this type-checking,
# previously imported at module scope by visit_FunctionDef() above.
node_func_name = Name(
BEARTYPE_RAISER_TARGET_ATTR_NAME, ctx=NODE_CONTEXT_LOAD)
#FIXME: Excise us up, please.
# # Validate this expectation.
# assert isinstance(node.target, Name), (
# f'Non-simple AST annotated assignment node {repr(node)} '
# f'target {repr(node.target)} not {repr(Name)} instance.')

# Child node passing the value newly assigned to this attribute by this
# assignment as the first parameter to die_if_unbearable().
node_func_arg_pith = Name(node.target.id, ctx=NODE_CONTEXT_LOAD)
node_func_arg_pith: AST = None # type: ignore[assignment]

# Child node referencing the target variable being assigned to,
# localized purely as a negligible optimization.
node_target = node.target

# If this target variable is a simple local or global variable...
if isinstance(node_target, Name):
# Child node referencing this local or global variable.
node_func_arg_pith = Name(node_target.id, ctx=NODE_CONTEXT_LOAD)
# Else, this target variable is *NOT* a simple local or global variable.
#
# If this target variable is an instance or class variable...
elif isinstance(node_target, Attribute):
#FIXME: Insufficient. Attributes can contain arbitrary nested child
#nodes, including other attributes and/or names. Thankfully, the
#only reason to even bother attempting to do this is to rigorously
#sanitize line and column numbers -- which doesn't appear to be
#particularly necessary or even desirable for dynamically generated
#code. For now, we simply shallowly reuse the existing "value" node.
# # Child node referencing the object containing this instance or
# # class variable (e.g., the "self" in "self.attr: str = 'Attr!'").
# node_func_arg_pith_obj = Name(
# node_target.value.id, ctx=NODE_CONTEXT_LOAD)
# copy_node_metadata(node_src=node, node_trg=node_func_arg_pith_obj)

# Child node referencing this instance or class variable.
node_func_arg_pith = Attribute(
value=node_target.value,
# Unqualified basename of this instance or class variable.
attr=node_target.attr,
ctx=NODE_CONTEXT_LOAD,
)
# Else, this target variable is *NOT* an instance or class variable. In
# this case, this target variable is currently unsupported by this node
# transformer for automated type-checking. Simply preserve and return
# this node as is.
#
# Examples include:
# * "ast.Subscripted", in which case this target variable is an item of
# a container. It is unclear whether PEP 526 even supports annotated
# variable assignments of container items *OR* whether any @beartype
# users even annotate variable assignments of container items. Ergo,
# this node transformer currently ignores this odd edge case.
else:
return node

# List of all nodes encapsulating keyword arguments passed to
# die_if_unbearable(), defaulting to the empty list and thus *NO* such
Expand All @@ -743,6 +791,11 @@ def visit_AnnAssign(self, node: AnnAssign) -> NodeVisitResult:
# configuration. In this case, avoid passing that configuration to
# the beartype decorator for both efficiency and simplicity.

# Child node referencing the function performing this type-checking,
# previously imported at module scope by visit_FunctionDef() above.
node_func_name = Name(
BEARTYPE_RAISER_TARGET_ATTR_NAME, ctx=NODE_CONTEXT_LOAD)

# Child node type-checking this newly assigned attribute against the
# type hint annotating this assignment via our die_if_unbearable().
node_func_call = Call(
Expand Down
2 changes: 1 addition & 1 deletion beartype/claw/_importlib/clawimpcache.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class ModuleNameToBeartypeConf(Dict[str, 'BeartypeConf']):
.. code-block:: python
# Otherwise syntactically and semantically correct PEP 562-compliant
# Otherwise syntactically and semantically correct PEP 526-compliant
# annotated assignment expressions like this previously raised spurious
# non-human-readable exceptions from this dictionary resembling:
# KeyError: 'muh_module' # <-- what does this even mean!?!?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,11 @@
# ....................{ PEP 526 }....................
# Validate that the beartype_this_package() import hook installed by the parent
# "beartype_this_package.__init__" submodule implicitly appends all PEP
# 562-compliant annotated assignment statements in other submodules of this
# 526-compliant annotated assignment statements in other submodules of this
# "beartype_this_package" subpackage with calls to beartype's statement-level
# beartype.door.die_if_unbearable() exception-raiser.

# Assert that a PEP 562-compliant assignment statement assigning an object
# Assert that a PEP 526-compliant assignment statement assigning an object
# satisfying the type hint annotating that statement raises *NO* exception.
#
# Note that this type hint is intentionally annotated as "float" rather than
Expand All @@ -60,7 +60,7 @@
# "is_pep484_tower=True").
loves_philosophy: float = len('The fountains mingle with the river')

# Assert that a PEP 562-compliant assignment statement assigning an object
# Assert that a PEP 526-compliant assignment statement assigning an object
# violating the type hint annotating that statement raises the expected
# exception.
with raises(BeartypeDoorHintViolation):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,33 @@ 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.
# ....................{ PASS }....................
# Implicitly assert that assigning a valid value to an annotated local
# or global variable in a method raises *NO* exception.
#
# Note that this edge case is distinct from assigning to an annotated
# instance or class variable in a method.
his_wandering_step: int = len('More graceful than her own')

# Assert that assigning an annotated local variable an invalid value
# raises the expected exception.
# Implicitly assert that assigning a valid value to an annotated
# instance or class variable in a method raises *NO* exception.
#
# Note that this edge case is distinct from assigning to an annotated
# local or global variable in a method.
self.the_awful_ruins: bytes = b'The awful ruins of the days of old:'

# ....................{ FAIL }....................
# Explicitly assert that assigning an invalid value to an annotated
# local or global variable raises the expected exception.
with raises(BeartypeDoorHintViolation):
his_wandering_step: int = 'Obedient to high thoughts, has visited'

# Explicitly assert that assigning an invalid value to an annotated
# instance or class variable raises the expected exception.
with raises(BeartypeDoorHintViolation):
self.the_awful_ruins: bytes = (
'Athens, and Tyre, and Balbec, and the waste')

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,20 @@

# ....................{ PEP 526 }....................
# Validate that the import hook presumably installed by the caller implicitly
# appends all PEP 562-compliant annotated assignment statements in this
# appends 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 562-compliant annotated assignment statement assigning an
# Assert that a PEP 526-compliant annotated assignment statement assigning an
# object satisfying the type hint annotating that statement raises *NO*
# exception.
and_winter_robing: int = len('And winter robing with pure snow and crowns')

# Assert that a PEP 562-compliant annotated statement lacking an assignment
# Assert that a PEP 526-compliant annotated statement lacking an assignment
# raises *NO* exception.
such_magic_as_compels_the_charmed_night: str

# Assert that a PEP 562-compliant annotated assignment statement assigning an
# Assert that a PEP 526-compliant annotated assignment statement assigning an
# object violating the type hint annotating that statement raises the expected
# exception.
with raises(BeartypeDoorHintViolation):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@

# ....................{ PEP 526 }....................
# Validate that *NO* import hooks installed by the caller apply to this
# submodule. In this case, assert that PEP 562-compliant annotated assignment
# submodule. In this case, assert that PEP 526-compliant annotated assignment
# statements are *NOT* appended with calls to beartype's statement-level
# beartype.door.die_if_unbearable() exception-raiser.

# Assert that a PEP 562-compliant assignment statement assigning an object
# Assert that a PEP 526-compliant assignment statement assigning an object
# violating the type hint annotating that statement raises *NO* exception.
and_winter_robing: str = b'And winter robing with pure snow and crowns'
assert isinstance(and_winter_robing, bytes)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@

# ....................{ PEP 526 }....................
# Validate that *NO* import hooks installed by the caller apply to this
# submodule. In this case, assert that PEP 562-compliant annotated assignment
# submodule. In this case, assert that PEP 526-compliant annotated assignment
# statements are *NOT* appended with calls to beartype's statement-level
# beartype.door.die_if_unbearable() exception-raiser.

# Assert that a PEP 562-compliant assignment statement assigning an object
# Assert that a PEP 526-compliant assignment statement assigning an object
# violating the type hint annotating that statement raises *NO* exception.
and_winter_robing: str = b'And winter robing with pure snow and crowns'
assert isinstance(and_winter_robing, bytes)
Expand Down

0 comments on commit 84613f2

Please sign in to comment.