Skip to content

Commit

Permalink
"typing_extensions.Annotated" x 18.
Browse files Browse the repository at this point in the history
This commit is the next in a commit chain adding transparent support
for the third-party `typing_extensions.Annotated` type hint back-ported
to Python < 3.9, en route to resolving #34. Once finalized, this will
enable usage of beartype validators under Python < 3.9 via this hint.
Specifically, this commit continues disastrously breaking literally
everything by continuing to disembowel the feckless
`beartype._util.hint.data.pep.datapep` submodule and its untrustworthy
`beartype._util.hint.data.pep.proposal` crony subpackage in favour of
`beartype._util.hint.data.pep.sign`, which is the only subpackage left
standing. Save us from our reckless selves, GitHub! (*Crabby crabapples!*)
  • Loading branch information
leycec committed Jun 28, 2021
1 parent e120577 commit 87e8d47
Show file tree
Hide file tree
Showing 11 changed files with 529 additions and 568 deletions.
127 changes: 127 additions & 0 deletions beartype/_decor/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
#!/usr/bin/env python3
# --------------------( LICENSE )--------------------
# Copyright (c) 2014-2021 Beartype authors.
# See "LICENSE" for further details.

# ....................{ TODO }....................
#FIXME: [FEATURE] Define the following supplementary decorators:
#* @beartype.beartype_O1(), identical to the current @beartype.beartype()
# decorator but provided for disambiguity. This decorator only type-checks
# exactly one item from each container for each call rather than all items.
#* @beartype.beartype_Ologn(), type-checking log(n) random items from each
# container of "n" items for each call.
#* @beartype.beartype_On(), type-checking all items from each container for
# each call. We have various ideas littered about GitHub on how to optimize
# this for various conditions, but this is never going to be ideal and should
# thus never be the default.
#
#To differentiate between these three strategies, consider:
#* Declare an enumeration in "beartype._decor._data" resembling:
# from enum import Enum
# BeartypeStrategyKind = Enum('BeartypeStrategyKind ('O1', 'Ologn', 'On',))
#* Define a new "BeartypeData.strategy_kind" instance variable.
#* Set this variable to the corresponding "BeartypeStrategyKind" enumeration
# member based on which of the three decorators listed above was called.
#* Explicitly pass the value of the "BeartypeData.strategy_kind" instance
# variable to the beartype._decor._code._pep._pephint.pep_code_check_hint()
# function as a new memoized "strategy_kind" parameter.
#* Conditionally generate type-checking code throughout that function depending
# on the value of that parameter.

#FIXME: Ensure that *ALL* calls to memoized callables throughout the codebase
#are called with purely positional rather than keyword arguments. Currently, we
#suspect the inverse is the case. To do so, we'll probably want to augment the
#wrapper closure returned by the @callable_cached decorator to emit non-fatal
#warnings when called with non-empty keyword arguments.
#
#Alternately, we might simply want to prohibit keyword arguments altogether by
#defining a new @callable_cached_positional decorator restricted to positional
#arguments. Right... That probably makes more sense. Make it so, ensign!
#
#Then, for generality:
#
#* Preserve the existing @callable_cached decorator as is. We won't be using
# it, but there's little sense in destroying something beautiful.
#* Globally replace all existing "@callable_cached" substrings with
# "@callable_cached_positional". Voila!

#FIXME: *CRITICAL EDGE CASE:* If the passed "func" is a coroutine, that
#coroutine *MUST* be called preceded by the "await" keyword rather than merely
#called as is. Detecting coroutines is trivial, thankfully: e.g.,
#
# if inspect.iscoroutinefunction(func):
#
#Actually, shouldn't that be the more general-purpose test:
#
# if inspect.isawaitable(func):
#
#The latter seems more correct. In any case, given that:
#
#* Modify the "CODE_CALL_CHECKED" and "CODE_CALL_UNCHECKED" snippets to
# conditionally precede the function call with the substring "await ": e.g.,
# CODE_CALL_UNCHECKED = '''
# return {func_await}__beartype_func(*args, **kwargs)
# '''
# Note the absence of delimiting space. This is, of course, intentional.
#* Unconditionally format the "func_await" substring into both of those
# snippets, define ala:
# format_await = 'await ' if inspect.iscoroutinefunction(func) else ''
#* Oh, and note that our defined wrapper function must also be preceded by the
# "async " keyword. So, we'll also need to augment "CODE_SIGNATURE".
#FIXME: As a counterargument to the above approach, note this commentary I
#stumbled across while researching an entirely separate topic:
# "...trying to automatically detect whether a function is sync or async
# it’s almost always a bad idea, because it’s very difficult to do reliably.
# Instead it’s almost always better to make the user say explicitly which
# one they mean, for example by having two versions of a decorator and
# telling the user to use @mydecorator_sync on sync functions and
# @mydecorator_async on async functions."
#Is this actually the case? Clearly, we'll need to research just how
#deterministic the inspect.isawaitable() tester is. Does that tester fall down
#(i.e., return false negatives or positives) in well-known edge cases?
#FIXME: Unit test this extensively, please.

