pyflowstep 0.2.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyflowstep-0.2.0 → pyflowstep-0.3.0}/PKG-INFO +71 -3
- {pyflowstep-0.2.0 → pyflowstep-0.3.0}/README.md +70 -2
- {pyflowstep-0.2.0 → pyflowstep-0.3.0}/pyproject.toml +1 -1
- {pyflowstep-0.2.0 → pyflowstep-0.3.0}/pyproject.toml.orig +1 -1
- {pyflowstep-0.2.0 → pyflowstep-0.3.0}/src/pyflowstep/__init__.py +6 -0
- {pyflowstep-0.2.0 → pyflowstep-0.3.0}/src/pyflowstep/annotations.py +90 -59
- {pyflowstep-0.2.0 → pyflowstep-0.3.0}/src/pyflowstep/dependencies.py +318 -296
- {pyflowstep-0.2.0 → pyflowstep-0.3.0}/src/pyflowstep/exceptions.py +12 -0
- {pyflowstep-0.2.0 → pyflowstep-0.3.0}/src/pyflowstep/flow.py +12 -2
- pyflowstep-0.3.0/src/pyflowstep/inputs.py +178 -0
- {pyflowstep-0.2.0 → pyflowstep-0.3.0}/src/pyflowstep/steps.py +29 -10
- {pyflowstep-0.2.0 → pyflowstep-0.3.0}/src/pyflowstep/compilers.py +0 -0
- {pyflowstep-0.2.0 → pyflowstep-0.3.0}/src/pyflowstep/json_schema.py +0 -0
- {pyflowstep-0.2.0 → pyflowstep-0.3.0}/src/pyflowstep/parsers.py +0 -0
- {pyflowstep-0.2.0 → pyflowstep-0.3.0}/src/pyflowstep/py.typed +0 -0
- {pyflowstep-0.2.0 → pyflowstep-0.3.0}/src/pyflowstep/registry.py +0 -0
- {pyflowstep-0.2.0 → pyflowstep-0.3.0}/src/pyflowstep/validators.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyflowstep
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A lightweight, typed Python library for composing functions into readable, reusable flows that can be defined as JSON.
|
|
5
5
|
Keywords: flow,pipeline,composition,functional,fluent-interface,json,json-schema,workflow,dsl
|
|
6
6
|
Author: aaltatan
|
|
@@ -32,6 +32,7 @@ A lightweight, typed Python library for composing functions into readable, reusa
|
|
|
32
32
|
- step arguments are validated and parsed when a flow is **built**, not halfway through running it
|
|
33
33
|
- raw JSON values become typed arguments with a `Parse` marker next to the parameter
|
|
34
34
|
- steps get external objects (a mailer, a database session) through FastAPI-style `Depends`
|
|
35
|
+
- the caller hands per-run values to a flow by name: `flow(page, user=user)` fills every `user: Input[User]`
|
|
35
36
|
- registry-based registration keeps steps organized and discoverable
|
|
36
37
|
- flow definitions can be compiled from dictionaries or JSON
|
|
37
38
|
- every registry can describe its flow language as a JSON Schema
|
|
@@ -181,7 +182,7 @@ pipeline # Flow(add >> multiply >> add)
|
|
|
181
182
|
pipeline(1) # 31
|
|
182
183
|
```
|
|
183
184
|
|
|
184
|
-
`step` and `tap` do one thing: turn a function into a flow factory. Naming a step belongs to the [registry](#registry).
|
|
185
|
+
`step` and `tap` do one thing: turn a function into a flow factory. Naming a step belongs to the [registry](#registry). Three markers can sit on a parameter: [`Parse`](#parsing-arguments) to convert the value passed for it, [`Depends`](#dependencies) to inject an object nobody passes, and [`Input`](#run-inputs) to receive a value from whoever runs the flow.
|
|
185
186
|
|
|
186
187
|
Arguments are bound against the function signature **immediately**, so mistakes fail fast:
|
|
187
188
|
|
|
@@ -532,6 +533,71 @@ See [`examples/dependencies.py`](examples/dependencies.py) for a complete flow w
|
|
|
532
533
|
|
|
533
534
|
---
|
|
534
535
|
|
|
536
|
+
## Run inputs
|
|
537
|
+
|
|
538
|
+
Some values exist only where the flow is run: the logged-in user, the record being processed, credentials read from a prompt. No provider can build them and JSON must not hold them. Annotate the parameter with `Input[T]` and the caller supplies it, by name, when it runs the flow:
|
|
539
|
+
|
|
540
|
+
```python
|
|
541
|
+
from pyflowstep import Input, StepsRegistry
|
|
542
|
+
|
|
543
|
+
steps = StepsRegistry[Page]()
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
@steps.tap()
|
|
547
|
+
def login(page: Page, url: str, credentials: Input[Credentials]) -> None:
|
|
548
|
+
page.navigate(url)
|
|
549
|
+
page.fill("#user", credentials.username)
|
|
550
|
+
page.fill("#password", credentials.password)
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
@steps.tap()
|
|
554
|
+
def fill_year(page: Page, selector: str, voucher: Input[Voucher]) -> None:
|
|
555
|
+
page.fill(selector, voucher.year)
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
flow = login("https://example.com/login") >> fill_year("#year") # inputs are not arguments
|
|
559
|
+
|
|
560
|
+
flow(page, credentials=credentials, voucher=voucher) # they are given to the run
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
```json
|
|
564
|
+
[
|
|
565
|
+
{"name": "login", "args": ["https://example.com/login"]},
|
|
566
|
+
{"name": "fill_year", "args": ["#year"]}
|
|
567
|
+
]
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
The name of the input is the name of the parameter, and every step that declares it receives the same value.
|
|
571
|
+
|
|
572
|
+
### Checked before anything runs
|
|
573
|
+
|
|
574
|
+
A flow knows the inputs its steps require, and checks them before the first step runs. A forgotten input never fails halfway through:
|
|
575
|
+
|
|
576
|
+
```python
|
|
577
|
+
flow.inputs # frozenset({'credentials', 'voucher'})
|
|
578
|
+
|
|
579
|
+
flow(page, credentials=credentials)
|
|
580
|
+
# MissingInputError: missing run input 'voucher' for step 'fill_year'; run the flow as flow(subject, voucher=...)
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
### Rules
|
|
584
|
+
|
|
585
|
+
- **Invisible to JSON**, like a dependency: an input is absent from the JSON schema, cannot be parsed, and passing one while building the flow raises `UnexpectedKeywordArgumentError` (or `TooManyArgumentsError`).
|
|
586
|
+
- **A default value makes the input optional**: `note: Input[str] = ""` is used when the caller passes no `note`. Optional inputs are not listed in `flow.inputs`.
|
|
587
|
+
- **Extra inputs are ignored**, so one caller can run different flows, each using the inputs it needs.
|
|
588
|
+
- **Nested flows see the inputs of the run they join.** A flow called from inside a step can be given inputs of its own, `inner(page, voucher=other)`; they are laid over the outer ones for that call only.
|
|
589
|
+
- **Declarations are checked early**, when the step is created, with `InvalidInputError`: an input on the subject, on a positional-only, `*args` or `**kwargs` parameter, or on a parameter that is also a dependency. A provider cannot take an input.
|
|
590
|
+
|
|
591
|
+
### `Input` or `Depends`?
|
|
592
|
+
|
|
593
|
+
| The object… | Use |
|
|
594
|
+
| ------------------------------------------------------------ | ------------------------------------- |
|
|
595
|
+
| is written in the flow definition (a selector, a limit) | a plain argument, with `Parse` if needed |
|
|
596
|
+
| can be built by a function, the same way for every caller (a mailer, a session) | `Depends(provider)` |
|
|
597
|
+
| is only known to whoever runs the flow (a user, a record, credentials) | `Input[T]` |
|
|
598
|
+
|
|
599
|
+
---
|
|
600
|
+
|
|
535
601
|
## Compiling flows from JSON
|
|
536
602
|
|
|
537
603
|
`FlowCompiler` turns a flow definition — a list of step dictionaries — into a `Flow`.
|
|
@@ -738,13 +804,15 @@ checkout = note_if(is_large(Decimal(100)), "needs approval") >> apply_tax(round(
|
|
|
738
804
|
| `Parse(fn)` | marker | Apply `fn` to the value passed for a parameter, see [Parsing arguments](#parsing-arguments) |
|
|
739
805
|
| `Depends(provider)` | marker | Inject a parameter by calling `provider`, see [Dependencies](#dependencies) |
|
|
740
806
|
| `override_dependencies(mapping)` | context manager | Replace providers inside a `with` block, for tests |
|
|
807
|
+
| `Input[T]` | marker | Receive a parameter from the caller of the flow, `flow(subject, name=value)`, see [Run inputs](#run-inputs) |
|
|
808
|
+
| `Flow.inputs` | property | The names of the inputs a flow requires |
|
|
741
809
|
| `FlowCompiler[T](steps)` | class | `compile(definition)` turns parsed step dicts into a flow |
|
|
742
810
|
| `validate_step_dict(item, path)` | function | Validate one step dictionary |
|
|
743
811
|
| `get_json_schema(annotation)` | function | JSON Schema of a type annotation |
|
|
744
812
|
| `get_step_json_schema(name, step)` | function | JSON Schema of one step dictionary |
|
|
745
813
|
| `get_flow_json_schema(steps)` | function | JSON Schema of a whole flow definition |
|
|
746
814
|
|
|
747
|
-
All exceptions derive from `PyflowstepError`; argument errors, `InvalidParserError` and `
|
|
815
|
+
All exceptions derive from `PyflowstepError`; argument errors, `InvalidParserError`, `InvalidDependencyError`, `InvalidInputError` and `MissingInputError` also derive from `TypeError`, definition errors from `ValueError`, and `StepDoesNotExistError` from `LookupError`.
|
|
748
816
|
|
|
749
817
|
---
|
|
750
818
|
|
|
@@ -9,6 +9,7 @@ A lightweight, typed Python library for composing functions into readable, reusa
|
|
|
9
9
|
- step arguments are validated and parsed when a flow is **built**, not halfway through running it
|
|
10
10
|
- raw JSON values become typed arguments with a `Parse` marker next to the parameter
|
|
11
11
|
- steps get external objects (a mailer, a database session) through FastAPI-style `Depends`
|
|
12
|
+
- the caller hands per-run values to a flow by name: `flow(page, user=user)` fills every `user: Input[User]`
|
|
12
13
|
- registry-based registration keeps steps organized and discoverable
|
|
13
14
|
- flow definitions can be compiled from dictionaries or JSON
|
|
14
15
|
- every registry can describe its flow language as a JSON Schema
|
|
@@ -158,7 +159,7 @@ pipeline # Flow(add >> multiply >> add)
|
|
|
158
159
|
pipeline(1) # 31
|
|
159
160
|
```
|
|
160
161
|
|
|
161
|
-
`step` and `tap` do one thing: turn a function into a flow factory. Naming a step belongs to the [registry](#registry).
|
|
162
|
+
`step` and `tap` do one thing: turn a function into a flow factory. Naming a step belongs to the [registry](#registry). Three markers can sit on a parameter: [`Parse`](#parsing-arguments) to convert the value passed for it, [`Depends`](#dependencies) to inject an object nobody passes, and [`Input`](#run-inputs) to receive a value from whoever runs the flow.
|
|
162
163
|
|
|
163
164
|
Arguments are bound against the function signature **immediately**, so mistakes fail fast:
|
|
164
165
|
|
|
@@ -509,6 +510,71 @@ See [`examples/dependencies.py`](examples/dependencies.py) for a complete flow w
|
|
|
509
510
|
|
|
510
511
|
---
|
|
511
512
|
|
|
513
|
+
## Run inputs
|
|
514
|
+
|
|
515
|
+
Some values exist only where the flow is run: the logged-in user, the record being processed, credentials read from a prompt. No provider can build them and JSON must not hold them. Annotate the parameter with `Input[T]` and the caller supplies it, by name, when it runs the flow:
|
|
516
|
+
|
|
517
|
+
```python
|
|
518
|
+
from pyflowstep import Input, StepsRegistry
|
|
519
|
+
|
|
520
|
+
steps = StepsRegistry[Page]()
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
@steps.tap()
|
|
524
|
+
def login(page: Page, url: str, credentials: Input[Credentials]) -> None:
|
|
525
|
+
page.navigate(url)
|
|
526
|
+
page.fill("#user", credentials.username)
|
|
527
|
+
page.fill("#password", credentials.password)
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
@steps.tap()
|
|
531
|
+
def fill_year(page: Page, selector: str, voucher: Input[Voucher]) -> None:
|
|
532
|
+
page.fill(selector, voucher.year)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
flow = login("https://example.com/login") >> fill_year("#year") # inputs are not arguments
|
|
536
|
+
|
|
537
|
+
flow(page, credentials=credentials, voucher=voucher) # they are given to the run
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
```json
|
|
541
|
+
[
|
|
542
|
+
{"name": "login", "args": ["https://example.com/login"]},
|
|
543
|
+
{"name": "fill_year", "args": ["#year"]}
|
|
544
|
+
]
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
The name of the input is the name of the parameter, and every step that declares it receives the same value.
|
|
548
|
+
|
|
549
|
+
### Checked before anything runs
|
|
550
|
+
|
|
551
|
+
A flow knows the inputs its steps require, and checks them before the first step runs. A forgotten input never fails halfway through:
|
|
552
|
+
|
|
553
|
+
```python
|
|
554
|
+
flow.inputs # frozenset({'credentials', 'voucher'})
|
|
555
|
+
|
|
556
|
+
flow(page, credentials=credentials)
|
|
557
|
+
# MissingInputError: missing run input 'voucher' for step 'fill_year'; run the flow as flow(subject, voucher=...)
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
### Rules
|
|
561
|
+
|
|
562
|
+
- **Invisible to JSON**, like a dependency: an input is absent from the JSON schema, cannot be parsed, and passing one while building the flow raises `UnexpectedKeywordArgumentError` (or `TooManyArgumentsError`).
|
|
563
|
+
- **A default value makes the input optional**: `note: Input[str] = ""` is used when the caller passes no `note`. Optional inputs are not listed in `flow.inputs`.
|
|
564
|
+
- **Extra inputs are ignored**, so one caller can run different flows, each using the inputs it needs.
|
|
565
|
+
- **Nested flows see the inputs of the run they join.** A flow called from inside a step can be given inputs of its own, `inner(page, voucher=other)`; they are laid over the outer ones for that call only.
|
|
566
|
+
- **Declarations are checked early**, when the step is created, with `InvalidInputError`: an input on the subject, on a positional-only, `*args` or `**kwargs` parameter, or on a parameter that is also a dependency. A provider cannot take an input.
|
|
567
|
+
|
|
568
|
+
### `Input` or `Depends`?
|
|
569
|
+
|
|
570
|
+
| The object… | Use |
|
|
571
|
+
| ------------------------------------------------------------ | ------------------------------------- |
|
|
572
|
+
| is written in the flow definition (a selector, a limit) | a plain argument, with `Parse` if needed |
|
|
573
|
+
| can be built by a function, the same way for every caller (a mailer, a session) | `Depends(provider)` |
|
|
574
|
+
| is only known to whoever runs the flow (a user, a record, credentials) | `Input[T]` |
|
|
575
|
+
|
|
576
|
+
---
|
|
577
|
+
|
|
512
578
|
## Compiling flows from JSON
|
|
513
579
|
|
|
514
580
|
`FlowCompiler` turns a flow definition — a list of step dictionaries — into a `Flow`.
|
|
@@ -715,13 +781,15 @@ checkout = note_if(is_large(Decimal(100)), "needs approval") >> apply_tax(round(
|
|
|
715
781
|
| `Parse(fn)` | marker | Apply `fn` to the value passed for a parameter, see [Parsing arguments](#parsing-arguments) |
|
|
716
782
|
| `Depends(provider)` | marker | Inject a parameter by calling `provider`, see [Dependencies](#dependencies) |
|
|
717
783
|
| `override_dependencies(mapping)` | context manager | Replace providers inside a `with` block, for tests |
|
|
784
|
+
| `Input[T]` | marker | Receive a parameter from the caller of the flow, `flow(subject, name=value)`, see [Run inputs](#run-inputs) |
|
|
785
|
+
| `Flow.inputs` | property | The names of the inputs a flow requires |
|
|
718
786
|
| `FlowCompiler[T](steps)` | class | `compile(definition)` turns parsed step dicts into a flow |
|
|
719
787
|
| `validate_step_dict(item, path)` | function | Validate one step dictionary |
|
|
720
788
|
| `get_json_schema(annotation)` | function | JSON Schema of a type annotation |
|
|
721
789
|
| `get_step_json_schema(name, step)` | function | JSON Schema of one step dictionary |
|
|
722
790
|
| `get_flow_json_schema(steps)` | function | JSON Schema of a whole flow definition |
|
|
723
791
|
|
|
724
|
-
All exceptions derive from `PyflowstepError`; argument errors, `InvalidParserError` and `
|
|
792
|
+
All exceptions derive from `PyflowstepError`; argument errors, `InvalidParserError`, `InvalidDependencyError`, `InvalidInputError` and `MissingInputError` also derive from `TypeError`, definition errors from `ValueError`, and `StepDoesNotExistError` from `LookupError`.
|
|
725
793
|
|
|
726
794
|
---
|
|
727
795
|
|
|
@@ -27,10 +27,12 @@ from .exceptions import (
|
|
|
27
27
|
ArgumentError,
|
|
28
28
|
InvalidDependencyError,
|
|
29
29
|
InvalidFlowDefinitionError,
|
|
30
|
+
InvalidInputError,
|
|
30
31
|
InvalidParserError,
|
|
31
32
|
InvalidStepError,
|
|
32
33
|
InvalidStepNameError,
|
|
33
34
|
MissingArgumentError,
|
|
35
|
+
MissingInputError,
|
|
34
36
|
MultipleValuesArgumentError,
|
|
35
37
|
ParseArgumentError,
|
|
36
38
|
PositionalOnlyArgumentError,
|
|
@@ -41,6 +43,7 @@ from .exceptions import (
|
|
|
41
43
|
UnexpectedKeywordArgumentError,
|
|
42
44
|
)
|
|
43
45
|
from .flow import Action, Flow, compose
|
|
46
|
+
from .inputs import Input
|
|
44
47
|
from .json_schema import get_flow_json_schema, get_json_schema, get_step_json_schema
|
|
45
48
|
from .parsers import Parse
|
|
46
49
|
from .registry import StepsRegistry
|
|
@@ -53,12 +56,15 @@ __all__ = [
|
|
|
53
56
|
"Flow",
|
|
54
57
|
"FlowCompiler",
|
|
55
58
|
"FlowDefinition",
|
|
59
|
+
"Input",
|
|
56
60
|
"InvalidDependencyError",
|
|
57
61
|
"InvalidFlowDefinitionError",
|
|
62
|
+
"InvalidInputError",
|
|
58
63
|
"InvalidParserError",
|
|
59
64
|
"InvalidStepError",
|
|
60
65
|
"InvalidStepNameError",
|
|
61
66
|
"MissingArgumentError",
|
|
67
|
+
"MissingInputError",
|
|
62
68
|
"MultipleValuesArgumentError",
|
|
63
69
|
"Parse",
|
|
64
70
|
"ParseArgumentError",
|
|
@@ -1,59 +1,90 @@
|
|
|
1
|
-
"""Read the annotations that carry markers such as `Parse` and `Depends`."""
|
|
2
|
-
|
|
3
|
-
from collections.abc import Callable
|
|
4
|
-
from functools import partial
|
|
5
|
-
from inspect import get_annotations, isroutine
|
|
6
|
-
from typing import Annotated, Any, TypeAliasType, get_args, get_origin
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def resolved_annotations(fn: Callable[..., Any]) -> dict[str, Any]:
|
|
10
|
-
"""Return the annotations describing a call to `fn`, with strings evaluated when possible.
|
|
11
|
-
|
|
12
|
-
For a class this is its `__init__`, for a callable object its `__call__`,
|
|
13
|
-
and for a `functools.partial` the function it wraps.
|
|
14
|
-
|
|
15
|
-
Example:
|
|
16
|
-
```python
|
|
17
|
-
>>> def wait(page, selector: "str", timeout: float = 5.0): ...
|
|
18
|
-
>>> resolved_annotations(wait)
|
|
19
|
-
{'selector': <class 'str'>, 'timeout': <class 'float'>}
|
|
20
|
-
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
"""
|
|
24
|
-
target = _annotated_function(fn)
|
|
25
|
-
|
|
26
|
-
try:
|
|
27
|
-
return get_annotations(target, eval_str=True)
|
|
28
|
-
except NameError:
|
|
29
|
-
return get_annotations(target)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def annotated_metadata(annotation: Any) -> tuple[Any, ...]:
|
|
33
|
-
"""Return the extra arguments of `Annotated[...]`, looking through `type` aliases.
|
|
34
|
-
|
|
35
|
-
Example:
|
|
36
|
-
```python
|
|
37
|
-
>>> type Port = Annotated[int, "tcp", 8080]
|
|
38
|
-
>>> annotated_metadata(Port)
|
|
39
|
-
('tcp', 8080)
|
|
40
|
-
>>> annotated_metadata(int)
|
|
41
|
-
()
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
""
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if
|
|
57
|
-
return
|
|
58
|
-
|
|
59
|
-
|
|
1
|
+
"""Read the annotations that carry markers such as `Parse` and `Depends`."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from functools import partial
|
|
5
|
+
from inspect import get_annotations, isroutine
|
|
6
|
+
from typing import Annotated, Any, TypeAliasType, get_args, get_origin
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def resolved_annotations(fn: Callable[..., Any]) -> dict[str, Any]:
|
|
10
|
+
"""Return the annotations describing a call to `fn`, with strings evaluated when possible.
|
|
11
|
+
|
|
12
|
+
For a class this is its `__init__`, for a callable object its `__call__`,
|
|
13
|
+
and for a `functools.partial` the function it wraps.
|
|
14
|
+
|
|
15
|
+
Example:
|
|
16
|
+
```python
|
|
17
|
+
>>> def wait(page, selector: "str", timeout: float = 5.0): ...
|
|
18
|
+
>>> resolved_annotations(wait)
|
|
19
|
+
{'selector': <class 'str'>, 'timeout': <class 'float'>}
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
target = _annotated_function(fn)
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
return get_annotations(target, eval_str=True)
|
|
28
|
+
except NameError:
|
|
29
|
+
return get_annotations(target)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def annotated_metadata(annotation: Any) -> tuple[Any, ...]:
|
|
33
|
+
"""Return the extra arguments of `Annotated[...]`, looking through `type` aliases.
|
|
34
|
+
|
|
35
|
+
Example:
|
|
36
|
+
```python
|
|
37
|
+
>>> type Port = Annotated[int, "tcp", 8080]
|
|
38
|
+
>>> annotated_metadata(Port)
|
|
39
|
+
('tcp', 8080)
|
|
40
|
+
>>> annotated_metadata(int)
|
|
41
|
+
()
|
|
42
|
+
>>> type Tagged[T] = Annotated[T, "tag"]
|
|
43
|
+
>>> annotated_metadata(Tagged[int])
|
|
44
|
+
('tag',)
|
|
45
|
+
>>> annotated_metadata(Annotated[Tagged[Port], "outer"])
|
|
46
|
+
('tcp', 8080, 'tag', 'outer')
|
|
47
|
+
>>> type Fixed[T] = Annotated[int, "fixed"]
|
|
48
|
+
>>> annotated_metadata(Fixed[str])
|
|
49
|
+
('fixed',)
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
"""
|
|
54
|
+
annotation = _alias_value(annotation)
|
|
55
|
+
|
|
56
|
+
if get_origin(annotation) is not Annotated:
|
|
57
|
+
return ()
|
|
58
|
+
|
|
59
|
+
inner, *metadata = get_args(annotation)
|
|
60
|
+
return (*annotated_metadata(inner), *metadata)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _alias_value(annotation: Any) -> Any:
|
|
64
|
+
"""Return what a `type` alias stands for, also when it is subscripted (`Alias[int]`)."""
|
|
65
|
+
while True:
|
|
66
|
+
origin = get_origin(annotation)
|
|
67
|
+
|
|
68
|
+
if isinstance(annotation, TypeAliasType):
|
|
69
|
+
annotation = annotation.__value__
|
|
70
|
+
elif isinstance(origin, TypeAliasType):
|
|
71
|
+
annotation = _substituted(origin.__value__, get_args(annotation))
|
|
72
|
+
else:
|
|
73
|
+
return annotation
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _substituted(value: Any, arguments: tuple[Any, ...]) -> Any:
|
|
77
|
+
try:
|
|
78
|
+
return value[arguments]
|
|
79
|
+
except TypeError: # the alias does not use its type parameters
|
|
80
|
+
return value
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _annotated_function(fn: Callable[..., Any]) -> Callable[..., Any]:
|
|
84
|
+
if isinstance(fn, partial):
|
|
85
|
+
return _annotated_function(fn.func)
|
|
86
|
+
|
|
87
|
+
if isinstance(fn, type):
|
|
88
|
+
return fn.__init__
|
|
89
|
+
|
|
90
|
+
return fn if isroutine(fn) else type(fn).__call__
|