Skip to content

Commit

Permalink
PEP 647 x 6.
Browse files Browse the repository at this point in the history
This commit is the last in a commit chain adding support for PEP 647
(i.e., `typing.TypeGuard`), resolving feature request #221 kindly
submitted by Google X researcher extraordinaire @patrick-kidger.
Specifically, this commit:

* Integrates our recently defined low-level
  `beartype._util.hint.pep.proposal.utilpep647.reduce_hint_pep647()`
  reducer with the core high-level
  `beartype._check.conv.convreduce.reduce_hint()` reducer controller.
* Exhaustively unit tests this integration.

(*Absurdly surly abs!*)
  • Loading branch information
leycec committed Apr 6, 2023
1 parent bc32d63 commit 3d552ab
Show file tree
Hide file tree
Showing 15 changed files with 412 additions and 325 deletions.
16 changes: 14 additions & 2 deletions beartype/_check/conv/convreduce.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,15 @@
HintSignPanderaAny,
# HintSignTextIO,
HintSignType,
HintSignTypeGuard,
HintSignTypeVar,
HintSignTypedDict,
)
from beartype._util.cache.utilcachecall import callable_cached
from beartype._util.hint.nonpep.mod.utilmodnumpy import reduce_hint_numpy_ndarray
from beartype._util.hint.nonpep.mod.utilmodpandera import reduce_hint_pandera
from beartype._util.hint.nonpep.mod.utilmodnumpy import (
reduce_hint_numpy_ndarray)
from beartype._util.hint.nonpep.mod.utilmodpandera import (
reduce_hint_pandera)
from beartype._util.hint.pep.proposal.pep484.utilpep484 import (
reduce_hint_pep484_none)
from beartype._util.hint.pep.proposal.pep484.utilpep484generic import (
Expand All @@ -59,6 +62,7 @@
from beartype._util.hint.pep.proposal.utilpep589 import reduce_hint_pep589
from beartype._util.hint.pep.proposal.utilpep591 import reduce_hint_pep591
from beartype._util.hint.pep.proposal.utilpep593 import reduce_hint_pep593
from beartype._util.hint.pep.proposal.utilpep647 import reduce_hint_pep647
from beartype._util.hint.pep.utilpepget import get_hint_pep_sign_or_none
from beartype._util.hint.pep.utilpepreduce import reduce_hint_pep_unsigned
from collections.abc import Callable
Expand Down Expand Up @@ -240,6 +244,14 @@ def reduce_hint(
# lower-level hint it annotates.
HintSignAnnotated: reduce_hint_pep593,

# ..................{ PEP 647 }..................
# If this hint is a PEP 647-compliant "typing.TypeGuard[...]" type hint,
# either:
# * If this hint annotates the return of some callable, reduce this hint to
# the standard "bool" type.
# * Else, raise an exception.
HintSignTypeGuard: reduce_hint_pep647,

# ..................{ NON-PEP ~ numpy }..................
# If this hint is a PEP-noncompliant typed NumPy array (e.g.,
# "numpy.typing.NDArray[np.float64]"), reduce this hint to the equivalent
Expand Down
6 changes: 4 additions & 2 deletions beartype/_util/hint/pep/proposal/utilpep647.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ def reduce_hint_pep647(
'''
Reduce the passed :pep:`647`-compliant **type guard** (i.e.,
subscription of the :obj:`typing.TypeGuard` type hint factory) to the
builtin :class:`bool` class, as advised by :pep:`647` when performing
runtime type-checking.
builtin :class:`bool` class as advised by :pep:`647` when performing
runtime type-checking if this hint annotates the return of some callable
(i.e., if ``arg_name`` is ``"return"``) *or* raise an exception otherwise
(i.e., if this hint annotates the return of *no* callable).
This reducer is intentionally *not* memoized (e.g., by the
:func:`callable_cached` decorator), as the implementation trivially reduces
Expand Down
135 changes: 125 additions & 10 deletions beartype_test/a00_unit/a20_util/hint/a00_pep/proposal/test_utilpep544.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,62 @@
#!/usr/bin/env python3
# --------------------( LICENSE )--------------------
# --------------------( LICENSE )--------------------
# Copyright (c) 2014-2023 Beartype authors.
# See "LICENSE" for further details.

'''
**Beartype** :pep:`544` **type hint utility unit tests.**
**Beartype** :pep:`544` **utility unit tests.**
This submodule unit tests the public API of the private
:mod:`beartype._util.hint.pep.proposal.utilpep544` submodule.
'''

# ....................{ IMPORTS }....................
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# ....................{ IMPORTS }....................
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# WARNING: To raise human-readable test errors, avoid importing from
# package-specific submodules at module scope.
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

# ....................{ TESTS ~ tester }....................
# ....................{ TESTS ~ tester }....................
def test_is_hint_pep544_protocol() -> None:
'''
Test usage of the private
:mod:`beartype._util.hint.pep.proposal.utilpep544.is_hint_pep544_protocol`
tester.
'''

# ....................{ IMPORTS }....................
# Defer test-specific imports.
from beartype._util.hint.pep.proposal.utilpep544 import (
is_hint_pep544_protocol)
from beartype._util.py.utilpyversion import IS_PYTHON_AT_LEAST_3_8
from beartype_test.a00_unit.data.data_type import Class
from beartype_test.a00_unit.data.data_type import (
TYPES_BUILTIN,
Class,
)

# Intentionally import @beartype-unaccelerated protocols.
from typing import (
SupportsAbs,
SupportsBytes,
SupportsComplex,
SupportsFloat,
SupportsInt,
SupportsRound,
Union,
)

# ....................{ LOCALS }....................
# Set of all PEP 544-compliant "typing" protocols.
TYPING_PROTOCOLS = {
SupportsAbs,
SupportsBytes,
SupportsComplex,
SupportsFloat,
SupportsInt,
SupportsRound,
}

# ....................{ PASS }....................
# Assert this tester rejects builtin types erroneously presenting themselves
# as PEP 544-compliant protocols under Python >= 3.8.
assert is_hint_pep544_protocol(int) is False
Expand All @@ -43,6 +70,22 @@ def test_is_hint_pep544_protocol() -> None:
# assert not issubclass(Yam, Protocol)
# assert is_hint_pep544_protocol(Yam) is False

# Assert this tester accepts these classes *ONLY* if the active Python
# interpreter targets at least Python >= 3.8 and thus supports PEP 544.
for typing_protocol in TYPING_PROTOCOLS:
assert is_hint_pep544_protocol(typing_protocol) is (
IS_PYTHON_AT_LEAST_3_8)

# Assert this tester rejects all builtin types. For unknown reasons, some
# but *NOT* all builtin types (e.g., "int") erroneously present themselves
# to be PEP 544-compliant protocols. *sigh*
for class_builtin in TYPES_BUILTIN:
assert is_hint_pep544_protocol(class_builtin) is False

# Assert this tester rejects standard type hints in either case.
assert is_hint_pep544_protocol(Union[int, str]) is False

# ....................{ VERSION }....................
# If the active Python interpreter targets at least Python >= 3.8 and thus
# supports PEP 544...
if IS_PYTHON_AT_LEAST_3_8:
Expand All @@ -55,9 +98,6 @@ def test_is_hint_pep544_protocol() -> None:
runtime_checkable,
)

# Intentionally import @beartype-unaccelerated protocols.
from typing import SupportsInt

# User-defined protocol parametrized by *NO* type variables declaring
# arbitrary concrete and abstract methods.
@runtime_checkable
Expand All @@ -73,3 +113,78 @@ def omega(self) -> str: pass

# Assert this accepts a user-defined protocol.
assert is_hint_pep544_protocol(ProtocolCustomUntypevared) is True


def test_is_hint_pep544_io_generic() -> None:
'''
Test the
:func:`beartype._util.hint.pep.proposal.utilpep544.is_hint_pep484_generic_io`
tester.
'''

# Defer test-specific imports.
from beartype._util.hint.pep.proposal.utilpep544 import (
is_hint_pep484_generic_io)
from beartype._util.py.utilpyversion import IS_PYTHON_AT_LEAST_3_8
from beartype_test.a00_unit.data.hint.pep.proposal.data_pep484 import (
PEP484_GENERICS_IO)
from typing import Union

# Assert this tester accepts these classes *ONLY* if the active Python
# interpreter targets at least Python >= 3.8 and thus supports PEP 544.
for pep484_generic_io in PEP484_GENERICS_IO:
assert is_hint_pep484_generic_io(pep484_generic_io) is (
IS_PYTHON_AT_LEAST_3_8)

# Assert this tester rejects standard type hints in either case.
assert is_hint_pep484_generic_io(Union[int, str]) is False

# ....................{ TESTS ~ getters }....................
def test_get_hint_pep544_io_protocol_from_generic() -> None:
'''
Test the
:func:`beartype._util.hint.pep.proposal.utilpep544.reduce_hint_pep484_generic_io_to_pep544_protocol`
tester.
'''

# Defer test-specific imports.
from beartype.roar import BeartypeDecorHintPep544Exception
from beartype._util.hint.pep.proposal.utilpep544 import (
reduce_hint_pep484_generic_io_to_pep544_protocol)
from beartype._util.hint.pep.proposal.utilpep593 import is_hint_pep593
from beartype._util.py.utilpyversion import IS_PYTHON_AT_LEAST_3_8
from beartype_test.a00_unit.data.hint.pep.proposal.data_pep484 import (
PEP484_GENERICS_IO)
from pytest import raises
from typing import Union

# For each PEP 484-compliant "typing" IO generic base class...
for pep484_generic_io in PEP484_GENERICS_IO:
# If the active Python interpreter targets at least Python >= 3.8 and
# thus supports PEP 544...
if IS_PYTHON_AT_LEAST_3_8:
# Defer version-dependent imports.
from typing import Protocol

# Equivalent protocol reduced from this generic.
pep544_protocol_io = (
reduce_hint_pep484_generic_io_to_pep544_protocol(
pep484_generic_io, ''))

# Assert this protocol is either...
assert (
# A PEP 593-compliant type metahint generalizing a protocol
# *OR*...
is_hint_pep593(pep544_protocol_io) or
# A PEP 544-compliant protocol.
issubclass(pep544_protocol_io, Protocol)
)
# Else, assert this function raises an exception.
else:
with raises(BeartypeDecorHintPep544Exception):
reduce_hint_pep484_generic_io_to_pep544_protocol(
pep484_generic_io, '')

# Assert this function rejects standard type hints in either case.
with raises(BeartypeDecorHintPep544Exception):
reduce_hint_pep484_generic_io_to_pep544_protocol(Union[int, str], '')
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,10 @@
# See "LICENSE" for further details.

'''
**Beartype** `PEP 561`_ **unit tests.**
**Beartype** :pep:`561` **utility unit tests.**
This submodule unit tests `PEP 561`_ support implemented in the :mod:`beartype`
package.
.. _PEP 561:
https://www.python.org/dev/peps/pep-0561
This submodule unit tests :pep:`561` support implemented in the
:func:`beartype.beartype` decorator.
'''

# ....................{ IMPORTS }....................
Expand All @@ -22,18 +19,15 @@
# ....................{ TESTS }....................
def test_pep561_pytyped() -> None:
'''
Test `PEP 561`_ support implemented in the :mod:`beartype` package by
Test :pep:`561` support implemented in the :mod:`beartype` package by
asserting that this package provides the ``py.typed`` file required by
`PEP 561`_.
:pep:`561`.
Note that this unit test exercises a necessary but *not* sufficient
condition for this package to comply with `PEP 561`_. The comparable
condition for this package to comply with :pep:`561`. The comparable
:mod:`beartype_test.a90_func.pep.test_pep561` submodule defines a
functional test exercising the remaining necessary condition: **the
absence of static type-checking errors across this package.**
.. _PEP 561:
https://www.python.org/dev/peps/pep-0561
'''

# Defer test-specific imports.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,17 @@
# See "LICENSE" for further details.

'''
**Beartype** `PEP 585`_ **unit tests.**
**Beartype** :pep:`585` **utility unit tests.**
This submodule unit tests `PEP 585`_ support implemented in the
:func:`beartype.beartype` decorator.
.. _PEP 585:
https://www.python.org/dev/peps/pep-0585
This submodule unit tests the public API of the private
:mod:`beartype._util.hint.pep.proposal.utilpep585` submodule.
'''

# ....................{ IMPORTS }....................
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# WARNING: To raise human-readable test errors, avoid importing from
# package-specific submodules at module scope.
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
from pytest import raises

# ....................{ TESTS ~ kind : builtin }....................
def test_is_hint_pep585_builtin() -> None:
Expand Down Expand Up @@ -72,6 +68,7 @@ def test_get_hint_pep585_generic_typevars() -> None:
get_hint_pep585_generic_typevars)
from beartype_test.a00_unit.data.hint.pep.data_pep import (
HINTS_PEP_META)
from pytest import raises

# Assert this getter...
for hint_pep_meta in HINTS_PEP_META:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
#!/usr/bin/env python3
# --------------------( LICENSE )--------------------
# --------------------( LICENSE )--------------------
# Copyright (c) 2014-2023 Beartype authors.
# See "LICENSE" for further details.

'''
**Beartype** :pep:`586` **type hint utility unit tests.**
**Beartype** :pep:`586` **utility unit tests.**
This submodule unit tests the public API of the private
:mod:`beartype._util.hint.pep.proposal.utilpep586` submodule.
'''

# ....................{ IMPORTS }....................
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# ....................{ IMPORTS }....................
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# WARNING: To raise human-readable test errors, avoid importing from
# package-specific submodules at module scope.
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
from enum import Enum
from pytest import raises

# ....................{ CONSTANTS }....................
# ....................{ PRIVATE ~ constants }....................
#FIXME: Shift these into the unit test defined below, please.
class _Color(Enum):
'''
Arbitrary enumeration accessed as given in a prominent :pep:`586` example.
Expand Down Expand Up @@ -47,7 +48,7 @@ class _Color(Enum):
subsection.
'''

# ....................{ TESTS }....................
# ....................{ TESTS }....................
def test_is_hint_pep586() -> None:
'''
Test the
Expand All @@ -62,6 +63,11 @@ def test_is_hint_pep586() -> None:
die_unless_hint_pep586)
from typing import Optional

# Assert this validator raises the expected exception when passed an
# object that is *NOT* a literal.
with raises(BeartypeDecorHintPep586Exception):
die_unless_hint_pep586(Optional[str])

# If the active Python interpreter targets at least Python >= 3.9 and thus
# supports PEP 586...
if IS_PYTHON_AT_LEAST_3_9:
Expand Down Expand Up @@ -95,8 +101,3 @@ def test_is_hint_pep586() -> None:
with raises(BeartypeDecorHintPep586Exception):
die_unless_hint_pep586(Literal[
26, "hello world", b"hello world", True, object(), _Color.RED])

# Assert this validator raises the expected exception when passed an
# object that is *NOT* a literal.
with raises(BeartypeDecorHintPep586Exception):
die_unless_hint_pep586(Optional[str])
Loading

0 comments on commit 3d552ab

Please sign in to comment.