#FIXME: Non-critical optimization: if the active Python interpreter is already
#performing static type checking (e.g., with Pyre or mypy), @beartype should
#unconditionally reduce to a noop for the current process. Note that:
#
#* Detecting static type checking is trivial, as PEP 563 standardizes the newly
# declared "typing.TYPE_CHECKING" boolean constant to be true only if static
# type checking is currently occurring. Note that @beartype supports this now.
#* Detecting whether static type checking just occurred is clearly less
# trivial and possibly even infeasible. We're unclear what exactly separates
# the "static type checking" phase from the runtime phase performed by static
# type checkers, but something clearly does. If all else fails, we can
# probably attempt to detect whether the basename of the command invoked by
# the parent process matches "(Pyre|mypy|pyright|pytype)" or... something. Of
# course, that itself is non-trivial due to Windows, so here we are. *sigh*

#FIXME: Emit one non-fatal warning for each annotated type that is either:
#
#* "beartype.cave.UnavailableType".
#* "beartype.cave.UnavailableTypes".
#
#Both cases imply user-side misconfiguration, but not sufficiently awful enough
#to warrant fatal exceptions. Moreover, emitting warnings rather than
#exceptions enables end users to unconditionally disable all unwanted warnings,
#whereas no such facilities exist for unwanted exceptions.
#FIXME: Validate all tuple annotations to be non-empty *EXCLUDING*
#"beartype.cave.UnavailableTypes", which is intentionally empty.
#FIXME: Unit test the above edge case.

#FIXME: Add support for all possible kinds of parameters. @beartype currently
#supports most but *NOT* all types. Specifically:
#
#* Type-check variadic keyword arguments. Currently, only variadic positional
# arguments are type-checked. When doing so, remove the
# "Parameter.VAR_KEYWORD" type from the "_PARAM_KIND_IGNORABLE" set.
#* Type-check positional-only arguments under Python >= 3.8. Note that, since
# C-based callables have *ALWAYS* supported positional-only arguments, the
# "Parameter.POSITIONAL_ONLY" type is defined for *ALL* Python versions
# despite only being usable in actual Python from Python >= 3.8. In other
# words, support for type-checking positional-only arguments should be added
# unconditionally without reference to Python version -- we suspect, anyway.
# When doing so, remove the "Parameter.POSITIONAL_ONLY" type from the
# "_PARAM_KIND_IGNORABLE" set.
#* Remove the "_PARAM_KIND_IGNORABLE" set entirely.

