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 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264
|
# Usage
The library is divided into two submodules:
- [`typing_inspection.typing_objects`][]: provides functions to check if a variable is a [`typing`][] object:
```python
from typing_extensions import Literal, get_origin
from typing_inspection.typing_objects import is_literal
is_literal(get_origin(Literal[1, 2])) # True
```
!!! note
You might be tempted to use a simple identity check:
```pycon
>>> get_origin(Literal[1, 2]) is typing.Literal
```
However, [`typing_extensions`][] might provide a different version of the [`typing`][] objects. Instead,
the [`typing_objects`][typing_inspection.typing_objects] functions make sure to check against both variants,
if they are different.
- [`typing_inspection.introspection`][]: provides high-level introspection functions, taking runtime edge cases
into account.
## Inspecting annotations
If, as a library, you rely heavily on type hints, you may encounter subtle unexpected behaviors and performance
issues when inspecting annotations. As such, this section provides a recommended workflow to do so.
### Fetching type hints
The first step is to gather the type annotations from the object you want to inspect. The
[`typing.get_type_hints()`][typing.get_type_hints] function can be used to do so. If you want to make use of annotated
metadata, make sure to set the `include_extras` argument to `True`.
```pycon
>>> class A:
... x: int
... y: Annotated[int, ...]
...
>>> get_type_hints(A, include_extras=True)
{'x': int, 'y': Annotated[int, ...]}
```
!!! note
Currently, `typing-inspection` does not provide any utility to fetch (and evaluate) type annotations. The current
[`typing`][] utilities might contain subtle bugs across the different Python versions, so there is value in
having similar functionality. It might be best to wait for [PEP 649](https://peps.python.org/pep-0649/) to be fully
implemented first. In the meanwhile, the [`typing_extensions.get_type_hints()`][typing_extensions.get_type_hints]
backport can be used.
### Unpacking metadata and qualifiers
The annotations fetched in the previous step are called [annotation expressions][annotation expression].
An annotation expression is a [type expression][], optionally surrounded by one or more [type qualifiers][type qualifier]
or by the [`Annotated`][typing.Annotated] form.
For instance, in the following example:
```python
from typing import Annotated, ClassVar
class A:
x: ClassVar[Annotated[int, "meta"]]
```
The type hint of `x` is an annotation expression. The underlying type expression is `int`. It is wrapped
by the [`ClassVar`][typing.ClassVar] type qualifier, and the [`Annotated`][typing.Annotated] [special form][].
The goal of this step is to:
- Unwrap the underlying [type expression][].
- Keep track of the type qualifiers and annotated metadata.
To unwrap the type hint, use the [`inspect_annotation()`][typing_inspection.introspection.inspect_annotation] function:
```pycon
>>> from typing_inspection.introspection import AnnotationSource, inspect_annotation
>>> inspect_annotation(
... ClassVar[Annotated[int, "meta"]],
... annotation_source=AnnotationSource.CLASS,
... )
...
InspectedAnnotation(type=int, qualifiers={"class_var"}, metadata=["meta"])
```
Note that depending on the annotation source, different type qualifiers can be (dis)allowed.
For instance, [`TypedDict`][typing.TypedDict] classes allow [`Required`][typing.Required] and [`NotRequired`][typing.NotRequired],
which are not allowed elsewhere (the allowed typed qualifiers are documented in the
[`AnnotationSource`][typing_inspection.introspection.AnnotationSource] enum class).
A [ForbiddenQualifier][typing_inspection.introspection.ForbiddenQualifier] exception is raised if an invalid qualifier is used.
If you want to allow all of them, use the [`AnnotationSource.ANY`][typing_inspection.introspection.AnnotationSource.ANY] annotation
source.
The result of the [`inspect_annotation()`][typing_inspection.introspection.inspect_annotation] function contains the underlying
[type expression][], the qualifiers and the annotated metadata.
#### Handling bare type qualifiers
Note that some qualifiers are allowed to be used without any
type expression. In this case, the [`InspectedAnnotation.type`][typing_inspection.introspection.InspectedAnnotation.type] attribute
will take the value of the [`UNKNOWN`][typing_inspection.introspection.UNKNOWN] sentinel.
Depending on the type qualifier that was used, you can infer the actual type in different ways:
```python
from typing import get_type_hints
from typing_inspection.introspection import UNKNOWN, AnnotationSource, inspect_annotation
class A:
# For `Final` annotations, the type should be inferred from the assignment
# (and you may error if no assignment is available).
# In this case, you can infer to either `int` or `Literal[1]`:
x: Annotated[Final, 'meta'] = 1
# For `ClassVar` annotations, the type can be inferred as `Any`,
# or from the assignment if available (both options are valid in all cases):
y: ClassVar
inspected_annotation = inspect_annotation(
get_type_hints(A)['x'],
annotation_source=AnnotationSource.CLASS,
)
if inspected_annotation.type is UNKNOWN:
ann_type = type(A.x)
else:
ann_type = inspected_annotation.type
```
!!! note "Parsing [PEP 695](https://peps.python.org/pep-0695/) type aliases"
In Python 3.12, the new [type][] statement can be used to define [type aliases][type-aliases].
When a type alias is wrapped by the [`Annotated`][typing.Annotated] form, the type alias' value will *not* be unpacked by Python
at runtime. This means that while the following is technically valid:
```python
type MyInt = Annotated[int, "int_meta"]
class A:
x: Annotated[MyInt, "other_meta"]
```
it might be necessary to parse the type alias during annotation inspection. This behavior can be controlled using the
`unpack_type_aliases` parameter:
```pycon
>>> inspect_annotation(
... Annotated[MyInt, "other_meta"],
... annotation_source=AnnotationSource.CLASS,
... unpack_type_aliases="eager",
... )
...
InspectedAnnotation(type=int, qualifiers={}, metadata=["int_meta", "other_meta"])
```
Whether you should unpack type aliases depends on your use case. If the annotated metadata present in the type alias
is *only* meant to be applied on the annotated type (and not the attribute that will be type hinted), you probably
need to keep type aliases as is, and possibly error later if invalid metadata is found when inspecting the type alias.
Note that type aliases are lazily evaluated. During type alias inspection, any undefined symbol
will raise a [`NameError`][]. To prevent this from happening, you can use `'skip'` to avoid expanding
type aliases (the default), or `'lenient'` to fallback to `'skip'` if the type alias contains an undefined
symbol:
```pycon
>>> type BrokenType = Annotated[Undefined, ...]
>>> type MyAlias = Annotated[BrokenType, "meta"]
>>> inspect_annotation(
... MyAlias,
... annotation_source=AnnotationSource.CLASS,
... unpack_type_aliases="lenient",
... )
...
InspectedAnnotation(type=BrokenType, qualifiers={}, metadata=["meta"])
```
### Inspecting the type expression
With the qualifiers and [`Annotated`][typing.Annotated] forms removed, we can now proceed to inspect
the type expression.
First of all, some simple typing [special forms][special form] can be checked:
```python
from typing_inspection.typing_objects import is_any, is_self
# This would come from `InspectedAnnotation.type`, after checking for `INFERRED`:
type_expr = ...
if is_any(type_expr):
... # Handle `typing.Any`
if is_self(type_expr):
... # Handle `typing.Self`
```
We will then use the [`typing.get_origin()`][typing.get_origin] function to fetch the origin of the type. Depending
on the type, the origin has different meanings:
```python
from typing_inspection.introspection import get_literal_values, is_union_origin
from typing_inspection.typing_objects import is_annotated, is_literal
origin = get_origin(type_expr)
if is_union_origin(origin):
# Handle `typing.Union` (or the new `|` syntax)
union_args = type_expr.__args__
...
# You may also want to check for Annotated forms. While we unwrapped them
# in step 2, `Annotated` can be used in parts of the annotation, e.g.
# `list[Annotated[int, ...]]`:
if is_annotated(origin):
annotated_type = type_expr.__origin__ # not to be confused with the origin above
metadata = type_expr.__metadata__
if is_literal(origin):
# Handle `typing.Literal`
literal_values = get_literal_values(type_expr)
```
While [`Literal`][typing.Literal] values can be retrieved using `type_expr.__args__`, the
[`get_literal_values()`][typing_inspection.introspection.get_literal_values] function ensures
[PEP 695](https://peps.python.org/pep-0695/) type aliases are properly expanded.
Next, we will take care of the typing aliases deprecated by [PEP 585](https://peps.python.org/pep-0585/).
For instance, [`typing.List`][] is deprecated and replaced by the built-in [`list`][] type. In this case,
the origin of an *unparameterized* deprecated type alias is the replacement type, so we will use this one:
```python
from typing_inspection.typing_objects import DEPRECATED_ALIASES
# If `type_expr` is `typing.List`, `origin` is the built-in `list`.
# We thus replace `type_expr` with `list`, and set `origin` to `None`
# to emulate the same behavior if `type_expr` was `list` in the beginning:
if origin is not None and type_expr in DEPRECATED_ALIASES:
type_expr = origin
origin = None
```
At this point, if `origin` is not `None`, you can safely assume that `type_expr` is a parameterized generic type.
You can then define your own logic to handle the type expression, and have different code paths if you are
dealing with a parameterized type (e.g. `list[int]`) or a "bare" type:
```python
if origin is not None:
handle_generic_type(type=origin, arguments=type_expr.__args__)
else:
handle_type(type=type_expr)
```
!!! note
If a deprecated type alias is *parameterized* (e.g. `typing.List[int]`), the origin will be the
replacement type (e.g. `list`), and not the deprecated alias (e.g. `typing.List`). This means
that handling `typing.List[int]` or `list` should be equivalent.
|