Skip to content

Commit

Permalink
Granular PEP 526 messages x 2.
Browse files Browse the repository at this point in the history
This commit is the next in a commit chain improving the granularity of
both exception and warning messages emitted for **PEP 526-compliant
annotated variable assignments** (e.g., `muh_var: int | str = True`).
Previously, @beartype provided *no* contextual metadata describing these
assignments in these messages. Once this commit chain is complete,
@beartype will prefix these messages with a substring describing the
fully-qualified class, callable, and/or module directly performing these
assignments. Specifically, this commit refactors the abstract syntax
tree (AST) transformation applied by beartype import hooks to reliably
construct and pass this prefix to our
`beartype.door.die_if_unbearable()` type-checker. (*Sassy seasoning!*)
  • Loading branch information
leycec committed Mar 1, 2024
1 parent c92d5bb commit a9c05e2
Show file tree
Hide file tree
Showing 11 changed files with 333 additions and 74 deletions.
2 changes: 1 addition & 1 deletion beartype/_check/checkmagic.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
'''

# ....................{ NAMES ~ func }....................
FUNC_CHECKER_NAME_PREFIX = f'{NAME_PREFIX}tester_'
FUNC_CHECKER_NAME_PREFIX = f'{NAME_PREFIX}checker_'
'''
Substring prefixing the unqualified basenames of all type-checking raiser and
tester functions created by the
Expand Down
34 changes: 28 additions & 6 deletions beartype/_check/checkmake.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,11 @@ def make_func_tester(
# ....................{ FACTORIES ~ code }....................
#FIXME: Unit test us up, please.
@callable_cached
def make_code_tester_check(hint: object, conf: BeartypeConf) -> CodeGenerated:
def make_code_tester_check(
hint: object,
conf: BeartypeConf,
exception_prefix : str,
) -> CodeGenerated:
'''
Pure-Python code snippet of a type-checking tester function type-checking an
arbitrary object against the passed type hint under the passed beartype
Expand All @@ -200,6 +204,9 @@ def make_code_tester_check(hint: object, conf: BeartypeConf) -> CodeGenerated:
conf : BeartypeConf
**Beartype configuration** (i.e., self-caching dataclass encapsulating
all settings configuring type-checking for the passed object).
exception_prefix : str
Human-readable label prefixing the representation of this object in the
exception message.
Returns
-------
Expand Down Expand Up @@ -411,7 +418,10 @@ def make_code_raiser_func_pep484_noreturn_check(
#FIXME: Unit test us up, please.
@callable_cached
def make_code_raiser_hint_object_check(
hint: object, conf: BeartypeConf) -> CodeGenerated:
hint: object,
conf: BeartypeConf,
exception_prefix: str,
) -> CodeGenerated:
'''
Pure-Python code snippet of a type-checking raiser function type-checking an
arbitrary object against the passed type hint under the passed beartype
Expand All @@ -427,6 +437,9 @@ def make_code_raiser_hint_object_check(
conf : BeartypeConf
**Beartype configuration** (i.e., self-caching dataclass encapsulating
all settings configuring type-checking for the passed object).
exception_prefix : str
Human-readable label prefixing the representation of this object in the
exception message.
Returns
-------
Expand Down Expand Up @@ -462,6 +475,15 @@ def make_code_raiser_hint_object_check(
''
)

#FIXME: Refactor as follows, please:
#* Define a new "ARG_NAME_EXCEPTION_PREFIX" global in "checkmagic".
#* Import that above.
#* Assign here:
# func_scope[ARG_NAME_EXCEPTION_PREFIX] = exception_prefix
#* Refactor the "CODE_GET_HINT_OBJECT_VIOLATION" global to additionally pass
# the following keyword parameter:
# exception_prefix={ARG_NAME_EXCEPTION_PREFIX},

# Pass hidden parameters to this raiser function exposing:
# * The get_hint_object_violation() getter called by the
# "CODE_GET_HINT_OBJECT_VIOLATION" snippet.
Expand Down Expand Up @@ -500,17 +522,17 @@ def make_code_raiser_hint_object_check(
'''
**Type-checking function name uniquifier** (i.e., iterator yielding the next
integer incrementation starting at 0, leveraged by the
:func:`_make_func_checker` factory to uniquify the names of the type-checking
:func:`._make_func_checker` factory to uniquify the names of the type-checking
functions dynamically generated by that factory).
'''

# ....................{ PRIVATE ~ testers }....................
def _func_checker_ignorable(obj: object) -> bool:
'''
**Ignorable type-checking tester function singleton** (i.e., function
unconditionally returning ``True``, semantically equivalent to a tester
unconditionally returning :data:`True`, semantically equivalent to a tester
testing whether an arbitrary object passed to this tester satisfies an
ignorable PEP-compliant type hint).
ignorable type hint).
The :func:`make_func_tester` factory efficiently returns this singleton when
passed an ignorable type hint rather than inefficiently regenerating a
Expand Down Expand Up @@ -623,7 +645,7 @@ def _make_func_checker(
code_check,
func_scope,
hint_refs_type_basename,
) = make_code_check(hint, conf)
) = make_code_check(hint, conf, exception_prefix)

# If this hint contains one or more relative forward references,
# this hint is non-portable across lexical scopes. In this case,
Expand Down
6 changes: 6 additions & 0 deletions beartype/_check/error/errorget.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,12 @@ class variable or method annotated by this hint *or* :data:`None`).
# Substring prefixing the message of the violation to be raised below.
exception_prefix: str = None # type: ignore[assignment]

#FIXME: Refactor this function to accept a new "exception_prefix" parameter.
#FIXME: Refactor the first "if" branch below to instead resemble:
#else:
# exception_cls = conf.violation_door_type
# exception_prefix = f'{exception_prefix}value '

# If the passed object is neither a parameter or return of a decorated
# callable, this object was directly passed to either the
# beartype.door.is_bearable() or beartype.door.die_if_unbearable()
Expand Down
116 changes: 104 additions & 12 deletions beartype/_util/ast/utilastmake.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
alias,
keyword,
)
from beartype.roar import BeartypeClawImportAstException
from beartype.typing import (
List,
Optional,
Expand Down Expand Up @@ -115,38 +116,97 @@ def make_node_importfrom(

# ....................{ FACTORIES ~ attribute }....................
#FIXME: Unit test us up, please.
def make_node_attribute_load(
def make_node_object_attr_load(
# Mandatory parameters.
node_name_load: AST,
attr_name: str,
node_sibling: AST,

# Optional parameters.
node_obj: Optional[AST] = None,
obj_name: Optional[str] = None,
) -> Attribute:
'''
Create and return a new **object attribute access abstract syntax tree (AST)
node** (i.e., node encapsulating an access of an object attribute) of the
passed object with the passed attribute name.
Note that exactly one of the ``node_obj`` and ``obj_name`` parameters *must*
be passed. If neither or both of these parameters are passed, an exception
is raised.
Parameters
----------
node_name_load : AST
Name node accessing the parent object to access this attribute from.
attr_name : str
Unqualified basename of the attribute of this object to be accessed.
node_sibling : AST
Sibling node to copy source code metadata from.
node_obj : Optional[AST]
Either:
* If the caller prefers supplying the name node accessing the parent
object to load this attribute from, that node.
* Else, :data:`None`. In this case, the caller *must* pass the
``obj_name`` parameter.
Defaults to :data:`None`.
obj_name : Optional[str]
Either:
* If the caller prefers supplying the unqualified basename of the parent
object to load this attribute from in the current lexical scope,
that basename.
* Else, :data:`None`. In this case, the caller *must* pass the
``node_obj`` parameter.
Defaults to :data:`None`.
Returns
-------
Attribute
Object attribute node accessing this attribute of this object.
Raises
------
BeartypeClawImportAstException
If either:
* Neither the ``node_obj`` nor ``obj_name`` parameters are passed.
* Both of the ``node_obj`` and ``obj_name`` parameters are passed.
'''
assert isinstance(node_name_load, AST), (
f'{repr(node_name_load)} not AST node.')
assert isinstance(attr_name, str), f'{repr(attr_name)} not string.'

# If the caller passed *NO* name node accessing the parent object to load
# this attribute from...
if not node_obj:
# If the caller also passed *NO* unqualified basename of that object,
# raise an exception.
if not obj_name:
raise BeartypeClawImportAstException(
f'Attribute "{attr_name}" parent object undefined '
f'(i.e., neither "node_obj" nor "obj_name" parameters passed).'
)
# Else, the caller also passed the unqualified basename of that object.

# Child node accessing that object with this basename.
node_obj = make_node_name_load(name=obj_name, node_sibling=node_sibling)
# Else, the caller passed a name node accessing that object.
#
# If the caller also passed the unqualified basename of that object, raise
# an exception.
elif obj_name:
raise BeartypeClawImportAstException(
f'Attribute "{attr_name}" parent object overly defined '
f'(i.e., both "node_obj" and "obj_name" parameters passed).'
)
# Else, the caller passed *NO* unqualified basename of that object.
#
# In any case, the "node_obj" variable is now the desired object node.
assert isinstance(node_obj, AST), (
f'{repr(node_obj)} not AST node.')

# Object attribute node accessing this attribute of this object.
node_attribute_load = Attribute(
value=node_name_load, attr=attr_name, ctx=NODE_CONTEXT_LOAD)
value=node_obj, attr=attr_name, ctx=NODE_CONTEXT_LOAD)

# Copy source code metadata from this sibling node onto this new node.
copy_node_metadata(node_src=node_sibling, node_trg=node_attribute_load)
Expand All @@ -156,11 +216,7 @@ def make_node_attribute_load(

# ....................{ FACTORIES ~ call }....................
#FIXME: Unit test us up, please.
def make_node_call_expr(
*args,
node_sibling: AST,
**kwargs
) -> Expr:
def make_node_call_expr(*args, node_sibling: AST, **kwargs) -> Expr:
'''
Create and return a new **callable call expression abstract syntax tree
(AST) node** (i.e., node encapsulating a Python expression expressing a call
Expand Down Expand Up @@ -256,6 +312,42 @@ def make_node_call(
# Return this call node.
return node_func_call

# ....................{ FACTORIES ~ call : arg }....................
#FIXME: Unit test us up, please.
def make_node_kwarg(
kwarg_name: str, kwarg_value: AST, node_sibling: AST) -> keyword:
'''
Create and return a new **keyword argument abstract syntax tree (AST) node**
(i.e., node encapsulating a keyword argument of a call to an arbitrary
function or method) passing the keyword argument with the passed name and
value to some parent node encapsulating a call to some function or method.
Parameters
----------
kwarg_name : str
Name of this keyword argument.
kwarg_value : AST
Node passing the value of this keyword argument.
node_sibling : AST
Sibling node to copy source code metadata from.
Returns
-------
keyword
Keyword node passing a keyword argument with this name and value.
'''
assert isinstance(kwarg_name, str), f'{repr(kwarg_name)} not string.'
assert isinstance(kwarg_value, AST), f'{repr(kwarg_value)} not AST node.'

# Child node encapsulating this keyword argument.
node_kwarg = keyword(arg=kwarg_name, value=kwarg_value)

# Copy source code metadata from this sibling node onto this new node.
copy_node_metadata(node_src=node_sibling, node_trg=node_kwarg)

# Return this expression node.
return node_kwarg

# ....................{ FACTORIES ~ literal : string }....................
#FIXME: Unit test us up, please.
def make_node_str(text: str, node_sibling: AST) -> Constant:
Expand Down
24 changes: 11 additions & 13 deletions beartype/_util/ast/utilastmunge.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@
# ....................{ COPIERS }....................
#FIXME: Unit test us up, please.
def copy_node_metadata(
node_src: AST,
node_trg: Union[AST, Iterable[AST]],
) -> None:
node_src: AST, node_trg: Union[AST, Iterable[AST]]) -> None:
'''
Copy all **source code metadata** (i.e., beginning and ending line and
column numbers) from the passed source abstract syntax tree (AST) node onto
Expand All @@ -37,13 +35,13 @@ def copy_node_metadata(
The tradeoffs are as follows:
* :func:`ast.fix_missing_locations` is ``O(n)`` time complexity for ``n``
the number of AST nodes across the entire AST tree, but requires only a
single trivial call and is thus considerably more "plug-and-play" than
this function.
* This function is ``O(1)`` time complexity irrespective of the size of the
AST tree, but requires one still mostly trivial call for each synthetic
AST node inserted into the AST tree by the
* :func:`ast.fix_missing_locations` is :math:`O(n)` time complexity for
:math:`n` the number of AST nodes across the entire AST tree, but requires
only a single trivial call and is thus considerably more "plug-and-play"
than this function.
* This function is :math:`O(1)` time complexity irrespective of the size of
the AST tree, but requires one still mostly trivial call for each
synthetic AST node inserted into the AST tree by the
:class:`BeartypeNodeTransformer` above.
Caveats
Expand Down Expand Up @@ -86,9 +84,9 @@ def copy_node_metadata(
See Also
--------
:func:`ast.copy_location`
Less efficient analogue of this function running in ``O(k)`` time
complexity for ``k`` the number of types of source code metadata.
Typically, ``k == 4``.
Less efficient analogue of this function running in :math:`O(k)` time
complexity for :math:`k` the number of types of source code metadata.
Typically, :math:`k == 4`.
'''
assert isinstance(node_src, AST), f'{repr(node_src)} not AST node.'

Expand Down
32 changes: 11 additions & 21 deletions beartype/claw/_ast/_clawastutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@
BeartypeConf,
)
from beartype._util.ast.utilastmake import (
make_node_kwarg,
make_node_name_load,
make_node_object_attr_load,
make_node_str,
)
from beartype._util.ast.utilastmunge import copy_node_metadata
Expand Down Expand Up @@ -159,26 +161,21 @@ def _make_node_keyword_conf_beartype(self, node_sibling: AST) -> keyword:
Keyword node passing this configuration to an arbitrary function.
'''

# Node encapsulating a reference to the beartype import hook state
# global.
node_claw_state = make_node_name_load(
name=BEARTYPE_CLAW_STATE_OBJ_NAME, node_sibling=node_sibling)

# Node encapsulating the fully-qualified name of the current module.
node_module_name = make_node_str(
text=self._module_name_beartype, node_sibling=node_sibling) # type: ignore[attr-defined]

# Node encapsulating a reference to the beartype configuration object
# cache (i.e., dictionary mapping from fully-qualified module names to
# the beartype configurations associated with those modules).
node_module_name_to_conf = Attribute(
value=node_claw_state,
attr=BEARTYPE_CLAW_STATE_CONF_CACHE_VAR_NAME,
ctx=NODE_CONTEXT_LOAD,
node_module_name_to_conf = make_node_object_attr_load(
obj_name=BEARTYPE_CLAW_STATE_OBJ_NAME,
attr_name='module_name_to_beartype_conf',
node_sibling=node_sibling,
)

# Node encapsulating the indexation of a dictionary by the
# fully-qualified name of that module of the currently visited module.
# fully-qualified name of the current module.
node_module_name_index: AST = None # type: ignore[assignment]

# If the active Python interpreter targets Python >= 3.9...
Expand Down Expand Up @@ -211,22 +208,15 @@ def _make_node_keyword_conf_beartype(self, node_sibling: AST) -> keyword:
ctx=NODE_CONTEXT_LOAD,
)

# Node encapsulating the passing of this beartype configuration by the
# Node encapsulating the passing of this beartype configuration as the
# "conf" keyword argument to an arbitrary function call of some suitable
# "beartype" function orchestrated by the caller.
node_keyword_conf = keyword(arg='conf', value=node_conf)
node_keyword_conf = make_node_kwarg(
kwarg_name='conf', kwarg_value=node_conf, node_sibling=node_sibling)

# Copy all source code metadata (e.g., line numbers) from this sibling
# node onto these new nodes.
copy_node_metadata(
node_src=node_sibling,
node_trg=(
node_module_name,
node_module_name_to_conf,
node_conf,
node_keyword_conf,
)
)
copy_node_metadata(node_src=node_sibling, node_trg=node_conf)

# Return this "conf" keyword node.
return node_keyword_conf
Expand Down

0 comments on commit a9c05e2

Please sign in to comment.