-
-
Notifications
You must be signed in to change notification settings - Fork 50
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Parameter default type-checking x 1.
This commit is the first in a commit chain type-checking the default values of optional parameters accepted by `@beartype`-decorated callables, en-route to resolving feature request #154 kindly submitted by @dosisod (Logan Hunt) a literal lifetime ago when @leycec still had hair. And what kind of an unabashedly superheroic name is "Logan Hunt," anyway? He was destined for greatness. Specifically, this commit fully implements support for type-checking default values. Since we have yet to fully test this, further commits technically remain. Technically. (*Veritably, remnants of a venerable revenant!*)
- Loading branch information
Showing
8 changed files
with
777 additions
and
739 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,245 @@ | ||
#!/usr/bin/env python3 | ||
# --------------------( LICENSE )-------------------- | ||
# Copyright (c) 2014-2024 Beartype authors. | ||
# See "LICENSE" for further details. | ||
|
||
''' | ||
**Beartype decorator return code generator** (i.e., low-level callables | ||
dynamically generating Python expressions type-checking the annotated return of | ||
the callable currently being decorated by the :func:`beartype.beartype` | ||
decorator in a general-purpose manner). | ||
This private submodule is *not* intended for importation by downstream callers. | ||
''' | ||
|
||
# ....................{ IMPORTS }.................... | ||
from beartype.typing import NoReturn | ||
from beartype._check.checkcall import BeartypeCall | ||
from beartype._check.checkmake import ( | ||
make_code_raiser_func_pith_check, | ||
make_code_raiser_func_pep484_noreturn_check, | ||
) | ||
from beartype._check.convert.convsanify import sanify_hint_root_func | ||
from beartype._data.func.datafuncarg import ( | ||
ARG_NAME_RETURN, | ||
ARG_NAME_RETURN_REPR, | ||
) | ||
from beartype._data.hint.datahinttyping import LexicalScope | ||
from beartype._decor.wrap.wrapsnip import ( | ||
CODE_RETURN_CHECK_PREFIX, | ||
CODE_RETURN_CHECK_SUFFIX, | ||
PEP484_CODE_CHECK_NORETURN, | ||
) | ||
from beartype._decor.wrap._wraputil import unmemoize_func_wrapper_code | ||
from beartype._util.error.utilerrraise import reraise_exception_placeholder | ||
from beartype._util.error.utilerrwarn import reissue_warnings_placeholder | ||
from beartype._util.hint.utilhinttest import ( | ||
is_hint_ignorable, | ||
is_hint_needs_cls_stack, | ||
) | ||
from beartype._util.kind.map.utilmapset import update_mapping | ||
from beartype._util.text.utiltextprefix import prefix_callable_return | ||
from beartype._util.utilobject import SENTINEL | ||
from warnings import catch_warnings | ||
|
||
# ....................{ CODERS }.................... | ||
def code_check_return(bear_call: BeartypeCall) -> str: | ||
''' | ||
Generate a Python code snippet type-checking the annotated return declared | ||
by the decorated callable if any *or* the empty string otherwise (i.e., if | ||
this return is unannotated). | ||
Parameters | ||
---------- | ||
bear_call : BeartypeCall | ||
Decorated callable to be type-checked. | ||
Returns | ||
------- | ||
str | ||
Code type-checking any annotated return of the decorated callable. | ||
Raises | ||
------ | ||
BeartypeDecorHintPep484585Exception | ||
If this callable is either: | ||
* A coroutine *not* annotated by a :obj:`typing.Coroutine` type hint. | ||
* A generator *not* annotated by a :obj:`typing.Generator` type hint. | ||
* An asynchronous generator *not* annotated by a | ||
:obj:`typing.AsyncGenerator` type hint. | ||
BeartypeDecorHintNonpepException | ||
If the type hint annotating this return (if any) of this callable is | ||
neither: | ||
* **PEP-compliant** (i.e., :mod:`beartype`-agnostic hint compliant with | ||
annotation-centric PEPs). | ||
* **PEP-noncompliant** (i.e., :mod:`beartype`-specific type hint *not* | ||
compliant with annotation-centric PEPs)). | ||
''' | ||
assert bear_call.__class__ is BeartypeCall, ( | ||
f'{repr(bear_call)} not @beartype call.') | ||
|
||
# Type hint annotating this callable's return if any *OR* "SENTINEL" | ||
# otherwise (i.e., if this return is unannotated). | ||
# | ||
# Note that "None" is a semantically meaningful PEP 484-compliant type hint | ||
# equivalent to "type(None)". Ergo, we *MUST* explicitly distinguish | ||
# between that type hint and an unannotated return. | ||
hint = bear_call.func_arg_name_to_hint_get(ARG_NAME_RETURN, SENTINEL) | ||
|
||
# If this return is unannotated, silently reduce to a noop. | ||
if hint is SENTINEL: | ||
return '' | ||
# Else, this return is annotated. | ||
|
||
# Python code snippet to be returned, defaulting to the empty string | ||
# implying this callable's return to either be unannotated *OR* annotated by | ||
# a safely ignorable type hint. | ||
func_wrapper_code = '' | ||
|
||
# Lexical scope (i.e., dictionary mapping from the relative unqualified name | ||
# to value of each locally or globally scoped attribute accessible to a | ||
# callable or class), initialized to "None" for safety. | ||
func_scope: LexicalScope = None # type: ignore[assignment] | ||
|
||
# Attempt to... | ||
try: | ||
# With a context manager "catching" *ALL* non-fatal warnings emitted | ||
# during this logic for subsequent "playrback" below... | ||
with catch_warnings(record=True) as warnings_issued: | ||
# Preserve the original unsanitized type hint for subsequent | ||
# reference *BEFORE* sanitizing this type hint. | ||
hint_insane = hint | ||
|
||
# Sanitize this hint to either: | ||
# * If this hint is PEP-noncompliant, the PEP-compliant type hint | ||
# converted from this PEP-noncompliant type hint. | ||
# * If this hint is PEP-compliant and supported, this hint as is. | ||
# * Else, raise an exception. | ||
# | ||
# Do this first *BEFORE* passing this hint to any further callables. | ||
hint = sanify_hint_root_func( | ||
hint=hint, pith_name=ARG_NAME_RETURN, bear_call=bear_call) | ||
|
||
# If this is the PEP 484-compliant "typing.NoReturn" type hint | ||
# permitted *ONLY* as a return annotation... | ||
if hint is NoReturn: | ||
# Pre-generated code snippet validating this callable to *NEVER* | ||
# successfully return by unconditionally generating a violation. | ||
code_noreturn_check = PEP484_CODE_CHECK_NORETURN.format( | ||
func_call_prefix=bear_call.func_wrapper_code_call_prefix) | ||
|
||
# Code snippet handling the previously generated violation by | ||
# either raising that violation as a fatal exception or emitting | ||
# that violation as a non-fatal warning. | ||
( | ||
code_noreturn_violation, | ||
func_scope, | ||
_ | ||
) = make_code_raiser_func_pep484_noreturn_check(bear_call.conf) | ||
|
||
# Full code snippet to be returned. | ||
func_wrapper_code = ( | ||
f'{code_noreturn_check}{code_noreturn_violation}') | ||
# Else, this is *NOT* "typing.NoReturn". In this case... | ||
else: | ||
# If this PEP-compliant hint is unignorable, generate and return | ||
# a snippet type-checking this return against this hint. | ||
if not is_hint_ignorable(hint): | ||
# Type stack if required by this hint *OR* "None" otherwise. | ||
# See is_hint_needs_cls_stack() for details. | ||
# | ||
# Note that the original unsanitized "hint_insane" (e.g., | ||
# "typing.Self") rather than the new sanitized "hint" (e.g., | ||
# the class currently being decorated by @beartype) is | ||
# passed to that tester. See _code_check_args() for details. | ||
cls_stack = ( | ||
bear_call.cls_stack | ||
if is_hint_needs_cls_stack(hint_insane) else | ||
None | ||
) | ||
# print(f'return hint {repr(hint)} cls_stack: {repr(cls_stack)}') | ||
|
||
# Empty tuple, passed below to satisfy the | ||
# _unmemoize_func_wrapper_code() API. | ||
hint_refs_type_basename = () | ||
|
||
# Code snippet type-checking any arbitrary return. | ||
( | ||
code_return_check_pith, | ||
func_scope, | ||
hint_refs_type_basename, | ||
) = make_code_raiser_func_pith_check( # type: ignore[assignment] | ||
hint, | ||
bear_call.conf, | ||
cls_stack, | ||
False, # <-- True only for parameters | ||
) | ||
|
||
# Unmemoize this snippet against this return. | ||
code_return_check = unmemoize_func_wrapper_code( | ||
bear_call=bear_call, | ||
func_wrapper_code=code_return_check_pith, | ||
pith_repr=ARG_NAME_RETURN_REPR, | ||
hint_refs_type_basename=hint_refs_type_basename, | ||
) | ||
|
||
#FIXME: [SPEED] Optimize the following two string munging | ||
#operations into a single string-munging operation resembling: | ||
# func_wrapper_code = CODE_RETURN_CHECK.format( | ||
# func_call_prefix=bear_call.func_wrapper_code_call_prefix, | ||
# check_expr=code_return_check_pith_unmemoized, | ||
# ) | ||
# | ||
#Then define "CODE_RETURN_CHECK" in the "wrapsnip" submodule to | ||
#resemble: | ||
# CODE_RETURN_CHECK = ( | ||
# f'{CODE_RETURN_CHECK_PREFIX}{{check_expr}}' | ||
# f'{CODE_RETURN_CHECK_SUFFIX}' | ||
# ) | ||
|
||
# Code snippet type-checking this return. | ||
code_return_check_prefix = CODE_RETURN_CHECK_PREFIX.format( | ||
func_call_prefix=( | ||
bear_call.func_wrapper_code_call_prefix)) | ||
|
||
# Full code snippet to be returned, consisting of: | ||
# * Calling the decorated callable and localize its return | ||
# *AND*... | ||
# * Type-checking this return *AND*... | ||
# * Returning this return from this wrapper function. | ||
func_wrapper_code = ( | ||
f'{code_return_check_prefix}' | ||
f'{code_return_check}' | ||
f'{CODE_RETURN_CHECK_SUFFIX}' | ||
) | ||
# Else, this hint is ignorable. | ||
# if not func_wrapper_code: print(f'Ignoring {bear_call.func_name} return hint {repr(hint)}...') | ||
# If one or more warnings were issued, reissue these warnings with each | ||
# placeholder substring (i.e., "EXCEPTION_PLACEHOLDER" instance) | ||
# replaced by a human-readable description of this callable and | ||
# annotated return. | ||
if warnings_issued: | ||
reissue_warnings_placeholder( | ||
warnings=warnings_issued, | ||
target_str=prefix_callable_return(bear_call.func_wrappee), | ||
) | ||
# Else, *NO* warnings were issued. | ||
# If any exception was raised, reraise this exception with each placeholder | ||
# substring (i.e., "EXCEPTION_PLACEHOLDER" instance) replaced by a | ||
# human-readable description of this callable and annotated return. | ||
except Exception as exception: | ||
reraise_exception_placeholder( | ||
exception=exception, | ||
target_str=prefix_callable_return(bear_call.func_wrappee), | ||
) | ||
|
||
# If a local scope is required to type-check this return, merge this scope | ||
# into the local scope currently required by the current wrapper function. | ||
if func_scope: | ||
update_mapping(bear_call.func_wrapper_scope, func_scope) | ||
# Else, *NO* local scope is required to type-check this return. | ||
|
||
# Return this code. | ||
return func_wrapper_code |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
#!/usr/bin/env python3 | ||
# --------------------( LICENSE )-------------------- | ||
# Copyright (c) 2014-2024 Beartype authors. | ||
# See "LICENSE" for further details. | ||
|
||
''' | ||
**Beartype decorator code generator utilities** (i.e., low-level callables | ||
assisting the parent :func:`beartype._decor.wrap.wrapmain` submodule). | ||
This private submodule is *not* intended for importation by downstream callers. | ||
''' | ||
|
||
# ....................{ IMPORTS }.................... | ||
from beartype._check.checkcall import BeartypeCall | ||
from beartype._check.checkmagic import CODE_PITH_ROOT_NAME_PLACEHOLDER | ||
from beartype._check.code.codescope import add_func_scope_ref | ||
from beartype._check.code.snip.codesnipstr import ( | ||
CODE_HINT_REF_TYPE_BASENAME_PLACEHOLDER_PREFIX, | ||
CODE_HINT_REF_TYPE_BASENAME_PLACEHOLDER_SUFFIX, | ||
) | ||
from beartype._data.error.dataerrmagic import EXCEPTION_PLACEHOLDER | ||
from beartype._util.hint.pep.proposal.pep484585.utilpep484585ref import ( | ||
get_hint_pep484585_ref_names_relative_to) | ||
from beartype._util.text.utiltextmunge import replace_str_substrs | ||
from collections.abc import Iterable | ||
|
||
# ....................{ CACHERS }.................... | ||
def unmemoize_func_wrapper_code( | ||
bear_call: BeartypeCall, | ||
func_wrapper_code: str, | ||
pith_repr: str, | ||
hint_refs_type_basename: tuple, | ||
) -> str: | ||
''' | ||
Convert the passed memoized code snippet type-checking any parameter or | ||
return of the decorated callable into an "unmemoized" code snippet | ||
type-checking a specific parameter or return of that callable. | ||
Specifically, this function (in order): | ||
#. Globally replaces all references to the | ||
:data:`.CODE_PITH_ROOT_NAME_PLACEHOLDER` placeholder substring | ||
cached into this code with the passed ``pith_repr`` parameter. | ||
#. Unmemoizes this code by globally replacing all relative forward | ||
reference placeholder substrings cached into this code with Python | ||
expressions evaluating to the classes referred to by those substrings | ||
relative to that callable when accessed via the private | ||
``__beartypistry`` parameter. | ||
Parameters | ||
---------- | ||
bear_call : BeartypeCall | ||
Decorated callable to be type-checked. | ||
func_wrapper_code : str | ||
Memoized callable-agnostic code snippet type-checking any parameter or | ||
return of the decorated callable. | ||
pith_repr : str | ||
Machine-readable representation of the name of this parameter or | ||
return. | ||
hint_refs_type_basename : tuple | ||
Tuple of the unqualified classnames referred to by all relative forward | ||
reference type hints visitable from the current root type hint. | ||
Returns | ||
------- | ||
str | ||
This memoized code unmemoized by globally resolving all relative | ||
forward reference placeholder substrings cached into this code relative | ||
to the currently decorated callable. | ||
''' | ||
assert bear_call.__class__ is BeartypeCall, ( | ||
f'{repr(bear_call)} not @beartype call.') | ||
assert isinstance(func_wrapper_code, str), ( | ||
f'{repr(func_wrapper_code)} not string.') | ||
assert isinstance(pith_repr, str), f'{repr(pith_repr)} not string.' | ||
assert isinstance(hint_refs_type_basename, Iterable), ( | ||
f'{repr(hint_refs_type_basename)} not iterable.') | ||
|
||
# Generate an unmemoized parameter-specific code snippet type-checking this | ||
# parameter by replacing in this parameter-agnostic code snippet... | ||
func_wrapper_code = replace_str_substrs( | ||
text=func_wrapper_code, | ||
# This placeholder substring cached into this code with... | ||
old=CODE_PITH_ROOT_NAME_PLACEHOLDER, | ||
# This object representation of the name of this parameter or return. | ||
new=pith_repr, | ||
) | ||
|
||
# If this code contains one or more relative forward reference placeholder | ||
# substrings memoized into this code, unmemoize this code by globally | ||
# resolving these placeholders relative to the decorated callable. | ||
if hint_refs_type_basename: | ||
# Metadata describing the callable currently being decorated by | ||
# beartype, localized purely as a negligible optimization. | ||
func = bear_call.func_wrappee | ||
func_scope = bear_call.func_wrapper_scope | ||
cls_stack = bear_call.cls_stack | ||
|
||
# For each unqualified classname referred to by a relative forward | ||
# reference type hints visitable from the current root type hint... | ||
for ref_basename in hint_refs_type_basename: | ||
# Possibly undefined fully-qualified module name and possibly | ||
# unqualified classname referred to by this relative forward | ||
# reference, relative to the decorated type stack and callable. | ||
ref_module_name, ref_name = get_hint_pep484585_ref_names_relative_to( | ||
hint=ref_basename, | ||
cls_stack=cls_stack, | ||
func=func, | ||
exception_prefix=EXCEPTION_PLACEHOLDER, | ||
) | ||
|
||
# Name of the hidden parameter providing this forward reference | ||
# proxy to be passed to this wrapper function. | ||
ref_expr = add_func_scope_ref( | ||
func_scope=func_scope, | ||
ref_module_name=ref_module_name, | ||
ref_name=ref_name, | ||
exception_prefix=EXCEPTION_PLACEHOLDER, | ||
) | ||
|
||
# Generate an unmemoized callable-specific code snippet checking | ||
# this class by globally replacing in this callable-agnostic code... | ||
func_wrapper_code = replace_str_substrs( | ||
text=func_wrapper_code, | ||
# This placeholder substring cached into this code with... | ||
old=( | ||
f'{CODE_HINT_REF_TYPE_BASENAME_PLACEHOLDER_PREFIX}' | ||
f'{ref_name}' | ||
f'{CODE_HINT_REF_TYPE_BASENAME_PLACEHOLDER_SUFFIX}' | ||
), | ||
# Python expression evaluating to this class when accessed via | ||
# this hidden parameter. | ||
new=ref_expr, | ||
) | ||
|
||
# Return this unmemoized callable-specific code snippet. | ||
return func_wrapper_code |
Oops, something went wrong.