Skip to content

Commit

Permalink
Parameter default type-checking x 1.
Browse files Browse the repository at this point in the history
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
leycec committed Mar 21, 2024
1 parent fb82b1b commit dfe4791
Show file tree
Hide file tree
Showing 8 changed files with 777 additions and 739 deletions.
5 changes: 1 addition & 4 deletions beartype/_check/error/_util/errorutiltext.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,7 @@
color_pith,
color_type,
)
from beartype._util.text.utiltextlabel import (
label_object_type,
label_type,
)
from beartype._util.text.utiltextlabel import label_object_type
from beartype._util.text.utiltextprefix import prefix_beartypeable
from beartype._util.text.utiltextrepr import represent_object
from collections.abc import Callable
Expand Down
354 changes: 354 additions & 0 deletions beartype/_decor/wrap/_wrapargs.py

Large diffs are not rendered by default.

245 changes: 245 additions & 0 deletions beartype/_decor/wrap/_wrapreturn.py
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
137 changes: 137 additions & 0 deletions beartype/_decor/wrap/_wraputil.py
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

0 comments on commit dfe4791

Please sign in to comment.