Skip to content

Commit

Permalink
Fixed-length tuple type hint disambiguation x 3.
Browse files Browse the repository at this point in the history
This commit is the last in a commit chain disambiguating fixed-length
tuple type hints of the form `tuple[{child_hint_1}, ...,
{child_hint_N}]` from variable-length tuple type hints of the form
`tuple[{child_hint}, ...]` across the @beartype codebase in a more
elegant manner. Although @beartype currently (of course) disambiguates
these two kinds of tuple type hints, it does rather inelegantly in a
manner breaking @leycec's head. As a beneficial side effect of this
refactoring, the `beartype.door.TypeHint` API now explicitly
disambiguates between fixed- and variable-length tuple type hints by
creating and returning unambiguous instances of either:

* The new public `beartype.door.TupleFixedTypeHint` subclass when
  instantiating `beartype.door.TypeHint` with a fixed-length tuple
  type hint: e.g.,

  ```python
  >>> from beartype.door import TypeHint
  >>> repr(TypeHint(tuple[int, str]))
  'TupleFixedTypeHint(tuple[int, str])'
  ```

* The new public `beartype.door.TupleVariableTypeHint` subclass when
  instantiating `beartype.door.TypeHint` with a variable-length tuple
  type hint: e.g.,

  ```python
  >>> from beartype.door import TypeHint
  >>> repr(TypeHint(tuple[int, ...]))
  'TupleVariableTypeHint(tuple[int, ...])'
  ```

Beartype: *Open the door to a whole new typing world.* Cue Disney music.
(*Indubitable doubt is industriously dusty!*)
  • Loading branch information
leycec committed Jun 11, 2024
1 parent ffc127c commit 465da19
Show file tree
Hide file tree
Showing 16 changed files with 130 additions and 231 deletions.
8 changes: 4 additions & 4 deletions beartype/_check/code/codemake.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,6 @@
from beartype._util.hint.pep.utilpeptest import (
die_if_hint_pep_unsupported,
is_hint_pep,
is_hint_pep_args,
)
from beartype._util.hint.utilhinttest import is_hint_ignorable
from beartype._util.kind.map.utilmapset import update_mapping
Expand Down Expand Up @@ -764,16 +763,16 @@ def _enqueue_hint_child(pith_child_expr: str) -> str:
# type-checked against that type *AND is either...
hint_curr_sign in HINT_SIGNS_ORIGIN_ISINSTANCEABLE and (
# Unsubscripted *OR*...
not is_hint_pep_args(hint_curr) or
#FIXME: Remove this branch *AFTER* deeply supporting all
#hints.
not get_hint_pep_args(hint_curr) or
# Currently unsupported with deep type-checking...
hint_curr_sign not in HINT_SIGNS_SUPPORTED_DEEP
)
):
# Then generate trivial code shallowly type-checking the current
# pith as an instance of the origin type originating this sign
# (e.g., "list" for the hint "typing.List[int]").
# print(f'Shallow checking unsubscripted hint {repr(hint_curr)}...')

# Code type-checking the current pith against this origin type.
func_curr_code = CODE_PEP484_INSTANCE_format(
pith_curr_expr=pith_curr_expr,
Expand Down Expand Up @@ -1414,6 +1413,7 @@ def _enqueue_hint_child(pith_child_expr: str) -> str:
exception_prefix=EXCEPTION_PREFIX,
)
)
# print(f'Sanifying sequence hint {repr(hint_curr)} child hint {repr(hint_child)}...')

# Unignorable sane child hint sanified from this possibly
# ignorable insane child hint *OR* "None" otherwise (i.e.,
Expand Down
2 changes: 2 additions & 0 deletions beartype/_check/convert/convreduce.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
HintSignSet,
HintSignSized,
HintSignTuple,
HintSignTupleFixed,
HintSignType,
HintSignTypeAlias,
HintSignTypeGuard,
Expand Down Expand Up @@ -623,6 +624,7 @@ def reduce_hint_pep{pep_number}(
HintSignSet: reduce_hint_pep484_deprecated,
HintSignSized: reduce_hint_pep484_deprecated,
HintSignTuple: reduce_hint_pep484_deprecated,
HintSignTupleFixed: reduce_hint_pep484_deprecated,
HintSignType: reduce_hint_pep484_deprecated,
HintSignValuesView: reduce_hint_pep484_deprecated,

Expand Down
8 changes: 2 additions & 6 deletions beartype/_check/error/_errcause.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,7 @@
get_hint_pep_args,
get_hint_pep_sign,
)
from beartype._util.hint.pep.utilpeptest import (
is_hint_pep,
is_hint_pep_args,
)
from beartype._util.hint.pep.utilpeptest import is_hint_pep
from beartype._check.convert.convsanify import (
sanify_hint_child_if_unignorable_or_none)