155 changes: 23 additions & 132 deletions beartype/_decor/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,126 +16,8 @@
'''

# ....................{ TODO }....................
#FIXME: [FEATURE] Define the following supplementary decorators:
#* @beartype.beartype_O1(), identical to the current @beartype.beartype()
# decorator but provided for disambiguity. This decorator only type-checks
# exactly one item from each container for each call rather than all items.
#* @beartype.beartype_Ologn(), type-checking log(n) random items from each
# container of "n" items for each call.
#* @beartype.beartype_On(), type-checking all items from each container for
# each call. We have various ideas littered about GitHub on how to optimize
# this for various conditions, but this is never going to be ideal and should
# thus never be the default.
#
#To differentiate between these three strategies, consider:
#* Declare an enumeration in "beartype._decor._data" resembling:
# from enum import Enum
# BeartypeStrategyKind = Enum('BeartypeStrategyKind ('O1', 'Ologn', 'On',))
#* Define a new "BeartypeData.strategy_kind" instance variable.
#* Set this variable to the corresponding "BeartypeStrategyKind" enumeration
# member based on which of the three decorators listed above was called.
#* Explicitly pass the value of the "BeartypeData.strategy_kind" instance
# variable to the beartype._decor._code._pep._pephint.pep_code_check_hint()
# function as a new memoized "strategy_kind" parameter.
#* Conditionally generate type-checking code throughout that function depending
# on the value of that parameter.

#FIXME: Ensure that *ALL* calls to memoized callables throughout the codebase
#are called with purely positional rather than keyword arguments. Currently, we
#suspect the inverse is the case. To do so, we'll probably want to augment the
#wrapper closure returned by the @callable_cached decorator to emit non-fatal
#warnings when called with non-empty keyword arguments.
#
#Alternately, we might simply want to prohibit keyword arguments altogether by
#defining a new @callable_cached_positional decorator restricted to positional
#arguments. Right... That probably makes more sense. Make it so, ensign!
#
#Then, for generality:
#
#* Preserve the existing @callable_cached decorator as is. We won't be using
# it, but there's little sense in destroying something beautiful.
#* Globally replace all existing "@callable_cached" substrings with
# "@callable_cached_positional". Voila!

#FIXME: *CRITICAL EDGE CASE:* If the passed "func" is a coroutine, that
#coroutine *MUST* be called preceded by the "await" keyword rather than merely
#called as is. Detecting coroutines is trivial, thankfully: e.g.,
#
# if inspect.iscoroutinefunction(func):
#
#Actually, shouldn't that be the more general-purpose test:
#
# if inspect.isawaitable(func):
#
#The latter seems more correct. In any case, given that:
#
#* Modify the "CODE_CALL_CHECKED" and "CODE_CALL_UNCHECKED" snippets to
# conditionally precede the function call with the substring "await ": e.g.,
# CODE_CALL_UNCHECKED = '''
# return {func_await}__beartype_func(*args, **kwargs)
# '''
# Note the absence of delimiting space. This is, of course, intentional.
#* Unconditionally format the "func_await" substring into both of those
# snippets, define ala:
# format_await = 'await ' if inspect.iscoroutinefunction(func) else ''
#* Oh, and note that our defined wrapper function must also be preceded by the
# "async " keyword. So, we'll also need to augment "CODE_SIGNATURE".
#FIXME: As a counterargument to the above approach, note this commentary I
#stumbled across while researching an entirely separate topic:
# "...trying to automatically detect whether a function is sync or async
# it’s almost always a bad idea, because it’s very difficult to do reliably.
# Instead it’s almost always better to make the user say explicitly which
# one they mean, for example by having two versions of a decorator and
# telling the user to use @mydecorator_sync on sync functions and
# @mydecorator_async on async functions."
#Is this actually the case? Clearly, we'll need to research just how
#deterministic the inspect.isawaitable() tester is. Does that tester fall down
#(i.e., return false negatives or positives) in well-known edge cases?
#FIXME: Unit test this extensively, please.

#FIXME: Non-critical optimization: if the active Python interpreter is already
#performing static type checking (e.g., with Pyre or mypy), @beartype should
#unconditionally reduce to a noop for the current process. Note that:
#
#* Detecting static type checking is trivial, as PEP 563 standardizes the newly
# declared "typing.TYPE_CHECKING" boolean constant to be true only if static
# type checking is currently occurring. Note that @beartype supports this now.
#* Detecting whether static type checking just occurred is clearly less
# trivial and possibly even infeasible. We're unclear what exactly separates
# the "static type checking" phase from the runtime phase performed by static
# type checkers, but something clearly does. If all else fails, we can
# probably attempt to detect whether the basename of the command invoked by
# the parent process matches "(Pyre|mypy|pyright|pytype)" or... something. Of
# course, that itself is non-trivial due to Windows, so here we are. *sigh*

