feat: add psagentai agent with deployment and configuration
Initial project setup including the main agent script, configuration files, profile JSON state, systemd service unit, startup script, deployment documentation, and Python dependencies. Includes .env, venv/, and __pycache__/ which should likely be excluded via .gitignore to avoid committing secrets and artifacts.
This commit is contained in:
@@ -0,0 +1,587 @@
|
||||
"""High-level introspection utilities, used to inspect type annotations."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import types
|
||||
from collections.abc import Generator
|
||||
from dataclasses import InitVar
|
||||
from enum import Enum, IntEnum, auto
|
||||
from typing import Any, Literal, NamedTuple, TypeAlias, cast
|
||||
|
||||
from typing_extensions import assert_never, get_args, get_origin # noqa: UP035
|
||||
|
||||
from . import typing_objects
|
||||
|
||||
__all__ = (
|
||||
'AnnotationSource',
|
||||
'ForbiddenQualifier',
|
||||
'InspectedAnnotation',
|
||||
'Qualifier',
|
||||
'get_literal_values',
|
||||
'inspect_annotation',
|
||||
'is_union_origin',
|
||||
)
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
|
||||
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
|
||||
>>> is_union_origin(types.UnionType)
|
||||
True
|
||||
```
|
||||
|
||||
!!! note
|
||||
Since Python 3.14, both `Union[<t1>, <t2>, ...]` and `<t1> | <t2> | ...` forms create instances
|
||||
of the same [`typing.Union`][] class. As such, it is recommended to not use this function
|
||||
anymore (provided that you only support Python 3.14 or greater), and instead use the
|
||||
[`typing_objects.is_union()`][typing_inspection.typing_objects.is_union] function directly:
|
||||
|
||||
```python
|
||||
from typing import Union, get_origin
|
||||
|
||||
from typing_inspection import typing_objects
|
||||
|
||||
typ = int | str # Or Union[int, str]
|
||||
origin = get_origin(typ)
|
||||
if typing_objects.is_union(origin):
|
||||
...
|
||||
```
|
||||
"""
|
||||
return typing_objects.is_union(obj)
|
||||
|
||||
|
||||
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
|
||||
>>> is_union_origin(types.UnionType)
|
||||
True
|
||||
```
|
||||
|
||||
!!! note
|
||||
Since Python 3.14, both `Union[<t1>, <t2>, ...]` and `<t1> | <t2> | ...` forms create instances
|
||||
of the same [`typing.Union`][] class. As such, it is recommended to not use this function
|
||||
anymore (provided that you only support Python 3.14 or greater), and instead use the
|
||||
[`typing_objects.is_union()`][typing_inspection.typing_objects.is_union] function directly:
|
||||
|
||||
```python
|
||||
from typing import Union, get_origin
|
||||
|
||||
from typing_inspection import typing_objects
|
||||
|
||||
typ = int | str # Or Union[int, str]
|
||||
origin = get_origin(typ)
|
||||
if typing_objects.is_union(origin):
|
||||
...
|
||||
```
|
||||
"""
|
||||
return typing_objects.is_union(obj) or obj is types.UnionType
|
||||
|
||||
|
||||
_literal_allowed_types = int | bytes | str | bool | Enum | typing_objects.NoneType
|
||||
|
||||
|
||||
def _literal_type_check(value: Any, /) -> None:
|
||||
"""Type check the provided literal value against the legal parameters."""
|
||||
if not isinstance(value, _literal_allowed_types) 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.InitVar].
|
||||
""" # 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."""
|
||||
|
||||
_UnknownType: 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 | _UnknownType
|
||||
"""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: list[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