1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
|
.. _advanced-typing:
Static type checking explained
===============================
.. include:: ../substitutions.sub
Since an atom member is a Python descriptor in which the validation step is allowed
to perform a type conversion (ex with `Coerced`), the types may be different when
reading and writing the member value. Therefore, the type hint is logically generic
over 2 types:
- the type that will be returned when accessing the member, which we will refer
to as the getter or read type `T`
- the type that the member can be set to, which we will refer to as the setter
or write type `S`
In general, the type hints shipped with Atom are sufficient to narrow down the type
of the members without requiring any manual annotation. For example:
.. code-block::
class MyAtom(Atom):
i = Int()
i_f = Int(strict=False)
l = List(int)
d = Dict((str, bytes), List(int))
will be typed as something equivalent to:
.. code-block::
class MyAtom(Atom):
i: Member[int, int]
i_f: Member[int, float]
l: Member[TList[int], TList[int]]
d: Member[TDict[Union[str, bytes], TList[int]], TDict[Union[str, bytes], TList[int]]]
.. note::
Since many member names conflict with name found in the typing module we will
add a leading `T` to types coming from typing. However in real code we recommend
rather aliasing the Atom members with a leading `A` as illustrated in the next
example. Note that starting with Python 3.9 generic aliases allow to directly use
list, dict, set in type annotations which avoids conflicts.
However, in some cases, static typing can be more strict than Atom validation such as
for tuples and we may not want to validate at runtime the size of the tuple (even
though it may be a good idea to do so).
.. code-block::
from typing import Tuple
from Atom.api import Atom, Tuple as ATuple
class MyAtom(Atom):
t: "Member[Tuple[int, int], Tuple[int, int]]" = ATuple[int] # type: ignore
Let's walk through this case.
.. code-block::
from typing import Tuple
from Atom.api import Atom, Tuple as ATuple
First, since Atom and typing share many names, one must be careful to disambiguate the
names. Starting with Python 3.9, one can use generic aliases to limit the conflicts
by using native types rather than typing classes.
.. code-block::
class MyAtom(Atom):
t: "Member[Tuple[int, int], Tuple[int, int]]" = ATuple[int] # type: ignore
Here we want to specify, that our tuple member is expected to store 2-tuple of int.
Since Atom does not enforce the length of a tuple, its type hint looks like
`Member[Tuple[T, ...], Tuple[T, ...]]` and makes the assumption that no fancy
type conversion occurs. If we want to go further we need a type hint and this is where
things get a bit more complicated.
Member is actually defined in C and does not inherit from Protocol. However,
atom members implement `__getitem__` allowing the the use of generic aliases.
.. note::
If the line becomes too long it can be split on multiple lines as follows:
.. code-block::
class MyAtom(Atom):
t: "Member[Tuple[int, int], Tuple[int, int]]"
t = ATuple[int] # type: ignore
Similarly if one implements custom member subclasses and desire to make it compatible
with type annotations, one can define the type hint as follow:
.. code-block::
class MyMember(Member[int, str]): ...
.. note::
The above is valid outside of a .pyi file only under Python 3.9+.
.. note::
One can use types from the ``typing`` module or generic aliases in any place
where a type or a tuple of type is expected. Note however that when
using ``typing.List[int]`` or ``list[T]``, etc in such a position the content
of the container will not be validated at runtime by Atom.
``Optional``, ``Union`` and ``Callable`` from the ``typing`` module can also be
used, however because they are not seen as proper types by type checkers this
will break static type checking. The recommended workaround is to use ``Typed``
or ``Instance`` as appropriate for the first two cases and a separate annotation
for the ``typing.Callable`` case.
Member typing in term of Member[T, S]
-------------------------------------
Below we give the typing of most Atom member in term of Member to clarify the behavior
of each member with respect to typing. We also indicate their default typing, but please
note that the presence/value of some argument at the member creation will influence the
inferred type.
.. code-block::
Value[T] = Member[T, T] # default: Value[Any]
Constant[T] = Member[T, NoReturn] # default: Constant[Any]
ReadOnly[T] = Member[T, T] # default: ReadOnly[Any]
Callable[T] = Member[T, T] # default: Callable[TCallable]
Bool[S] = Member[bool, S] # default: Bool[bool]
Int[S] = Member[int, S] # default: Int[int]
Float[S] = Member[float, S] # default: Float[float]
Range[S] = Member[int, S] # default: Range[int]
FloatRange[S] = Member[float, S] # default: FloatRange[float]
Bytes[S] = Member[bytes, S] # default: Bytes[Union[bytes, str]]
Str[S] = Member[str, S] # default: Str[Union[bytes, str]]
List[T] = Member[TList[T], TList[T]]
# List() -> List[Any]
# List(int) -> List[int]
# List(List(int)) -> List[TList[int]]
Set[T] = Member[TSet[T], TSet[T]]
# Set() -> Set[Any]
# Set(int) -> Set[int]
Dict[KT, VT] = Member[TDict[KT, VT], TDict[KT, VT]]
# Dict() -> Dict[Any, Any]
# Dict(str, int) -> Dict[str, int]
# Dict(str, List[int]) -> Dict[str, TList[int]]
Typed[T] = Member[T, T]
# Typed(int) -> Typed[Optional[int]]
# Typed(int, optional=False) -> Typed[int]
ForwardTyped[T] = Member[T, T]
Instance[T] = Member[T, T]
ForwardInstance[T] = Member[T, T]
.. note::
All members that can take a tuple of types as argument (List, Dict, etc) have type
hints for up to a tuple of 3 types as argument. Supporting more types would make
type checking even slower, so we suggest using manual annotation.
Finally the case of |Coerced| is a bit special, since we cannot teach type checkers to
see a type both as a type and a callable. As a consequence for type checking to be
correct when the type itself handle the coercion the type should be manually specified
as coercer::
c = Coerced(int, coercer=int)
|