Expand Down Expand Up @@ -412,8 +409,7 @@ def find_cause(self) -> 'ViolationCause':
# type-checked against that type *AND is either...
self.hint_sign in HINT_SIGNS_ORIGIN_ISINSTANCEABLE and (
# Unsubscripted *OR*...
not is_hint_pep_args(self.hint) or
#FIXME: Remove this branch *AFTER* deeply supporting all hints.
not get_hint_pep_args(self.hint) or
# Currently unsupported with deep type-checking...
self.hint_sign not in HINT_SIGNS_SUPPORTED_DEEP
)
Expand Down
5 changes: 1 addition & 4 deletions beartype/_check/error/_pep/pep484585/errpep484585sequence.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,7 @@
'''

# ....................{ IMPORTS }....................
from beartype._data.hint.pep.sign.datapepsigns import (
HintSignTuple,
HintSignTupleFixed,
)
from beartype._data.hint.pep.sign.datapepsigns import HintSignTupleFixed
from beartype._data.hint.pep.sign.datapepsignmap import (
HINT_SIGN_ORIGIN_ISINSTANCEABLE_TO_ARGS_LEN_RANGE)
from beartype._data.hint.pep.sign.datapepsignset import (
Expand Down
10 changes: 10 additions & 0 deletions beartype/_data/hint/pep/sign/datapepsignmap.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class to various metadata associated with categories of type hints).
HintSignAsyncIterator,
HintSignAsyncIterable,
HintSignAwaitable,
# HintSignByteString,
HintSignChainMap,
HintSignCollection,
HintSignContainer,
Expand Down Expand Up @@ -61,6 +62,14 @@ class to various metadata associated with categories of type hints).
# * The final "stop" integer is *EXCLUSIVE* (i.e., the instantiated range
# excludes this integer).

_ARGS_LEN_0 = range(0, 1) # == [0, 1) == [0, 0]
'''
**Zero-argument length range** (i.e., :class:`range` instance effectively
equivalent to the integer ``0``, describing type hint factories subscriptable by
*no* child type hints).
'''


_ARGS_LEN_1 = range(1, 2) # == [1, 2) == [1, 1]
'''
**One-argument length range** (i.e., :class:`range` instance effectively
Expand Down Expand Up @@ -100,6 +109,7 @@ class to various metadata associated with categories of type hints).
HintSignAsyncIterable: _ARGS_LEN_1,
HintSignAsyncIterator: _ARGS_LEN_1,
HintSignAwaitable: _ARGS_LEN_1,
# HintSignByteString: _ARGS_LEN_1,
HintSignCollection: _ARGS_LEN_1,
HintSignContainer: _ARGS_LEN_1,
HintSignCounter: _ARGS_LEN_1,
Expand Down
21 changes: 16 additions & 5 deletions beartype/_data/hint/pep/sign/datapepsignset.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,6 @@

