Skip to content

Commit

Permalink
PEP 561 x 4.
Browse files Browse the repository at this point in the history
This commit is the next in a commit chain rendering `beartype` compliant
with PEP 561 by annotating the codebase in a manner specifically
compatible with the most popular third-party static type checker, mypy,
en-route to eventually resolving issue #25 kindly submitted also by best
macOS package manager ever @harens. Specifically, this commit further
reduces the number of pending mypy violations to merely 8. So close!
(*Unnerving servility!*)
  • Loading branch information
leycec committed Feb 18, 2021
1 parent c9ca6f5 commit ceee48b
Show file tree
Hide file tree
Showing 27 changed files with 470 additions and 161 deletions.
5 changes: 5 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,11 @@ whenever:
Checkers_>`__.
* You want to JIT_ your code with PyPy_, :superscript:`...which you should`,
which most static type checkers remain incompatible with.
* You prefer to write code rather than fight a static type checker, because
static type inference of a dynamically-typed language is guaranteed to fail
(and frequently does). If you've ever cursed the sky after suffixing working
code improperly typed by mypy_ with a vendor-specific pragma like ``# type:
ignore[{error_code}]``, ``beartype`` was specifically written for you.

If none of the above *still* apply, still use ``beartype``. It's `free
as in beer and speech <gratis versus libre_>`__, `cost-free at installation-
Expand Down
22 changes: 15 additions & 7 deletions beartype/_cave/abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,23 @@
# name='BoolType', method_names=('__bool__',))
#
#Dis goin' be good.
#FIXME: Actually, don't do any of the above. That would simply be reinventing
#the wheel, as the "typing.Protocol" superclass already exists and is more than
#up to the task. In fact, once we drop support for Python < 3.7, we should:
#* Redefine the "_BoolType" class declared below should in terms of the
# "typing.Protocol" superclass.
#* Shift the "_BoolType" class directly into the "beartype.cave" submodule.
#* Refactor away this entire submodule.

# ....................{ IMPORTS }....................
from abc import ABCMeta, abstractmethod
from typing import Union
from typing import Type

# See the "beartype.__init__" submodule for further commentary.
__all__ = ['STAR_IMPORTS_CONSIDERED_HARMFUL']

# ....................{ FUNCTIONS }....................
def _check_methods(C: type, *methods: str) -> (bool, type(NotImplemented)):
def _check_methods(C: type, *methods: str):
'''
Private utility function called by abstract base classes (ABCs)
implementing structural subtyping by detecting whether the passed class or
Expand All @@ -61,15 +68,16 @@ def _check_methods(C: type, *methods: str) -> (bool, type(NotImplemented)):
----------
C : type
Class to be validated as defining these methods.
methods : Tuple[str]
methods : Tuple[str, ...]
Tuple of the names of all methods to validate this class as defining.
Returns
----------
``True``
Only if this class defines all of these methods.
``NotImplemented``
Only if this class fails to define one or more of these methods.
Either:
* ``True`` if this class defines all of these methods.
* ``NotImplemented`` if this class fails to define one or more of these
methods.
'''

