Skip to content

Commit

Permalink
PEP 561 x 6.
Browse files Browse the repository at this point in the history
This commit is the last 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,
resolving issue #25 kindly submitted by best macOS package manager ever
@harens. Specifically, this commit reduces the number of mypy violations
to 0 and enables a CPython-specific functional test exercising mypy
against the current codebase, which guarantees test failures on any push
or pull request (PR) violating mypy expectations. Thanks again for all
the magnanimous beneficence, @harens! (*Lofty spacious loft spaces!*)
  • Loading branch information
leycec committed Feb 20, 2021
1 parent 2c9c92f commit f7ec6e8
Show file tree
Hide file tree
Showing 13 changed files with 95 additions and 57 deletions.
4 changes: 4 additions & 0 deletions .mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
# file's unambiguous basename of ".mypy.ini". One is enraged by bureaucracy!
[mypy]

# Comma-separated string listing the pathnames of all project paths to be
# checked by mypy by default if none are explicitly passed on the command line.
files = beartype/

# Display machine-readable "["- and "]"-bracketed error codes in *ALL*
# mypy-specific error messages. This option is disabled by default, which is
# awful, because these codes are the *ONLY* means of explicitly ignoring
Expand Down
32 changes: 27 additions & 5 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -342,11 +342,29 @@ whenever:

