Update 2025-04-17_20:04:08
This commit is contained in:
@ -0,0 +1,548 @@
|
||||
"""High-level introspection utilities, used to inspect type annotations."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import types
|
||||
from collections.abc import Generator, Sequence
|
||||
from dataclasses import InitVar
|
||||
from enum import Enum, IntEnum, auto
|
||||
from typing import Any, Literal, NamedTuple, cast
|
||||
|
||||
from typing_extensions import TypeAlias, assert_never, get_args, get_origin
|
||||
|
||||
from . import typing_objects
|
||||
|
||||
__all__ = (
|
||||
'AnnotationSource',
|
||||
'ForbiddenQualifier',
|
||||
'InspectedAnnotation',
|
||||
'Qualifier',
|
||||
'get_literal_values',
|
||||
'inspect_annotation',
|
||||
'is_union_origin',
|
||||
)
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
|
||||
def is_union_origin(obj: Any, /) -> bool:
|
||||
"""Return whether the provided origin is the union form.
|
||||
|
||||
```pycon
|
||||
>>> is_union_origin(typing.Union)
|
||||
True
|
||||
>>> is_union_origin(get_origin(int | str))
|
||||
True
|
||||
```
|
||||
"""
|
||||
return typing_objects.is_union(obj) or obj is types.UnionType
|
||||
|
||||
else:
|
||||
|
||||
def is_union_origin(obj: Any, /) -> bool:
|
||||
"""Return whether the provided origin is the union form.
|
||||
|
||||
```pycon
|
||||
>>> is_union_origin(typing.Union)
|
||||
True
|
||||
>>> is_union_origin(get_origin(int | str))
|
||||
True
|
||||
```
|
||||
"""
|
||||
return typing_objects.is_union(obj)
|
||||
|
||||
|
||||
def _literal_type_check(value: Any, /) -> None:
|
||||
"""Type check the provided literal value against the legal parameters."""
|
||||
if (
|
||||
not isinstance(value, (int, bytes, str, bool, Enum, typing_objects.NoneType))
|
||||
and value is not typing_objects.NoneType
|
||||
):
|
||||
raise TypeError(f'{value} is not a valid literal value, must be one of: int, bytes, str, Enum, None.')
|
||||
|
||||
|
||||
def get_literal_values(
|
||||
annotation: Any,
|
||||
/,
|
||||
*,
|
||||
type_check: bool = False,
|
||||
unpack_type_aliases: Literal['skip', 'lenient', 'eager'] = 'eager',
|
||||
) -> Generator[Any]:
|
||||
"""Yield the values contained in the provided [`Literal`][typing.Literal] [special form][].
|
||||
|
||||
Args:
|
||||
annotation: The [`Literal`][typing.Literal] [special form][] to unpack.
|
||||
type_check: Whether to check if the literal values are [legal parameters][literal-legal-parameters].
|
||||
Raises a [`TypeError`][] otherwise.
|
||||
unpack_type_aliases: What to do when encountering [PEP 695](https://peps.python.org/pep-0695/)
|
||||
[type aliases][type-aliases]. Can be one of:
|
||||
|
||||
- `'skip'`: Do not try to parse type aliases. Note that this can lead to incorrect results:
|
||||
```pycon
|
||||
>>> type MyAlias = Literal[1, 2]
|
||||
>>> list(get_literal_values(Literal[MyAlias, 3], unpack_type_aliases="skip"))
|
||||
[MyAlias, 3]
|
||||
```
|
||||
|
||||
- `'lenient'`: Try to parse type aliases, and fallback to `'skip'` if the type alias can't be inspected
|
||||
(because of an undefined forward reference).
|
||||
|
||||
- `'eager'`: Parse type aliases and raise any encountered [`NameError`][] exceptions (the default):
|
||||
```pycon
|
||||
>>> type MyAlias = Literal[1, 2]
|
||||
>>> list(get_literal_values(Literal[MyAlias, 3], unpack_type_aliases="eager"))
|
||||
[1, 2, 3]
|
||||
```
|
||||
|
||||
Note:
|
||||
While `None` is [equivalent to][none] `type(None)`, the runtime implementation of [`Literal`][typing.Literal]
|
||||
does not de-duplicate them. This function makes sure this de-duplication is applied:
|
||||
|
||||
```pycon
|
||||
>>> list(get_literal_values(Literal[NoneType, None]))
|
||||
[None]
|
||||
```
|
||||
|
||||
Example:
|
||||
```pycon
|
||||
>>> type Ints = Literal[1, 2]
|
||||
>>> list(get_literal_values(Literal[1, Ints], unpack_type_alias="skip"))
|
||||
["a", Ints]
|
||||
>>> list(get_literal_values(Literal[1, Ints]))
|
||||
[1, 2]
|
||||
>>> list(get_literal_values(Literal[1.0], type_check=True))
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
TypeError: 1.0 is not a valid literal value, must be one of: int, bytes, str, Enum, None.
|
||||
```
|
||||
"""
|
||||
# `literal` is guaranteed to be a `Literal[...]` special form, so use
|
||||
# `__args__` directly instead of calling `get_args()`.
|
||||
|
||||
if unpack_type_aliases == 'skip':
|
||||
_has_none = False
|
||||
# `Literal` parameters are already deduplicated, no need to do it ourselves.
|
||||
# (we only check for `None` and `NoneType`, which should be considered as duplicates).
|
||||
for arg in annotation.__args__:
|
||||
if type_check:
|
||||
_literal_type_check(arg)
|
||||
if arg is None or arg is typing_objects.NoneType:
|
||||
if not _has_none:
|
||||
yield None
|
||||
_has_none = True
|
||||
else:
|
||||
yield arg
|
||||
else:
|
||||
# We'll need to manually deduplicate parameters, see the `Literal` implementation in `typing`.
|
||||
values_and_type: list[tuple[Any, type[Any]]] = []
|
||||
|
||||
for arg in annotation.__args__:
|
||||
# Note: we could also check for generic aliases with a type alias as an origin.
|
||||
# However, it is very unlikely that this happens as type variables can't appear in
|
||||
# `Literal` forms, so the only valid (but unnecessary) use case would be something like:
|
||||
# `type Test[T] = Literal['a']` (and then use `Test[SomeType]`).
|
||||
if typing_objects.is_typealiastype(arg):
|
||||
try:
|
||||
alias_value = arg.__value__
|
||||
except NameError:
|
||||
if unpack_type_aliases == 'eager':
|
||||
raise
|
||||
# unpack_type_aliases == "lenient":
|
||||
if type_check:
|
||||
_literal_type_check(arg)
|
||||
values_and_type.append((arg, type(arg)))
|
||||
else:
|
||||
sub_args = get_literal_values(
|
||||
alias_value, type_check=type_check, unpack_type_aliases=unpack_type_aliases
|
||||
)
|
||||
values_and_type.extend((a, type(a)) for a in sub_args) # pyright: ignore[reportUnknownArgumentType]
|
||||
else:
|
||||
if type_check:
|
||||
_literal_type_check(arg)
|
||||
if arg is typing_objects.NoneType:
|
||||
values_and_type.append((None, typing_objects.NoneType))
|
||||
else:
|
||||
values_and_type.append((arg, type(arg))) # pyright: ignore[reportUnknownArgumentType]
|
||||
|
||||
try:
|
||||
dct = dict.fromkeys(values_and_type)
|
||||
except TypeError:
|
||||
# Unhashable parameters, the Python implementation allows them
|
||||
yield from (p for p, _ in values_and_type)
|
||||
else:
|
||||
yield from (p for p, _ in dct)
|
||||
|
||||
|
||||
Qualifier: TypeAlias = Literal['required', 'not_required', 'read_only', 'class_var', 'init_var', 'final']
|
||||
"""A [type qualifier][]."""
|
||||
|
||||
_all_qualifiers: set[Qualifier] = set(get_args(Qualifier))
|
||||
|
||||
|
||||
# TODO at some point, we could switch to an enum flag, so that multiple sources
|
||||
# can be combined. However, is there a need for this?
|
||||
class AnnotationSource(IntEnum):
|
||||
# TODO if/when https://peps.python.org/pep-0767/ is accepted, add 'read_only'
|
||||
# to CLASS and NAMED_TUPLE (even though for named tuples it is redundant).
|
||||
|
||||
"""The source of an annotation, e.g. a class or a function.
|
||||
|
||||
Depending on the source, different [type qualifiers][type qualifier] may be (dis)allowed.
|
||||
"""
|
||||
|
||||
ASSIGNMENT_OR_VARIABLE = auto()
|
||||
"""An annotation used in an assignment or variable annotation:
|
||||
|
||||
```python
|
||||
x: Final[int] = 1
|
||||
y: Final[str]
|
||||
```
|
||||
|
||||
**Allowed type qualifiers:** [`Final`][typing.Final].
|
||||
"""
|
||||
|
||||
CLASS = auto()
|
||||
"""An annotation used in the body of a class:
|
||||
|
||||
```python
|
||||
class Test:
|
||||
x: Final[int] = 1
|
||||
y: ClassVar[str]
|
||||
```
|
||||
|
||||
**Allowed type qualifiers:** [`ClassVar`][typing.ClassVar], [`Final`][typing.Final].
|
||||
"""
|
||||
|
||||
DATACLASS = auto()
|
||||
"""An annotation used in the body of a dataclass:
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class Test:
|
||||
x: Final[int] = 1
|
||||
y: InitVar[str] = 'test'
|
||||
```
|
||||
|
||||
**Allowed type qualifiers:** [`ClassVar`][typing.ClassVar], [`Final`][typing.Final], [`InitVar`][dataclasses-init-only-variables].
|
||||
""" # noqa: E501
|
||||
|
||||
TYPED_DICT = auto()
|
||||
"""An annotation used in the body of a [`TypedDict`][typing.TypedDict]:
|
||||
|
||||
```python
|
||||
class TD(TypedDict):
|
||||
x: Required[ReadOnly[int]]
|
||||
y: ReadOnly[NotRequired[str]]
|
||||
```
|
||||
|
||||
**Allowed type qualifiers:** [`ReadOnly`][typing.ReadOnly], [`Required`][typing.Required],
|
||||
[`NotRequired`][typing.NotRequired].
|
||||
"""
|
||||
|
||||
NAMED_TUPLE = auto()
|
||||
"""An annotation used in the body of a [`NamedTuple`][typing.NamedTuple].
|
||||
|
||||
```python
|
||||
class NT(NamedTuple):
|
||||
x: int
|
||||
y: str
|
||||
```
|
||||
|
||||
**Allowed type qualifiers:** none.
|
||||
"""
|
||||
|
||||
FUNCTION = auto()
|
||||
"""An annotation used in a function, either for a parameter or the return value.
|
||||
|
||||
```python
|
||||
def func(a: int) -> str:
|
||||
...
|
||||
```
|
||||
|
||||
**Allowed type qualifiers:** none.
|
||||
"""
|
||||
|
||||
ANY = auto()
|
||||
"""An annotation that might come from any source.
|
||||
|
||||
**Allowed type qualifiers:** all.
|
||||
"""
|
||||
|
||||
BARE = auto()
|
||||
"""An annotation that is inspected as is.
|
||||
|
||||
**Allowed type qualifiers:** none.
|
||||
"""
|
||||
|
||||
@property
|
||||
def allowed_qualifiers(self) -> set[Qualifier]:
|
||||
"""The allowed [type qualifiers][type qualifier] for this annotation source."""
|
||||
# TODO use a match statement when Python 3.9 support is dropped.
|
||||
if self is AnnotationSource.ASSIGNMENT_OR_VARIABLE:
|
||||
return {'final'}
|
||||
elif self is AnnotationSource.CLASS:
|
||||
return {'final', 'class_var'}
|
||||
elif self is AnnotationSource.DATACLASS:
|
||||
return {'final', 'class_var', 'init_var'}
|
||||
elif self is AnnotationSource.TYPED_DICT:
|
||||
return {'required', 'not_required', 'read_only'}
|
||||
elif self in (AnnotationSource.NAMED_TUPLE, AnnotationSource.FUNCTION, AnnotationSource.BARE):
|
||||
return set()
|
||||
elif self is AnnotationSource.ANY:
|
||||
return _all_qualifiers
|
||||
else: # pragma: no cover
|
||||
assert_never(self)
|
||||
|
||||
|
||||
class ForbiddenQualifier(Exception):
|
||||
"""The provided [type qualifier][] is forbidden."""
|
||||
|
||||
qualifier: Qualifier
|
||||
"""The forbidden qualifier."""
|
||||
|
||||
def __init__(self, qualifier: Qualifier, /) -> None:
|
||||
self.qualifier = qualifier
|
||||
|
||||
|
||||
class _UnknownTypeEnum(Enum):
|
||||
UNKNOWN = auto()
|
||||
|
||||
def __str__(self) -> str:
|
||||
return 'UNKNOWN'
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return '<UNKNOWN>'
|
||||
|
||||
|
||||
UNKNOWN = _UnknownTypeEnum.UNKNOWN
|
||||
"""A sentinel value used when no [type expression][] is present."""
|
||||
|
||||
_UnkownType: TypeAlias = Literal[_UnknownTypeEnum.UNKNOWN]
|
||||
"""The type of the [`UNKNOWN`][typing_inspection.introspection.UNKNOWN] sentinel value."""
|
||||
|
||||
|
||||
class InspectedAnnotation(NamedTuple):
|
||||
"""The result of the inspected annotation."""
|
||||
|
||||
type: Any | _UnkownType
|
||||
"""The final [type expression][], with [type qualifiers][type qualifier] and annotated metadata stripped.
|
||||
|
||||
If no type expression is available, the [`UNKNOWN`][typing_inspection.introspection.UNKNOWN] sentinel
|
||||
value is used instead. This is the case when a [type qualifier][] is used with no type annotation:
|
||||
|
||||
```python
|
||||
ID: Final = 1
|
||||
|
||||
class C:
|
||||
x: ClassVar = 'test'
|
||||
```
|
||||
"""
|
||||
|
||||
qualifiers: set[Qualifier]
|
||||
"""The [type qualifiers][type qualifier] present on the annotation."""
|
||||
|
||||
metadata: Sequence[Any]
|
||||
"""The annotated metadata."""
|
||||
|
||||
|
||||
def inspect_annotation( # noqa: PLR0915
|
||||
annotation: Any,
|
||||
/,
|
||||
*,
|
||||
annotation_source: AnnotationSource,
|
||||
unpack_type_aliases: Literal['skip', 'lenient', 'eager'] = 'skip',
|
||||
) -> InspectedAnnotation:
|
||||
"""Inspect an [annotation expression][], extracting any [type qualifier][] and metadata.
|
||||
|
||||
An [annotation expression][] is a [type expression][] optionally surrounded by one or more
|
||||
[type qualifiers][type qualifier] or by [`Annotated`][typing.Annotated]. This function will:
|
||||
|
||||
- Unwrap the type expression, keeping track of the type qualifiers.
|
||||
- Unwrap [`Annotated`][typing.Annotated] forms, keeping track of the annotated metadata.
|
||||
|
||||
Args:
|
||||
annotation: The annotation expression to be inspected.
|
||||
annotation_source: The source of the annotation. Depending on the source (e.g. a class), different type
|
||||
qualifiers may be (dis)allowed. To allow any type qualifier, use
|
||||
[`AnnotationSource.ANY`][typing_inspection.introspection.AnnotationSource.ANY].
|
||||
unpack_type_aliases: What to do when encountering [PEP 695](https://peps.python.org/pep-0695/)
|
||||
[type aliases][type-aliases]. Can be one of:
|
||||
|
||||
- `'skip'`: Do not try to parse type aliases (the default):
|
||||
```pycon
|
||||
>>> type MyInt = Annotated[int, 'meta']
|
||||
>>> inspect_annotation(MyInt, annotation_source=AnnotationSource.BARE, unpack_type_aliases='skip')
|
||||
InspectedAnnotation(type=MyInt, qualifiers={}, metadata=[])
|
||||
```
|
||||
|
||||
- `'lenient'`: Try to parse type aliases, and fallback to `'skip'` if the type alias
|
||||
can't be inspected (because of an undefined forward reference):
|
||||
```pycon
|
||||
>>> type MyInt = Annotated[Undefined, 'meta']
|
||||
>>> inspect_annotation(MyInt, annotation_source=AnnotationSource.BARE, unpack_type_aliases='lenient')
|
||||
InspectedAnnotation(type=MyInt, qualifiers={}, metadata=[])
|
||||
>>> Undefined = int
|
||||
>>> inspect_annotation(MyInt, annotation_source=AnnotationSource.BARE, unpack_type_aliases='lenient')
|
||||
InspectedAnnotation(type=int, qualifiers={}, metadata=['meta'])
|
||||
```
|
||||
|
||||
- `'eager'`: Parse type aliases and raise any encountered [`NameError`][] exceptions.
|
||||
|
||||
Returns:
|
||||
The result of the inspected annotation, where the type expression, used qualifiers and metadata is stored.
|
||||
|
||||
Example:
|
||||
```pycon
|
||||
>>> inspect_annotation(
|
||||
... Final[Annotated[ClassVar[Annotated[int, 'meta_1']], 'meta_2']],
|
||||
... annotation_source=AnnotationSource.CLASS,
|
||||
... )
|
||||
...
|
||||
InspectedAnnotation(type=int, qualifiers={'class_var', 'final'}, metadata=['meta_1', 'meta_2'])
|
||||
```
|
||||
"""
|
||||
allowed_qualifiers = annotation_source.allowed_qualifiers
|
||||
qualifiers: set[Qualifier] = set()
|
||||
metadata: list[Any] = []
|
||||
|
||||
while True:
|
||||
annotation, _meta = _unpack_annotated(annotation, unpack_type_aliases=unpack_type_aliases)
|
||||
if _meta:
|
||||
metadata = _meta + metadata
|
||||
continue
|
||||
|
||||
origin = get_origin(annotation)
|
||||
if origin is not None:
|
||||
if typing_objects.is_classvar(origin):
|
||||
if 'class_var' not in allowed_qualifiers:
|
||||
raise ForbiddenQualifier('class_var')
|
||||
qualifiers.add('class_var')
|
||||
annotation = annotation.__args__[0]
|
||||
elif typing_objects.is_final(origin):
|
||||
if 'final' not in allowed_qualifiers:
|
||||
raise ForbiddenQualifier('final')
|
||||
qualifiers.add('final')
|
||||
annotation = annotation.__args__[0]
|
||||
elif typing_objects.is_required(origin):
|
||||
if 'required' not in allowed_qualifiers:
|
||||
raise ForbiddenQualifier('required')
|
||||
qualifiers.add('required')
|
||||
annotation = annotation.__args__[0]
|
||||
elif typing_objects.is_notrequired(origin):
|
||||
if 'not_required' not in allowed_qualifiers:
|
||||
raise ForbiddenQualifier('not_required')
|
||||
qualifiers.add('not_required')
|
||||
annotation = annotation.__args__[0]
|
||||
elif typing_objects.is_readonly(origin):
|
||||
if 'read_only' not in allowed_qualifiers:
|
||||
raise ForbiddenQualifier('not_required')
|
||||
qualifiers.add('read_only')
|
||||
annotation = annotation.__args__[0]
|
||||
else:
|
||||
# origin is not None but not a type qualifier nor `Annotated` (e.g. `list[int]`):
|
||||
break
|
||||
elif isinstance(annotation, InitVar):
|
||||
if 'init_var' not in allowed_qualifiers:
|
||||
raise ForbiddenQualifier('init_var')
|
||||
qualifiers.add('init_var')
|
||||
annotation = cast(Any, annotation.type)
|
||||
else:
|
||||
break
|
||||
|
||||
# `Final`, `ClassVar` and `InitVar` are type qualifiers allowed to be used as a bare annotation:
|
||||
if typing_objects.is_final(annotation):
|
||||
if 'final' not in allowed_qualifiers:
|
||||
raise ForbiddenQualifier('final')
|
||||
qualifiers.add('final')
|
||||
annotation = UNKNOWN
|
||||
elif typing_objects.is_classvar(annotation):
|
||||
if 'class_var' not in allowed_qualifiers:
|
||||
raise ForbiddenQualifier('class_var')
|
||||
qualifiers.add('class_var')
|
||||
annotation = UNKNOWN
|
||||
elif annotation is InitVar:
|
||||
if 'init_var' not in allowed_qualifiers:
|
||||
raise ForbiddenQualifier('init_var')
|
||||
qualifiers.add('init_var')
|
||||
annotation = UNKNOWN
|
||||
|
||||
return InspectedAnnotation(annotation, qualifiers, metadata)
|
||||
|
||||
|
||||
def _unpack_annotated_inner(
|
||||
annotation: Any, unpack_type_aliases: Literal['lenient', 'eager'], check_annotated: bool
|
||||
) -> tuple[Any, list[Any]]:
|
||||
origin = get_origin(annotation)
|
||||
if check_annotated and typing_objects.is_annotated(origin):
|
||||
annotated_type = annotation.__origin__
|
||||
metadata = list(annotation.__metadata__)
|
||||
|
||||
# The annotated type might be a PEP 695 type alias, so we need to recursively
|
||||
# unpack it. Because Python already flattens `Annotated[Annotated[<type>, ...], ...]` forms,
|
||||
# we can skip the `is_annotated()` check in the next call:
|
||||
annotated_type, sub_meta = _unpack_annotated_inner(
|
||||
annotated_type, unpack_type_aliases=unpack_type_aliases, check_annotated=False
|
||||
)
|
||||
metadata = sub_meta + metadata
|
||||
return annotated_type, metadata
|
||||
elif typing_objects.is_typealiastype(annotation):
|
||||
try:
|
||||
value = annotation.__value__
|
||||
except NameError:
|
||||
if unpack_type_aliases == 'eager':
|
||||
raise
|
||||
else:
|
||||
typ, metadata = _unpack_annotated_inner(
|
||||
value, unpack_type_aliases=unpack_type_aliases, check_annotated=True
|
||||
)
|
||||
if metadata:
|
||||
# Having metadata means the type alias' `__value__` was an `Annotated` form
|
||||
# (or, recursively, a type alias to an `Annotated` form). It is important to check
|
||||
# for this, as we don't want to unpack other type aliases (e.g. `type MyInt = int`).
|
||||
return typ, metadata
|
||||
return annotation, []
|
||||
elif typing_objects.is_typealiastype(origin):
|
||||
# When parameterized, PEP 695 type aliases become generic aliases
|
||||
# (e.g. with `type MyList[T] = Annotated[list[T], ...]`, `MyList[int]`
|
||||
# is a generic alias).
|
||||
try:
|
||||
value = origin.__value__
|
||||
except NameError:
|
||||
if unpack_type_aliases == 'eager':
|
||||
raise
|
||||
else:
|
||||
# While Python already handles type variable replacement for simple `Annotated` forms,
|
||||
# we need to manually apply the same logic for PEP 695 type aliases:
|
||||
# - With `MyList = Annotated[list[T], ...]`, `MyList[int] == Annotated[list[int], ...]`
|
||||
# - With `type MyList[T] = Annotated[list[T], ...]`, `MyList[int].__value__ == Annotated[list[T], ...]`.
|
||||
|
||||
try:
|
||||
# To do so, we emulate the parameterization of the value with the arguments:
|
||||
# with `type MyList[T] = Annotated[list[T], ...]`, to emulate `MyList[int]`,
|
||||
# we do `Annotated[list[T], ...][int]` (which gives `Annotated[list[T], ...]`):
|
||||
value = value[annotation.__args__]
|
||||
except TypeError:
|
||||
# Might happen if the type alias is parameterized, but its value doesn't have any
|
||||
# type variables, e.g. `type MyInt[T] = int`.
|
||||
pass
|
||||
typ, metadata = _unpack_annotated_inner(
|
||||
value, unpack_type_aliases=unpack_type_aliases, check_annotated=True
|
||||
)
|
||||
if metadata:
|
||||
return typ, metadata
|
||||
return annotation, []
|
||||
|
||||
return annotation, []
|
||||
|
||||
|
||||
# This could eventually be made public:
|
||||
def _unpack_annotated(
|
||||
annotation: Any, /, *, unpack_type_aliases: Literal['skip', 'lenient', 'eager'] = 'eager'
|
||||
) -> tuple[Any, list[Any]]:
|
||||
if unpack_type_aliases == 'skip':
|
||||
if typing_objects.is_annotated(get_origin(annotation)):
|
||||
return annotation.__origin__, list(annotation.__metadata__)
|
||||
else:
|
||||
return annotation, []
|
||||
|
||||
return _unpack_annotated_inner(annotation, unpack_type_aliases=unpack_type_aliases, check_annotated=True)
|
Reference in New Issue
Block a user