#FIXME: Emit one non-fatal warning for each annotated type that is either:
#
#* "beartype.cave.UnavailableType".
#* "beartype.cave.UnavailableTypes".
#
#Both cases imply user-side misconfiguration, but not sufficiently awful enough
#to warrant fatal exceptions. Moreover, emitting warnings rather than
#exceptions enables end users to unconditionally disable all unwanted warnings,
#whereas no such facilities exist for unwanted exceptions.
#FIXME: Validate all tuple annotations to be non-empty *EXCLUDING*
#"beartype.cave.UnavailableTypes", which is intentionally empty.
#FIXME: Unit test the above edge case.

#FIXME: Add support for all possible kinds of parameters. @beartype currently
#supports most but *NOT* all types. Specifically:
#
#* Type-check variadic keyword arguments. Currently, only variadic positional
# arguments are type-checked. When doing so, remove the
# "Parameter.VAR_KEYWORD" type from the "_PARAM_KIND_IGNORABLE" set.
#* Type-check positional-only arguments under Python >= 3.8. Note that, since
# C-based callables have *ALWAYS* supported positional-only arguments, the
# "Parameter.POSITIONAL_ONLY" type is defined for *ALL* Python versions
# despite only being usable in actual Python from Python >= 3.8. In other
# words, support for type-checking positional-only arguments should be added
# unconditionally without reference to Python version -- we suspect, anyway.
# When doing so, remove the "Parameter.POSITIONAL_ONLY" type from the
# "_PARAM_KIND_IGNORABLE" set.
#* Remove the "_PARAM_KIND_IGNORABLE" set entirely.
# All "FIXME:" comments for this submodule reside in this package's "__init__"
# submodule to improve maintainability and readability here.

# ....................{ IMPORTS }....................
from beartype.roar import (
Expand All @@ -152,27 +34,36 @@
# See the "beartype.cave" submodule for further commentary.
__all__ = ['STAR_IMPORTS_CONSIDERED_HARMFUL']

# ....................{ GLOBALS }....................
T = TypeVar('T', bound=Callable[..., Any])
'''
:pep:`484`-compliant **generic callable type variable** (i.e., type hint
matching any arbitrary function or method).
This type variable notifies static analysis performed by both static type
checkers (e.g., :mod:`mypy`) and type-aware IDEs (e.g., VSCode) that the
:mod:`beartype` decorator preserves callable signatures by creating and
returning callables with the same signatures as the passed callables.
'''

# ....................{ DECORATORS }....................
def beartype(func: T) -> T:
'''
Decorate the passed **pure-Python callable** (e.g., function or method
declared in Python rather than C) to validate both all annotated parameters
passed to this callable *and* the annotated value returned by this callable
if any.
Dynamically create and return a **constant-time runtime type-checker**
(i.e., pure-Python function validating all parameters and returns of all
calls to the passed pure-Python callable against all PEP-compliant type
hints annotating those parameters and returns).
This decorator performs rudimentary type checking based on Python 3.x
function annotations, as officially documented by PEP 484 ("Type Hints").
While PEP 484 supports arbitrarily complex type composition, this decorator
requires *all* parameter and return value annotations to be either:
The type-checker returned by this decorator is:
* Classes (e.g., :class:`int`, :class:`OrderedDict`).
* Tuples of classes (e.g., ``(int, OrderedDict)``).
* Optimized uniquely for the passed callable.
* Guaranteed to run in O(1) constant-time with negligible constant factors.
* Type-check effectively instantaneously.
* Add effectively no runtime overhead to the passed callable.
If optimizations are enabled by the active Python interpreter (e.g., due to
option ``-O`` passed to this interpreter), this decorator reduces to a
noop.
option ``-O`` passed to this interpreter), this decorator silently reduces
to a noop.
Parameters
----------
Expand Down
14 changes: 14 additions & 0 deletions beartype/_util/data/hint/pep/datapepmap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env python3
# --------------------( LICENSE )--------------------
# Copyright (c) 2014-2021 Beartype authors.
# See "LICENSE" for further details.

'''
Project-wide **PEP-compliant type hint sign mappings** (i.e., dictionary
globals mapping to and fro instances of the
:class:`beartype._util.data.hint.pep.sign.datapepsigncls.HintSign` class,
enabling efficient mapping between non-signs and their associated signs).
This private submodule is *not* intended for importation by downstream callers.
'''

0 comments on commit 87e8d47

Please sign in to comment.