* You want to `check types decidable only at runtime <Versus Static Type
Checkers_>`__.
* 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.
* You want to write code rather than fight a static type checker, because
`static type inference <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
non-portable vendor-specific pragmas like ``# type:
ignore[{inscrutable_error_code}]``, ``beartype`` was written for you.
* You want to preserve `dynamic typing`_, because Python is a
`dynamically-typed`_ language. Unlike ``beartype``, static type checkers
enforce `static typing`_ and are thus strongly opinionated; they believe
`dynamic typing`_ is harmful and emit errors on `dynamically-typed`_ code,
including common Python use patterns like changing the type of a variable
(e.g., by assigning that variable objects of different types over that
variable's lifetime). In contrast:

.. ::
**Beartype believes dynamic typing is beneficial and never emits errors on
dynamically-typed code.** That's because ``beartype`` `operates
exclusively at the high level of pure-Python functions and methods <Versus
Static Type Checkers_>`__ rather than the low level of individual
statements *inside* pure-Python functions and methods. Unlike static type
checkers, ``beartype`` can't be opinionated about things that no one
should be.

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 Expand Up @@ -2934,9 +2952,13 @@ application stack at tool rather than Python runtime) include:
https://en.wikipedia.org/wiki/Random_walk
.. _shield wall:
https://en.wikipedia.org/wiki/Shield_wall
.. _dynamic typing:
.. _dynamically-typed:
.. _static typing:
.. _statically-typed:
https://en.wikipedia.org/wiki/Type_system
.. _type inference:
https://en.wikipedia.org/wiki/Type_inference
.. _zero-cost abstraction:
https://boats.gitlab.io/blog/post/zero-cost-abstractions

Expand Down
4 changes: 2 additions & 2 deletions beartype/_cave/mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def __setitem__(self, key: object, value: object) -> None:
'''

raise BeartypeCaveNoneTypeOrMutabilityException(
'{!r} externally immutable (i.e., not settable).'.format(self))
f'{repr(self)} externally immutable (i.e., not settable).')


def __missing__(self, hint: Union[type, str, tuple]) -> tuple:
Expand Down Expand Up @@ -180,7 +180,7 @@ def __missing__(self, hint: Union[type, str, tuple]) -> tuple:
# Nonetheless, raise a human-readable exception for sanity.
else:
raise BeartypeCaveNoneTypeOrKeyException(
'"NoneTypeOr" key {!r} unsupported.'.format(hint))
f'"NoneTypeOr" key {repr(hint)} unsupported.')

# Return this new tuple.
#
Expand Down
4 changes: 2 additions & 2 deletions beartype/_decor/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -544,12 +544,12 @@ def beartype(func: Callable) -> Callable:
# dictionary is guaranteed to contain a key with this wrapper's name whose
# value is this wrapper. Ergo, no additional validation of the existence of
# this key or type of this wrapper is needed.
func_wrapper = local_attrs[func_data.func_wrapper_name]
func_wrapper: Callable = local_attrs[func_data.func_wrapper_name] # type: ignore[assignment]

# Declare this wrapper to be generated by @beartype, which tests for the
# existence of this attribute above to avoid re-decorating callables
# already decorated by @beartype by efficiently reducing to a noop.
func_wrapper.__beartype_wrapper = True
func_wrapper.__beartype_wrapper = True # type: ignore[attr-defined]

# Propagate identifying metadata (stored as special attributes) from the
# original function to this wrapper for debuggability, including:
Expand Down
22 changes: 11 additions & 11 deletions beartype/_util/cache/pool/utilcachepool.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
# ....................{ IMPORTS }....................
from beartype.roar import _BeartypeUtilCachedKeyPoolException
from collections import defaultdict
from collections.abc import Hashable
from collections.abc import Callable, Hashable
from threading import Lock
from typing import Any, Callable, Dict
from typing import Any, Dict, Union

# ....................{ CLASSES }....................
class KeyPool(object):
Expand All @@ -31,12 +31,12 @@ class KeyPool(object):
Attributes
----------
_key_to_pool : defaultdict
Dictionary mapping from an **arbitrary key** (i.e., hashable objects)
to a corresponding **pool** (i.e., list of zero or more arbitrary
objects referred to as "pool items" cached under that key). For both
efficiency and simplicity, this dictionary is defined as a
:class:`defaultdict` implicitly initializing missing keys on initial
access to the empty list.
Dictionary mapping from an **arbitrary key** (i.e., hashable object) to
corresponding **pool** (i.e., list of zero or more arbitrary objects
referred to as "pool items" cached under that key). For both efficiency
and simplicity, this dictionary is defined as a :class:`defaultdict`
implicitly initializing missing keys on initial access to the empty
list.
_pool_item_id_to_is_acquired : dict
Dictionary mapping from the unique object identifier of a **pool item**
(i.e., arbitrary object cached under a pool of the :attr:`_key_to_pool`
Expand Down Expand Up @@ -76,14 +76,14 @@ class KeyPool(object):
# ..................{ INITIALIZER }..................
def __init__(
self,
item_maker: Callable[[Hashable,], Any],
item_maker: Union[type, Callable],
) -> None:
'''
Initialize this key pool with the passed factory callable.
Parameters
----------
item_maker : Callable[Hashable, Any]
item_maker : Union[type, Callable[[Hashable,], Any]]
Caller-defined factory callable internally called by the
:meth:`acquire` method on attempting to acquire a non-existent
object from an **empty pool** (i.e., either a missing key *or* an
Expand Down Expand Up @@ -114,7 +114,7 @@ def item_maker(key: Hashable) -> object: ...
# >>> dd = defaultdict(default_factory=list)
# >>> dd['ee']
# KeyError: 'ee'
self._key_to_pool = defaultdict(list)
self._key_to_pool: Dict[Hashable, list] = defaultdict(list)
self._pool_item_id_to_is_acquired: Dict[int, bool] = {}
self._thread_lock = Lock()

Expand Down
17 changes: 9 additions & 8 deletions beartype/_util/cache/pool/utilcachepoollistfixed.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from beartype._util.text.utiltextrepr import get_object_representation
from beartype.roar import _BeartypeUtilCachedFixedListException
from collections.abc import Iterable, Sized
from typing import NoReturn

# See the "beartype.cave" submodule for further commentary.
__all__ = ['STAR_IMPORTS_CONSIDERED_HARMFUL']
Expand Down Expand Up @@ -120,18 +121,18 @@ def copy(self) -> 'FixedList':
# Prohibit dunder methods modifying list length by overriding these methods
# to raise exceptions.

def __delitem__(self, index):
def __delitem__(self, index) -> NoReturn:
raise _BeartypeUtilCachedFixedListException(
f'{self._label} index {repr(index)} not deletable.')


def __iadd__(self, value):
def __iadd__(self, value) -> NoReturn: # type: ignore[misc]
raise _BeartypeUtilCachedFixedListException(
f'{self._label} not addable by '
f'{get_object_representation(value)}.')


def __imul__(self, value):
def __imul__(self, value) -> NoReturn: # type: ignore[misc]
raise _BeartypeUtilCachedFixedListException(
f'{self._label} not multipliable by '
f'{get_object_representation(value)}.')
Expand Down Expand Up @@ -219,29 +220,29 @@ def _die_if_slice_len_ne_value_len(self, index, value) -> None:
# Prohibit non-dunder methods modifying list length by overriding these
# methods to raise exceptions.

def append(self, obj) -> None:
def append(self, obj) -> NoReturn:
raise _BeartypeUtilCachedFixedListException(
f'{self._label} not appendable by '
f'{get_object_representation(obj)}.')


def clear(self) -> None:
def clear(self) -> NoReturn:
raise _BeartypeUtilCachedFixedListException(
f'{self._label} not clearable.')


def extend(self, obj) -> None:
def extend(self, obj) -> NoReturn:
raise _BeartypeUtilCachedFixedListException(
f'{self._label} not extendable by '
f'{get_object_representation(obj)}.')


def pop(self, *args) -> None:
def pop(self, *args) -> NoReturn:
raise _BeartypeUtilCachedFixedListException(
f'{self._label} not poppable.')


def remove(self, *args) -> None:
def remove(self, *args) -> NoReturn:
raise _BeartypeUtilCachedFixedListException(
f'{self._label} not removable.')

Expand Down
9 changes: 4 additions & 5 deletions beartype/_util/cache/utilcachecall.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
from beartype._util.utilobject import SENTINEL, Iota
from functools import wraps
from inspect import Parameter
from typing import Callable
from typing import Callable, Dict
from warnings import warn

# ....................{ CONSTANTS ~ private }....................
Expand Down Expand Up @@ -202,21 +202,20 @@ class and returns a boolean. Under conservative assumptions of 4 bytes of
if is_func_arg_variadic(func):
raise _BeartypeUtilCallableCachedException(
f'@callable_cached {label_callable(func)} '
f'variadic arguments not cacheable.'
)
f'variadic arguments not cacheable.')

# Dictionary mapping a tuple of all flattened parameters passed to each
# prior call of the decorated callable with the value returned by that
# call if any (i.e., if that call did *NOT* raise an exception).
params_flat_to_return_value = {}
params_flat_to_return_value: Dict[tuple, object] = {}

# get() method of this dictionary, localized for efficiency.
params_flat_to_return_value_get = params_flat_to_return_value.get

# Dictionary mapping a tuple of all flattened parameters passed to each
# prior call of the decorated callable with the exception raised by that
# call if any (i.e., if that call raised an exception).
params_flat_to_exception = {}
params_flat_to_exception: Dict[tuple, Exception] = {}

# get() method of this dictionary, localized for efficiency.
params_flat_to_exception_get = params_flat_to_exception.get
Expand Down
4 changes: 2 additions & 2 deletions beartype/_util/hint/pep/proposal/utilhintpep593.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
# ....................{ IMPORTS }....................
from beartype.roar import BeartypeDecorHintPepException
from beartype._util.py.utilpyversion import IS_PYTHON_AT_LEAST_3_9
from typing import Optional
from typing import Any, Optional

# See the "beartype.cave" submodule for further commentary.
__all__ = ['STAR_IMPORTS_CONSIDERED_HARMFUL']
Expand Down Expand Up @@ -137,7 +137,7 @@ def is_hint_pep593_ignorable_or_none(
'''

# ....................{ GETTERS ~ newtype }....................
def get_hint_pep593_hint(hint: object) -> object:
def get_hint_pep593_hint(hint: Any) -> object:
'''
PEP-compliant type hint annotated by the passed `PEP 593`_-compliant **type
metahint** (i.e., subscription of the :attr:`typing.Annotated` singleton).
Expand Down
5 changes: 4 additions & 1 deletion beartype/cave.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,10 @@
from io import IOBase as _IOBase
from typing import (
Any as _Any,
Dict as _Dict,
Union as _Union,
Tuple as _Tuple,
Type as _Type,
)
from weakref import (
ref as _ref,
Expand Down Expand Up @@ -1230,7 +1233,7 @@ class _UnavailableTypesTuple(tuple):
'''

# ....................{ TUPLES ~ core }....................
NoneTypeOr = _NoneTypeOrType()
NoneTypeOr: _Any = _NoneTypeOrType()
'''
**:class:``NoneType`` tuple factory** (i.e., dictionary mapping from arbitrary
types or tuples of types to the same types or tuples of types concatenated with
Expand Down
12 changes: 7 additions & 5 deletions beartype/meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,12 @@ def _convert_version_str_to_tuple(version_str: str) -> Tuple[int, ...]:

# ....................{ METADATA ~ libs : test }....................
LIBS_TESTTIME_MANDATORY_TOX = (
# A relatively modern version of pytest is required.
# A *VERY* modern version of mypy is recommended. Even fairly recent older
# versions of mypy are significantly deficient with respect to error
# reporting to the point of uselessness.
'mypy >=0.800',

# A fairly modern version of pytest is required.
'pytest >=4.0.0',
)
'''
Expand All @@ -296,10 +301,7 @@ def _convert_version_str_to_tuple(version_str: str) -> Tuple[int, ...]:
'''


LIBS_TESTTIME_OPTIONAL = (
# A relatively modern version of mypy is recommended.
'mypy >=0.790',
)
LIBS_TESTTIME_OPTIONAL = ()
'''
**Optional developer test-time package dependencies** (i.e., dependencies
recommended to test this package with :mod:`tox` as a developer at the command
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
# WARNING: To raise human-readable test errors, avoid importing from
# package-specific submodules at module scope.
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
from beartype_test.util.mark.pytskip import skip #skip_unless_package
from beartype_test.util.mark.pytskip import skip_if_pypy, skip_unless_package

# ....................{ TESTS }....................
#FIXME: Consider submitting as a StackOverflow post. Dis iz l33t, yo!
Expand All @@ -37,14 +37,12 @@
# accept the importability of the "mypy" package as sufficient, which it
# absolutely isn't, but what you gonna do, right?

#FIXME: Uncomment after mypy eventually passes. *sigh*
#FIXME: We additionally want to add some sort of GitHub Action for mypy to our
#"python_test.yml" CI configuration to ensure that both pushes and PRs are
#linted against mypy. Getting this to work has been a considerable burden, so
#let's try to keep this working as long as feasible, eh?

# @skip_unless_package('mypy')
@skip('beartype currently PEP 561-noncompliant and thus fails mypy.')
# If the active Python interpreter is PyPy, avoid this mypy-specific functional
# test. mypy is currently incompatible with PyPy for inscrutable reasons that
# should presumably be fixed at some future point. See also:
# https://mypy.readthedocs.io/en/stable/faq.html#does-it-run-on-pypy
@skip_unless_package('mypy')
@skip_if_pypy()
def test_pep561_mypy() -> None:
'''
Functional test testing this project's compliance with `PEP 561`_ by
Expand All @@ -60,16 +58,22 @@ def test_pep561_mypy() -> None:
from beartype_test.util.path.pytpathproject import get_project_package_dir
from mypy import api

# Tuple of all command-line options (i.e., "-"-prefixed strings) to be
# List of all command-line options (i.e., "-"-prefixed strings) to be
# effectively passed to the external "mypy" command.
MYPY_OPTIONS = ()
#
# Note this iterable *MUST* be defined as a list rather than type. If *NOT*
# the case, the function called below raises an exception. Hot garbage!
MYPY_OPTIONS = []

# Tuple of all command-line arguments (i.e., non-options) to be effectively
# List of all command-line arguments (i.e., non-options) to be effectively
# passed to the external "mypy" command.
MYPY_ARGUMENTS = (
#
# Note this iterable *MUST* be defined as a list rather than type. If *NOT*
# the case, the function called below raises an exception. Hot garbage!
MYPY_ARGUMENTS = [
# Absolute dirname of this project's top-level package.
str(get_project_package_dir()),
)
]

# Tuple of all command-line options to be effectively passed to the
# external "mypy" command.
Expand Down
2 changes: 1 addition & 1 deletion beartype_test/util/mark/pytskip.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ def skip_if_pypy():
from beartype._util.py.utilpyinterpreter import IS_PYPY

# Skip this test if the active Python interpreter is PyPy.
return skip_if(IS_PYPY, reason='PyPy.')
return skip_if(IS_PYPY, reason='Incompatible with PyPy.')


def skip_if_python_version_less_than(minimum_version: str):
Expand Down
5 changes: 4 additions & 1 deletion mypy
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@ script_dirname="$(dirname "${script_filename}")"
pushd "${script_dirname}" >/dev/null

# Statically type-check this project's codebase with all passed arguments.
command python3 -m mypy "${@}" beartype
command python3 -m mypy "${@}"
# command python3.6 -m mypy "${@}"
# command python3.8 -m mypy "${@}"
# command python3.9 -m mypy "${@}"

# 0-based exit code reported by the prior command.
exit_code=$?
Expand Down

0 comments on commit f7ec6e8

Please sign in to comment.