HINT_SIGNS_SEQUENCE_ARGS_1: _FrozenSetHintSign = frozenset((
# ..................{ PEP (484|585) }..................
HintSignByteString,
HintSignList,
HintSignMutableSequence,
HintSignSequence,
Expand All @@ -310,10 +309,21 @@
:class:`bytes` types as its sole subscripted argument, which does *not*
unconditionally constrain *all* items (i.e., unencoded and encoded characters
respectively) of compliant sequences but instead parametrizes this attribute.
* :obj:`typing.ByteString` sign, which accepts *no* subscripted arguments.
:obj:`typing.ByteString` is simply an alias for the
:class:`collections.abc.ByteString` abstract base class (ABC) and thus
already handled by our fallback logic for supported PEP-compliant type hints.
* :obj:`typing.ByteString` sign, which conditionally accepts either no or an
arbitrary number of subscripted arguments depending on whether that sign
identifies:
* A :pep:`484`-compliant ``typing.ByteString`` type hint subscriptable *no*
child type hints.
* A :pep:`585`-compliant ``collections.abc.ByteString[...]`` type hint
subscriptable by an arbitrary number of child type hints (but typically
simply :class:`str`).
Since neither PEP 484 nor 585 comment on ``ByteString`` in detail (or at all,
really), this non-orthogonality remains inexplicable, frustrating, and utterly
unsurprising. We elect to merely shrug. In all likelihood, this is an
ignorable error that no one particularly cares about -- especially since both
type hint factories have now been scheduled for removal as deprecated.
* :obj:`typing.Deque` sign, whose compliant objects (i.e.,
:class:`collections.deque` instances) only `guarantee O(n) indexation across
all sequence items <collections.deque_>`__:
Expand Down Expand Up @@ -583,6 +593,7 @@

# ..................{ PEP (484|585) }..................
HintSignGeneric,
HintSignTupleFixed,
HintSignType,

# ..................{ PEP 544 }..................
Expand Down
5 changes: 3 additions & 2 deletions beartype/_data/kind/datakindsequence.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@
**Empty tuple singleton.**
Yes, we know exactly what you're thinking: "Why would anyone do this, @leycec?
Why not just directly access the empty tuple singleton as ()?" Because Python
insanely requires us to do this under Python >= 3.8 to detect empty tuples:
Why not just directly access the empty tuple singleton as ``()``?" Because
Python insanely requires us to do this under Python >= 3.8 to detect empty
tuples:
.. code-block:: bash
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ class usually produces a generic non-class that *must* nonetheless be
See Also
--------
:func:`beartype._util.hint.pep.utilpeptest.is_hint_pep_typevars`
:func:`beartype._util.hint.pep.utilpepget.get_hint_pep_typevars`
Commentary on the relation between generics and parametrized hints.
'''

Expand Down
75 changes: 71 additions & 4 deletions beartype/_util/hint/pep/utilpepget.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,9 @@ def get_hint_pep_args(hint: object) -> tuple:
# continued to correctly declare an "__args__" dunder attribute of
# "((),)" until Python 3.11.
#
# Disambiguate these two cases on behalf of callers by returning a tuple
# containing only the empty tuple rather than returning the empty tuple.
# Disambiguate these two cases on behalf of callers by returning a
# 1-tuple containing only the empty tuple (i.e., "((),)") rather than
# returning the empty tuple (i.e., "()").
elif not hint_args:
return _HINT_ARGS_EMPTY_TUPLE
# Else, this hint is either subscripted *OR* is unsubscripted but not
Expand Down Expand Up @@ -141,7 +142,20 @@ def get_hint_pep_args(hint: object) -> tuple:
logic attempting to directly access this attribute. Thus this function,
which "fills in the gaps" by implementing this oversight.
**This getter never lies, unlike the comparable**
**This getter lies rarely due to subscription erasure** (i.e., the malicious
destruction of child type hints by parent type hint factories at
subscription time). Callers should not assume that the objects originally
subscripting this hint are still accessible. Although *most* hints preserve
their subscripted objects over their lifetimes, a small subset of edge-case
hints erase those objects at subscription time. This includes:
* :pep:`585`-compliant empty tuple type hints (i.e., ``tuple[()]``), which
despite being explicitly subscripted erroneously erase that subscription
at subscription time. This does *not* extend to :pep:`484`-compliant
empty tuple type hints (i.e., ``typing.Tuple[()]``), which correctly
preserve that subscripted empty tuple.
**This getter lies less than the comparable**
:func:`get_hint_pep_typevars` **getter.** Whereas
:func:`get_hint_pep_typevars` synthetically propagates type variables from
child to parent type hints (rather than preserving the literal type
Expand Down Expand Up @@ -234,6 +248,14 @@ def get_hint_pep_typevars(hint: object) -> TupleTypes:
hint declaration time ignoring duplicates) if any *or* the empty tuple
otherwise.
This getter correctly handles both:
* **Direct parametrizations** (i.e., cases in which this object itself is
directly parametrized by type variables).
* **Superclass parametrizations** (i.e., cases in which this object is
indirectly parametrized by one or more superclasses of its class being
directly parametrized by type variables).
This getter is intentionally *not* memoized (e.g., by the
:func:`callable_cached` decorator), as the implementation trivially reduces
to an efficient one-liner.
Expand All @@ -248,6 +270,31 @@ def get_hint_pep_typevars(hint: object) -> TupleTypes:
logic attempting to directly access this attribute. Thus this function,
which "fills in the gaps" by implementing this oversight.
**Generics** (i.e., PEP-compliant type hints whose classes subclass one or
more public :mod:`typing` pseudo-superclasses) are often but *not* always
typevared. For example, consider the untypevared generic:
.. code-block:: pycon
>>> from typing import List
>>> class UntypevaredGeneric(List[int]): pass
>>> UntypevaredGeneric.__mro__
(__main__.UntypevaredGeneric, list, typing.Generic, object)
>>> UntypevaredGeneric.__parameters__
()
Likewise, typevared hints are often but *not* always generic. For example,
consider the typevared non-generic:
.. code-block:: pycon
>>> from typing import List, TypeVar
>>> TypevaredNongeneric = List[TypeVar('T')]
>>> type(TypevaredNongeneric).__mro__
(typing._GenericAlias, typing._Final, object)
>>> TypevaredNongeneric.__parameters__
(~T,)
Parameters
----------
hint : object
Expand All @@ -262,17 +309,37 @@ def get_hint_pep_typevars(hint: object) -> TupleTypes:
value of that attribute.
* Else, the empty tuple.
Parameters
----------
hint : object
Object to be inspected.
Returns
-------
bool
:data:`True` only if this object is a PEP-compliant type hint
parametrized by one or more type variables.
Examples
--------
.. code-block:: python
.. code-block:: pycon
>>> import typing
>>> from beartype._util.hint.pep.utilpepget import (
... get_hint_pep_typevars)
>>> S = typing.TypeVar('S')
>>> T = typing.TypeVar('T')
>>> class UserList(typing.List[T]): pass
>>> get_hint_pep_typevars(typing.Any)
()
>>> get_hint_pep_typevars(typing.List[int])
()
>>> get_hint_pep_typevars(typing.List[T])
(T)
>>> get_hint_pep_typevars(UserList)
(T)
>>> get_hint_pep_typevars(typing.List[T, int, S, str, T])
(T, S)
'''
Expand Down
Loading

0 comments on commit 465da19

Please sign in to comment.