mro = C.__mro__
Expand Down
16 changes: 12 additions & 4 deletions beartype/_decor/_cache/cachehint.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,24 @@
This private submodule is *not* intended for importation by downstream callers.
'''

# ....................{ TODO }....................
#FIXME: The coercion function(s) defined below should also rewrite unhashable
#hints to be hashable *IF FEASIBLE.* This isn't always feasible, of course
#(e.g., "Annotated[[]]", "Literal[[]]"). The one notable place where this is
#feasible is with PEP 585-compliant type hints subscripted by unhashable rather
#than hashable iterables, which can *ALWAYS* be safely rewritten to be hashable
#(e.g., coercing "callable[[], None]" to "callable[(), None]").

# ....................{ IMPORTS }....................
from beartype._util.hint.utilhinttest import die_unless_hint
from collections.abc import Callable
from typing import Union
from typing import Any, Dict, Union

# See the "beartype.__init__" submodule for further commentary.
__all__ = ['STAR_IMPORTS_CONSIDERED_HARMFUL']

# ....................{ GLOBALS }....................
_HINT_REPR_TO_HINT = {}
_HINT_REPR_TO_HINT: Dict[str, Any] = {}
'''
**Type hint cache** (i.e., singleton dictionary mapping from the
machine-readable representations of all non-self-cached type hints to those
Expand Down Expand Up @@ -74,9 +82,9 @@
def cache_hint_nonpep563(
func: Callable,
pith_name: str,
hint: object,
hint: Any,
hint_label: str,
) -> object:
) -> Any:
'''
Coerce and cache the passed (possibly non-self-cached and/or
PEP-noncompliant) type hint annotating the parameter or return value with
Expand Down
7 changes: 4 additions & 3 deletions beartype/_decor/_cache/cachetype.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
get_object_classname,
get_object_class_basename,
)
from typing import Tuple

# See the "beartype.__init__" submodule for further commentary.
__all__ = ['STAR_IMPORTS_CONSIDERED_HARMFUL']
Expand Down Expand Up @@ -216,11 +217,11 @@ def register_typistry_type(hint: type) -> str:
@callable_cached
def register_typistry_tuple(
# Mandatory parameters.
hint: tuple,
hint: Tuple[type, ...],

# Optional parameters.
is_types_unique: bool = False,
) -> type:
) -> str:
'''
Register the passed tuple of one or more **PEP-noncompliant types** (i.e.,
classes neither defined by the :mod:`typing` module *nor* subclassing such
Expand Down Expand Up @@ -274,7 +275,7 @@ def register_typistry_tuple(
Parameters
----------
hint : tuple
hint : Tuple[type]
Tuple of all PEP-noncompliant types to be registered.
is_types_unique : bool
``True`` only if the caller guarantees this tuple to contain *no*
Expand Down
49 changes: 23 additions & 26 deletions beartype/_decor/_code/_pep/_pephint.py
Original file line number Diff line number Diff line change
Expand Up @@ -1861,8 +1861,7 @@

# ....................{ CODERS }....................
@callable_cached
def pep_code_check_hint(hint: object) -> (
Tuple[str, bool, Optional[Set[str]]]):
def pep_code_check_hint(hint: object) -> Tuple[str, bool, Tuple[str, ...]]:
'''
Python code type-checking the previously localized parameter or return
value annotated by the passed PEP-compliant type hint against this hint of
Expand Down Expand Up @@ -1898,7 +1897,7 @@ def pep_code_check_hint(hint: object) -> (

Returns
----------
Tuple[str, bool, Optional[Tuple[str]]
Tuple[str, bool, Tuple[str, ...]]
3-tuple ``(func_code, is_func_code_needs_random_int,
hints_forwardref_class_basename)``, where:

Expand All @@ -1911,7 +1910,7 @@ def pep_code_check_hint(hint: object) -> (
:func:`beartype._decor._code.codemain.generate_code` function
prefixes the body of this wrapper function with code generating such
an integer.
* ``hints_forwardref_class_basename`` is the tuple of the unqualified
* ``hints_forwardref_class_basename`` is a tuple of the unqualified
classnames of `PEP 484`_-compliant relative forward references
visitable from this root hint (e.g., ``('MuhClass', 'YoClass')``
given the root hint ``Union['MuhClass', List['YoClass']]``).
Expand Down Expand Up @@ -2053,29 +2052,29 @@ def pep_code_check_hint(hint: object) -> (
# ..................{ HINT ~ childs }..................
# Current tuple of all PEP-compliant child hints subscripting the currently
# visited hint (e.g., "(int, str)" if "hint_curr == Union[int, str]").
hint_childs = None
hint_childs: tuple = None # type: ignore[assignment]

# Number of PEP-compliant child hints subscripting the currently visited
# hint.
hint_childs_len = None
hint_childs_len: int = None # type: ignore[assignment]

# Set of all PEP-noncompliant child hints subscripting the currently
# visited hint.
hint_childs_nonpep = None
hint_childs_nonpep: set = None # type: ignore[assignment]

# Set of all PEP-compliant child hints subscripting the currently visited
# hint.
hint_childs_pep = None
hint_childs_pep: set = None # type: ignore[assignment]

# ..................{ HINT ~ pep 484 : forwardref }..................
# Set of the unqualified classnames referred to by all relative forward
# references visitable from this root hint if any *OR* "None" otherwise
# (i.e., if no such forward references are visitable).
hints_forwardref_class_basename = None
hints_forwardref_class_basename: set = None # type: ignore[assignment]

# Possibly unqualified classname referred to by the currently visited
# forward reference type hint.
hint_curr_forwardref_classname = None
hint_curr_forwardref_classname: str = None # type: ignore[assignment]

# ..................{ HINT ~ pep 572 }..................
# The following local variables isolated to this subsection are only
Expand Down Expand Up @@ -2148,17 +2147,17 @@ def pep_code_check_hint(hint: object) -> (

# Python >= 3.8-specific assignment expression assigning this full Python
# expression to the local variable assigned the value of this expression.
pith_curr_assign_expr = None
pith_curr_assign_expr: str = None # type: ignore[assignment]

# Name of the local variable uniquely assigned to by
# "pith_curr_assign_expr". Equivalently, this is the left-hand side (LHS)
# of that assignment expression.
pith_curr_assigned_expr = None
pith_curr_assigned_expr: str = None # type: ignore[assignment]

# ..................{ METADATA }..................
# Tuple of metadata describing the currently visited hint, appended by
# the previously visited parent hint to the "hints_meta" stack.
hint_curr_meta = None
hint_curr_meta: tuple = None # type: ignore[assignment]

# Fixed list of all metadata describing all visitable hints currently
# discovered by the breadth-first search (BFS) below. This lists acts as a
Expand Down Expand Up @@ -3444,21 +3443,19 @@ def _enqueue_hint_child(pith_child_expr: str) -> str:
),
)

# Tuple of the unqualified classnames referred to by all relative forward
# references visitable from this root hint converted from this set to
# reduce space consumption after memoization by @callable_cached.
hints_forwardref_class_basename = (
# If *NO* relative forward references are visitable from this root
# hint, the empty tuple.
()
if hints_forwardref_class_basename is None else
# Else, this set converted into a tuple.
tuple(hints_forwardref_class_basename)
)

# Return all metadata required by higher-level callers.
return (
func_code,
is_func_code_needs_random_int,
hints_forwardref_class_basename,
# Tuple of the unqualified classnames referred to by all relative
# forward references visitable from this hint converted from this set
# to reduce space consumption after memoization by @callable_cached.
(
# If *NO* relative forward references are visitable from this root
# hint, the empty tuple.
()
if hints_forwardref_class_basename is None else
# Else, this set converted into a tuple.
tuple(hints_forwardref_class_basename)
),
)
19 changes: 8 additions & 11 deletions beartype/_decor/_pep563.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,13 +167,12 @@ def _resolve_hints_postponed(data: BeartypeData) -> None:
.. _PEP 563:
https://www.python.org/dev/peps/pep-0563
'''
assert data.__class__ is BeartypeData, (
'{!r} not @beartype data.'.format(data))
assert data.__class__ is BeartypeData, f'{repr(data)} not @beartype data.'
# print('annotations: {!r}'.format(func.__annotations__))

# Localize attributes of this metadata for negligible efficiency gains.
func = data.func
func_globals = func.__globals__
func_globals = func.__globals__ # type: ignore[attr-defined]

# Dictionary mapping from parameter name to resolved annotation for each
# annotated parameter and return value of this callable.
Expand Down Expand Up @@ -242,8 +241,8 @@ def _resolve_hints_postponed(data: BeartypeData) -> None:
# higher-level human-readable beartype-specific exception.
except Exception as exception:
raise BeartypeDecorHintPep563Exception(
'{} postponed hint "{}" not evaluable.'.format(
pith_label, pith_hint)
f'{pith_label} postponed hint '
f'"{pith_hint}" not evaluable.'
) from exception
# Else, this annotation is *NOT* a PEP 563-formatted postponed string.
# Since PEP 563 is active for this callable, this implies this
Expand Down Expand Up @@ -366,8 +365,7 @@ def _die_if_hint_repr_exceeds_child_limit(
.. _PEP 563:
https://www.python.org/dev/peps/pep-0563
'''
assert isinstance(hint_repr, str), (
'{!r} not string.'.format(hint_repr))
assert isinstance(hint_repr, str), f'{repr(hint_repr)} not string.'

# Total number of hints transitively encapsulated in this hint (i.e., the
# total number of all child hints of this hint as well as this hint
Expand All @@ -389,7 +387,6 @@ def _die_if_hint_repr_exceeds_child_limit(
# which the @beartype decorator traverses this hint, raise an exception.
if hints_num >= SIZE_BIG:
raise BeartypeDecorHintPepException(
'{} hint representation "{}" '
'contains {} subscripted arguments '
'exceeding maximum limit {}.'.format(
pith_label, hint_repr, hints_num, SIZE_BIG-1))
f'{pith_label} hint representation "{hint_repr}" '
f'contains {hints_num} subscripted arguments '
f'exceeding maximum limit {SIZE_BIG-1}.')
85 changes: 85 additions & 0 deletions beartype/_decor/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,91 @@
# "_PARAM_KIND_IGNORABLE" set.
#* Remove the "_PARAM_KIND_IGNORABLE" set entirely.

#FIXME: [NEW PROJECT] Consider creating a new "beartype/bearclaw" project *OR
#SOMETHING* (e.g., "bearcall", "bearely", etc.) to enable arbitrary O(1)
#runtime type checking. Actually, "bearely" is a super-hot name, so let's run
#with that, shall we? By "arbitrary," we mean just that: O(1) runtime type
#checking that anyone can perform in any arbitrary expression without having to
#isolate that checking to a callable signature.
#
#First, let's spec the public API. Fortunately, that's trivial. Just as with
#"beartype", we define only a single public turbo-charged function:
#* Define a public "bearely" package.
#* Define a private "bearely._main" submodule *OR SOMETHING.*
#* In that submodule:
# * Define a public istypedas() tester with the signature:
#
# def istypedas(obj: object, hint: object) -> bool:
#
# ...where "obj" is any arbitrary object and "hint" is any PEP-compliant
# type hint (or, more generally, any @beartype-compliant type hint).
# * Actually, define a public is_typed_as() alias to the istypedas() tester.
# Not everyone wants cute names; PEP 8-compliant snake case is often
# preferable and we ourselves would probably prefer the former, for example.
#* Define a dunder "bearely.__init__" submodule publicizing
# bearely._main.istypedas() as "bearely.istypedas().
#* Have "bearely" depend upon "beartype" as its only mandatory runtime
# dependency.
#
#*YUP.* istypedas() is the single public turbo-charged function declared by
#this package. The nomenclature for this tester derives, of course, from the
#builtin isinstanceof() and issubclassof() builtins. istypedas() could be
#considered a generalization or proper superset of both -- in that anything you
#can do with those builtins you can do with istypedas(), but you can also do
#*MUCH* more with istypedas().
#
#Fortuitously, implementing istypedas() in terms of the existing @beartype
#decorator is trivial and requires absolutely *NO* refactoring of the
#"beartype" codebase itself, which is certainly nice (albeit non-essential):
#* Internally, istypedas() should maintain a *non-LRU* cache (probably in a
# separate "bearely._cache" submodule as a simple dictionary) named
# "HINT_OR_HINT_REPR_TO_BEARTYPE_WRAPPER" mapping from each arbitrary
# PEP-compliant type hint (passed as the second parameter to istypedas()) to
# the corresponding wrapper function dynamically generated by the @beartype
# decorator checking an arbitrary object against that hint. However, note
# there there's a significant caveat here:
# * *NOT ALL HINTS ARE CACHABLE.* If the passed hint is *NOT* cachable, we
# should instead cache that hint under its machine-readable repr() string.
# While slower to generate, generating that string is still guaranteed to be
# *MUCH* faster than dynamically declaring a new function each call.
#* The trivial way to implement the prior item is to dynamically define one new
# private @beartype-decorated noop function accepting an arbitrary parameter
# type-hinted by each type hint: e.g.,
# # Pseudo-code, obviously. Again, this snippet should probably be
# # shifted into a new "bearely._snip" submodule.
# is_typed_as_wrapper = exec(f'''
# @beartype
# def is_typed_as_wrapper(obj: {hint}): pass
# ''')
#* After either defining and caching that wrapper into the above dictionary
# *OR* retrieved a previously wrapper from that dictionary, trivially
# implement this check with EAFP as follows:
# try:
# is_typed_as_wrapper(obj)
# return True
# except:
# return False
#
#*DONE.* Sweet, yah? The above can and should be heavily optimized, of course.
#How? That remains to be determined. The principle issue with the above
#approach is that it unnecessarily incurs an additional stack frame. Since the
#original is_typed_as_wrapper() function wrapped by @beartype doesn't actually
#do anything, it would be really nice if the wrapper generated by @beartype
#omitted the call to that original function.
#
#This might be easier than expected. You're probably thinking AST inspector or
#disassembly, right? Neither of those two things are easy or fast, so let's do
#neither. Is there any alternative? There might be. In theory, the code object
#for any callable whose implementation is literally "pass" should be trivially
#detectable via metadata on that object. If nothing else, the byte code for
#that object should be a constant size; any code object whose byte code is
#larger than that size is *NOT* a "pass" noop.
#
#In any case, @beartype should efficiently detect noop callables and avoid
#calling those callables from the wrapper functions it generates for those
#callables. This would be genuinely useful from the general-purpose
#perspective, which means we should make this happen.

# ....................{ IMPORTS }....................
import functools, random
from beartype.roar import (
Expand Down

0 comments on commit ceee48b

Please sign in to comment.