flowrep 0.5.0__tar.gz → 0.5.1__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.
- {flowrep-0.5.0 → flowrep-0.5.1}/.gitignore +1 -1
- {flowrep-0.5.0 → flowrep-0.5.1}/PKG-INFO +11 -13
- {flowrep-0.5.0 → flowrep-0.5.1}/docs/README.md +7 -9
- {flowrep-0.5.0 → flowrep-0.5.1}/pyproject.toml +3 -3
- {flowrep-0.5.0 → flowrep-0.5.1}/src/flowrep/__init__.py +6 -4
- {flowrep-0.5.0 → flowrep-0.5.1}/src/flowrep/_version.py +2 -2
- flowrep-0.5.1/src/flowrep/api/__init__.py +15 -0
- flowrep-0.5.1/src/flowrep/api/schemas.py +51 -0
- flowrep-0.5.1/src/flowrep/api/tools.py +17 -0
- flowrep-0.5.1/src/flowrep/compiler/__init__.py +4 -0
- flowrep-0.5.1/src/flowrep/compiler/annotate.py +141 -0
- flowrep-0.5.1/src/flowrep/compiler/flow_control.py +384 -0
- flowrep-0.5.1/src/flowrep/compiler/function.py +362 -0
- flowrep-0.5.1/src/flowrep/compiler/source.py +270 -0
- flowrep-0.5.1/src/flowrep/compiler/statements.py +345 -0
- {flowrep-0.5.0 → flowrep-0.5.1}/src/flowrep/converters/python_workflow_definition.py +1 -1
- {flowrep-0.5.0 → flowrep-0.5.1}/src/flowrep/parsers/atomic_parser.py +1 -1
- {flowrep-0.5.0 → flowrep-0.5.1}/src/flowrep/parsers/case_helpers.py +1 -1
- {flowrep-0.5.0 → flowrep-0.5.1}/src/flowrep/parsers/for_parser.py +1 -1
- {flowrep-0.5.0 → flowrep-0.5.1}/src/flowrep/parsers/if_parser.py +1 -1
- {flowrep-0.5.0 → flowrep-0.5.1}/src/flowrep/parsers/parser_helpers.py +1 -1
- {flowrep-0.5.0 → flowrep-0.5.1}/src/flowrep/parsers/parser_protocol.py +1 -1
- {flowrep-0.5.0 → flowrep-0.5.1}/src/flowrep/parsers/symbol_scope.py +1 -1
- {flowrep-0.5.0 → flowrep-0.5.1}/src/flowrep/parsers/try_parser.py +1 -1
- {flowrep-0.5.0 → flowrep-0.5.1}/src/flowrep/parsers/while_parser.py +1 -1
- {flowrep-0.5.0 → flowrep-0.5.1}/src/flowrep/parsers/workflow_parser.py +1 -1
- {flowrep-0.5.0/src/flowrep/nodes → flowrep-0.5.1/src/flowrep/prospective}/__init__.py +2 -2
- {flowrep-0.5.0/src/flowrep/nodes → flowrep-0.5.1/src/flowrep/prospective}/for_recipe.py +2 -2
- {flowrep-0.5.0/src/flowrep/nodes → flowrep-0.5.1/src/flowrep/prospective}/helper_models.py +1 -1
- {flowrep-0.5.0/src/flowrep/nodes → flowrep-0.5.1/src/flowrep/prospective}/if_recipe.py +2 -2
- {flowrep-0.5.0/src/flowrep/nodes → flowrep-0.5.1/src/flowrep/prospective}/try_recipe.py +2 -2
- {flowrep-0.5.0/src/flowrep/nodes → flowrep-0.5.1/src/flowrep/prospective}/union_types.py +1 -1
- {flowrep-0.5.0/src/flowrep/nodes → flowrep-0.5.1/src/flowrep/prospective}/while_recipe.py +2 -2
- {flowrep-0.5.0/src/flowrep/nodes → flowrep-0.5.1/src/flowrep/prospective}/workflow_recipe.py +1 -1
- flowrep-0.5.0/src/flowrep/retrospective.py → flowrep-0.5.1/src/flowrep/retrospective/datastructures.py +6 -1
- {flowrep-0.5.0/src/flowrep → flowrep-0.5.1/src/flowrep/retrospective}/storage.py +20 -19
- {flowrep-0.5.0/src/flowrep → flowrep-0.5.1/src/flowrep/retrospective}/storage_widget.py +1 -1
- {flowrep-0.5.0 → flowrep-0.5.1}/src/flowrep/subgraph_validation.py +66 -29
- {flowrep-0.5.0 → flowrep-0.5.1}/src/flowrep/wfms.py +42 -53
- flowrep-0.5.0/src/flowrep/api/schemas.py +0 -50
- flowrep-0.5.0/src/flowrep/api/tools.py +0 -32
- {flowrep-0.5.0 → flowrep-0.5.1}/LICENSE +0 -0
- {flowrep-0.5.0 → flowrep-0.5.1}/src/flowrep/base_models.py +0 -0
- {flowrep-0.5.0/src/flowrep/api → flowrep-0.5.1/src/flowrep/converters}/__init__.py +0 -0
- {flowrep-0.5.0 → flowrep-0.5.1}/src/flowrep/edge_models.py +0 -0
- {flowrep-0.5.0/src/flowrep/converters → flowrep-0.5.1/src/flowrep/parsers}/__init__.py +0 -0
- {flowrep-0.5.0 → flowrep-0.5.1}/src/flowrep/parsers/label_helpers.py +0 -0
- {flowrep-0.5.0 → flowrep-0.5.1}/src/flowrep/parsers/object_scope.py +0 -0
- {flowrep-0.5.0/src/flowrep/nodes → flowrep-0.5.1/src/flowrep/prospective}/atomic_recipe.py +0 -0
- {flowrep-0.5.0/src/flowrep/parsers → flowrep-0.5.1/src/flowrep/retrospective}/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flowrep
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.1
|
|
4
4
|
Summary: flowrep - Your premier tool for workflow representations
|
|
5
5
|
Project-URL: Homepage, https://pyiron.org/
|
|
6
6
|
Project-URL: Documentation, https://flowrep.readthedocs.io
|
|
@@ -48,15 +48,15 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
48
48
|
Classifier: Topic :: Scientific/Engineering
|
|
49
49
|
Requires-Python: <3.15,>=3.11
|
|
50
50
|
Requires-Dist: pydantic<2.13.0,>=2.12.0
|
|
51
|
-
Requires-Dist: pyiron-snippets<2.0.0,>=1.
|
|
51
|
+
Requires-Dist: pyiron-snippets<2.0.0,>=1.3.0
|
|
52
52
|
Provides-Extra: notebooks
|
|
53
53
|
Requires-Dist: numpy==2.4.6; extra == 'notebooks'
|
|
54
54
|
Provides-Extra: pwd
|
|
55
55
|
Requires-Dist: python-workflow-definition==0.1.5; extra == 'pwd'
|
|
56
56
|
Provides-Extra: storage
|
|
57
|
-
Requires-Dist: bagofholding==0.1.
|
|
57
|
+
Requires-Dist: bagofholding==0.1.12; extra == 'storage'
|
|
58
58
|
Provides-Extra: storage-widget
|
|
59
|
-
Requires-Dist: bagofholding==0.1.
|
|
59
|
+
Requires-Dist: bagofholding==0.1.12; extra == 'storage-widget'
|
|
60
60
|
Requires-Dist: ipytree==0.2.2; extra == 'storage-widget'
|
|
61
61
|
Description-Content-Type: text/markdown
|
|
62
62
|
|
|
@@ -213,6 +213,8 @@ run-time, but their IO signature is always fully known a priori.
|
|
|
213
213
|
**Clear separation.** Of prospective, "class view" recipes, and retrospective,
|
|
214
214
|
"instance view" data objects.
|
|
215
215
|
|
|
216
|
+
**Convertable and compatible.** Recipes can be compiled back to python functions, and converted from [`python-workflow-definition`](https://github.com/pythonworkflow/python-workflow-definition/) recipes (also _to_ PWD, where recipe operations are the subset supported by the PWD framework).
|
|
217
|
+
|
|
216
218
|
### Example: flow control and nesting
|
|
217
219
|
|
|
218
220
|
So far we've seen `"workflow"` nodes, and alluded to `"atomic"` nodes.
|
|
@@ -315,20 +317,16 @@ these underlying functions when we run the recipe with a WfMS.
|
|
|
315
317
|
|
|
316
318
|
Recipes are *prospective* — they describe a computation template without holding
|
|
317
319
|
data. For retrospective analysis (inspecting what actually happened during a
|
|
318
|
-
run), flowrep provides two additional layers accessible through the
|
|
319
|
-
|
|
320
|
-
```python
|
|
321
|
-
>>> from flowrep.api import tools as frt
|
|
322
|
-
|
|
323
|
-
```
|
|
320
|
+
run), flowrep provides two additional layers accessible through the `tools` and
|
|
321
|
+
`schemas` submodules of the API.
|
|
324
322
|
|
|
325
|
-
**`flowrep.
|
|
323
|
+
**`flowrep.tools.recipe2data`** converts a recipe from the prospective "class view"
|
|
326
324
|
to the retrospective "instance view" — a mutable data structure whose input and output
|
|
327
325
|
ports can hold actual Python values. Retrospective data objects mirror the recipe graph
|
|
328
326
|
but trade JSON-serializability for the ability to carry arbitrary data:
|
|
329
327
|
|
|
330
328
|
```python
|
|
331
|
-
>>> wf_data =
|
|
329
|
+
>>> wf_data = fr.tools.recipe2data(double_and_add.flowrep_recipe)
|
|
332
330
|
|
|
333
331
|
```
|
|
334
332
|
|
|
@@ -338,7 +336,7 @@ by a minimal, built-in WfMS intended as a reference implementation and for use
|
|
|
338
336
|
in tests and documentation (like this!):
|
|
339
337
|
|
|
340
338
|
```python
|
|
341
|
-
>>> retrospective =
|
|
339
|
+
>>> retrospective = fr.tools.run_recipe(
|
|
342
340
|
... double_and_add.flowrep_recipe, a=3, b=100, target=40
|
|
343
341
|
... )
|
|
344
342
|
>>> retrospective.output_ports["result"].value
|
|
@@ -151,6 +151,8 @@ run-time, but their IO signature is always fully known a priori.
|
|
|
151
151
|
**Clear separation.** Of prospective, "class view" recipes, and retrospective,
|
|
152
152
|
"instance view" data objects.
|
|
153
153
|
|
|
154
|
+
**Convertable and compatible.** Recipes can be compiled back to python functions, and converted from [`python-workflow-definition`](https://github.com/pythonworkflow/python-workflow-definition/) recipes (also _to_ PWD, where recipe operations are the subset supported by the PWD framework).
|
|
155
|
+
|
|
154
156
|
### Example: flow control and nesting
|
|
155
157
|
|
|
156
158
|
So far we've seen `"workflow"` nodes, and alluded to `"atomic"` nodes.
|
|
@@ -253,20 +255,16 @@ these underlying functions when we run the recipe with a WfMS.
|
|
|
253
255
|
|
|
254
256
|
Recipes are *prospective* — they describe a computation template without holding
|
|
255
257
|
data. For retrospective analysis (inspecting what actually happened during a
|
|
256
|
-
run), flowrep provides two additional layers accessible through the
|
|
257
|
-
|
|
258
|
-
```python
|
|
259
|
-
>>> from flowrep.api import tools as frt
|
|
260
|
-
|
|
261
|
-
```
|
|
258
|
+
run), flowrep provides two additional layers accessible through the `tools` and
|
|
259
|
+
`schemas` submodules of the API.
|
|
262
260
|
|
|
263
|
-
**`flowrep.
|
|
261
|
+
**`flowrep.tools.recipe2data`** converts a recipe from the prospective "class view"
|
|
264
262
|
to the retrospective "instance view" — a mutable data structure whose input and output
|
|
265
263
|
ports can hold actual Python values. Retrospective data objects mirror the recipe graph
|
|
266
264
|
but trade JSON-serializability for the ability to carry arbitrary data:
|
|
267
265
|
|
|
268
266
|
```python
|
|
269
|
-
>>> wf_data =
|
|
267
|
+
>>> wf_data = fr.tools.recipe2data(double_and_add.flowrep_recipe)
|
|
270
268
|
|
|
271
269
|
```
|
|
272
270
|
|
|
@@ -276,7 +274,7 @@ by a minimal, built-in WfMS intended as a reference implementation and for use
|
|
|
276
274
|
in tests and documentation (like this!):
|
|
277
275
|
|
|
278
276
|
```python
|
|
279
|
-
>>> retrospective =
|
|
277
|
+
>>> retrospective = fr.tools.run_recipe(
|
|
280
278
|
... double_and_add.flowrep_recipe, a=3, b=100, target=40
|
|
281
279
|
... )
|
|
282
280
|
>>> retrospective.output_ports["result"].value
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = [ "hatchling==1.
|
|
2
|
+
requires = [ "hatchling==1.30.1", "hatch-vcs==0.5.0",]
|
|
3
3
|
build-backend = "hatchling.build"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
@@ -9,7 +9,7 @@ readme = "docs/README.md"
|
|
|
9
9
|
keywords = [ "pyiron",]
|
|
10
10
|
requires-python = ">=3.11, <3.15"
|
|
11
11
|
classifiers = [ "Development Status :: 4 - Beta", "Topic :: Scientific/Engineering", "License :: OSI Approved :: BSD License", "Intended Audience :: Science/Research", "Operating System :: OS Independent", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14",]
|
|
12
|
-
dependencies = [ "pydantic>=2.12.0,<2.13.0", "pyiron_snippets>=1.
|
|
12
|
+
dependencies = [ "pydantic>=2.12.0,<2.13.0", "pyiron_snippets>=1.3.0,<2.0.0",]
|
|
13
13
|
dynamic = [ "version",]
|
|
14
14
|
[[project.authors]]
|
|
15
15
|
name = "Sam Waseda"
|
|
@@ -30,7 +30,7 @@ Repository = "https://github.com/pyiron/flowrep"
|
|
|
30
30
|
[project.optional-dependencies]
|
|
31
31
|
pwd = [ "python-workflow-definition==0.1.5",]
|
|
32
32
|
notebooks = [ "numpy==2.4.6",]
|
|
33
|
-
storage = [ "bagofholding==0.1.
|
|
33
|
+
storage = [ "bagofholding==0.1.12",]
|
|
34
34
|
storage-widget = [ "flowrep[storage]", "ipytree==0.2.2",]
|
|
35
35
|
|
|
36
36
|
[tool.ruff]
|
|
@@ -6,10 +6,12 @@ attribute onto a function at definition time, and parsers (`parse_atomic`,
|
|
|
6
6
|
|
|
7
7
|
import importlib.metadata
|
|
8
8
|
|
|
9
|
-
from flowrep.api
|
|
10
|
-
from flowrep.api
|
|
11
|
-
from flowrep.api
|
|
12
|
-
from flowrep.api
|
|
9
|
+
from flowrep.api import atomic as atomic
|
|
10
|
+
from flowrep.api import parse_atomic as parse_atomic
|
|
11
|
+
from flowrep.api import parse_workflow as parse_workflow
|
|
12
|
+
from flowrep.api import schemas as schemas
|
|
13
|
+
from flowrep.api import tools as tools
|
|
14
|
+
from flowrep.api import workflow as workflow
|
|
13
15
|
|
|
14
16
|
try:
|
|
15
17
|
# Installed package will find its version
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.5.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 5,
|
|
21
|
+
__version__ = version = '0.5.1'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 5, 1)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The public interface for flowrep, protected by semver
|
|
3
|
+
|
|
4
|
+
The most fundamental user tools (parsing and decorating) are provided here at the top
|
|
5
|
+
level, while other resources are nested by category as tools or schemas. The shorter
|
|
6
|
+
tools list is likely to be useful for all power users, while schemas are intended to
|
|
7
|
+
primarily benefit downstream developers (e.g. WfMS creators).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from flowrep.api import schemas as schemas
|
|
11
|
+
from flowrep.api import tools as tools
|
|
12
|
+
from flowrep.api.tools import atomic as atomic
|
|
13
|
+
from flowrep.api.tools import parse_atomic as parse_atomic
|
|
14
|
+
from flowrep.api.tools import parse_workflow as parse_workflow
|
|
15
|
+
from flowrep.api.tools import workflow as workflow
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pydantic models, types and classes, enums, and constants used in constructing or
|
|
3
|
+
inspecting recipe models.
|
|
4
|
+
|
|
5
|
+
Primarily intended for power-users like workflow management system (WfMS) designers to
|
|
6
|
+
work deeply with recipe objects in a structured, well-typed way.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from flowrep.base_models import RESERVED_NAMES as RESERVED_NAMES
|
|
10
|
+
from flowrep.base_models import Label as Label
|
|
11
|
+
from flowrep.base_models import Labels as Labels
|
|
12
|
+
from flowrep.base_models import NodeRecipe as NodeRecipe
|
|
13
|
+
from flowrep.base_models import PythonReference as PythonReference
|
|
14
|
+
from flowrep.base_models import RecipeElementType as RecipeElementType
|
|
15
|
+
from flowrep.base_models import RestrictedParamKind as RestrictedParamKind
|
|
16
|
+
from flowrep.compiler.source import RenderedSource as RenderedSource
|
|
17
|
+
from flowrep.edge_models import Edges as Edges
|
|
18
|
+
from flowrep.edge_models import InputEdges as InputEdges
|
|
19
|
+
from flowrep.edge_models import InputSource as InputSource
|
|
20
|
+
from flowrep.edge_models import OutputEdges as OutputEdges
|
|
21
|
+
from flowrep.edge_models import OutputTarget as OutputTarget
|
|
22
|
+
from flowrep.edge_models import SourceHandle as SourceHandle
|
|
23
|
+
from flowrep.edge_models import TargetHandle as TargetHandle
|
|
24
|
+
from flowrep.prospective.atomic_recipe import AtomicRecipe as AtomicRecipe
|
|
25
|
+
from flowrep.prospective.atomic_recipe import UnpackMode as UnpackMode
|
|
26
|
+
from flowrep.prospective.for_recipe import ForEachRecipe as ForEachRecipe
|
|
27
|
+
from flowrep.prospective.helper_models import ConditionalCase as ConditionalCase
|
|
28
|
+
from flowrep.prospective.helper_models import ExceptionCase as ExceptionCase
|
|
29
|
+
from flowrep.prospective.helper_models import LabeledRecipe as LabeledRecipe
|
|
30
|
+
from flowrep.prospective.if_recipe import IfRecipe as IfRecipe
|
|
31
|
+
from flowrep.prospective.try_recipe import TryRecipe as TryRecipe
|
|
32
|
+
from flowrep.prospective.union_types import RecipeDiscrimination as RecipeDiscrimination
|
|
33
|
+
from flowrep.prospective.union_types import Recipes as Recipes
|
|
34
|
+
from flowrep.prospective.while_recipe import WhileRecipe as WhileRecipe
|
|
35
|
+
from flowrep.prospective.workflow_recipe import WorkflowRecipe as WorkflowRecipe
|
|
36
|
+
from flowrep.retrospective.datastructures import NOT_DATA as NOT_DATA
|
|
37
|
+
from flowrep.retrospective.datastructures import AtomicData as AtomicData
|
|
38
|
+
from flowrep.retrospective.datastructures import CompositeData as CompositeData
|
|
39
|
+
from flowrep.retrospective.datastructures import DagData as DagData
|
|
40
|
+
from flowrep.retrospective.datastructures import FlowControlData as FlowControlData
|
|
41
|
+
from flowrep.retrospective.datastructures import ForEachData as ForEachData
|
|
42
|
+
from flowrep.retrospective.datastructures import IfData as IfData
|
|
43
|
+
from flowrep.retrospective.datastructures import InputDataPort as InputDataPort
|
|
44
|
+
from flowrep.retrospective.datastructures import InputDataPorts as InputDataPorts
|
|
45
|
+
from flowrep.retrospective.datastructures import NodeData as NodeData
|
|
46
|
+
from flowrep.retrospective.datastructures import NotData as NotData
|
|
47
|
+
from flowrep.retrospective.datastructures import OutputDataPort as OutputDataPort
|
|
48
|
+
from flowrep.retrospective.datastructures import OutputDataPorts as OutputDataPorts
|
|
49
|
+
from flowrep.retrospective.datastructures import TryData as TryData
|
|
50
|
+
from flowrep.retrospective.datastructures import WhileData as WhileData
|
|
51
|
+
from flowrep.subgraph_validation import ProspectiveOutputEdges as ProspectiveOutputEdges
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Functions for creating and transforming flowrep data.
|
|
3
|
+
|
|
4
|
+
Intended for users who want to go beyond the simple parsers and decorators, as well as
|
|
5
|
+
for downstream library developers.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from flowrep.compiler.source import flowrep2python as flowrep2python
|
|
9
|
+
from flowrep.converters.python_workflow_definition import flowrep2pwd as flowrep2pwd
|
|
10
|
+
from flowrep.converters.python_workflow_definition import pwd2flowrep as pwd2flowrep
|
|
11
|
+
from flowrep.parsers.atomic_parser import atomic as atomic
|
|
12
|
+
from flowrep.parsers.atomic_parser import parse_atomic as parse_atomic
|
|
13
|
+
from flowrep.parsers.workflow_parser import parse_workflow as parse_workflow
|
|
14
|
+
from flowrep.parsers.workflow_parser import workflow as workflow
|
|
15
|
+
from flowrep.retrospective.datastructures import recipe2data as recipe2data
|
|
16
|
+
from flowrep.retrospective.storage import LexicalBagBrowser as LexicalBagBrowser
|
|
17
|
+
from flowrep.wfms import run_recipe as run_recipe
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Render Python defaults and annotations into self-contained source text.
|
|
2
|
+
|
|
3
|
+
Each helper produces candidate source text and then verifies that the text
|
|
4
|
+
reconstructs the original object (render-then-verify). On any failure it returns
|
|
5
|
+
``None`` so the caller can fall back to binding the live object into a namespace.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import ast
|
|
11
|
+
import builtins
|
|
12
|
+
import importlib
|
|
13
|
+
import types
|
|
14
|
+
import typing
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from pyiron_snippets import versions
|
|
18
|
+
|
|
19
|
+
_ANNOTATED_ORIGIN_SENTINEL = typing.get_origin(typing.Annotated[int, "x"])
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def render_annotation(ann: Any, imports: set[str]) -> str | None:
|
|
23
|
+
"""Return verified source text for an annotation, or ``None``.
|
|
24
|
+
|
|
25
|
+
Mutates ``imports`` with the bare module name for every non-builtin module
|
|
26
|
+
the text references. Returns ``None`` (without leaving partial imports
|
|
27
|
+
behind) when any part of the annotation cannot be rendered or the rendered
|
|
28
|
+
text fails to evaluate back to an equal object.
|
|
29
|
+
"""
|
|
30
|
+
local_imports: set[str] = set()
|
|
31
|
+
text = _render(ann, local_imports)
|
|
32
|
+
if text is None:
|
|
33
|
+
return None
|
|
34
|
+
if text == "None": # None / NoneType both legal and normalized by get_type_hints
|
|
35
|
+
return "None"
|
|
36
|
+
if not _verifies(text, ann, local_imports): # pragma: no cover - defensive
|
|
37
|
+
return None
|
|
38
|
+
imports.update(local_imports)
|
|
39
|
+
return text
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _render(ann: Any, imports: set[str]) -> str | None:
|
|
43
|
+
if ann is None or ann is type(None):
|
|
44
|
+
return "None"
|
|
45
|
+
|
|
46
|
+
origin = typing.get_origin(ann)
|
|
47
|
+
if origin is not None:
|
|
48
|
+
return _render_subscripted(ann, origin, imports)
|
|
49
|
+
|
|
50
|
+
if isinstance(ann, type):
|
|
51
|
+
return _render_plain_type(ann, imports)
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _render_subscripted(ann: Any, origin: Any, imports: set[str]) -> str | None:
|
|
56
|
+
args = typing.get_args(ann)
|
|
57
|
+
|
|
58
|
+
# Union / Optional: render members joined by ` | `. typing.Union[...] has
|
|
59
|
+
# origin typing.Union; the PEP 604 `int | str` form has origin
|
|
60
|
+
# types.UnionType. Both compare equal after eval, so render both as `A | B`.
|
|
61
|
+
if origin is typing.Union or origin is types.UnionType:
|
|
62
|
+
rendered = [_render(a, imports) for a in args]
|
|
63
|
+
if any(part is None for part in rendered):
|
|
64
|
+
return None
|
|
65
|
+
return " | ".join(part for part in rendered if part is not None)
|
|
66
|
+
|
|
67
|
+
# Annotated[T, *meta]: base via _render, metadata via render_default first
|
|
68
|
+
# (literals) then _render (types).
|
|
69
|
+
if origin is _ANNOTATED_ORIGIN_SENTINEL:
|
|
70
|
+
base = _render(args[0], imports)
|
|
71
|
+
if base is None:
|
|
72
|
+
return None
|
|
73
|
+
meta_texts = []
|
|
74
|
+
for meta in args[1:]:
|
|
75
|
+
meta_text = render_default(meta)
|
|
76
|
+
if meta_text is None:
|
|
77
|
+
meta_text = _render(meta, imports)
|
|
78
|
+
if meta_text is None:
|
|
79
|
+
return None
|
|
80
|
+
meta_texts.append(meta_text)
|
|
81
|
+
return f"typing.Annotated[{', '.join([base, *meta_texts])}]"
|
|
82
|
+
|
|
83
|
+
# Literal[...]: every arg must be a literal value.
|
|
84
|
+
if origin is typing.Literal:
|
|
85
|
+
arg_texts = [render_default(a) for a in args]
|
|
86
|
+
if any(part is None for part in arg_texts):
|
|
87
|
+
return None
|
|
88
|
+
return f"typing.Literal[{', '.join(part for part in arg_texts if part)}]"
|
|
89
|
+
|
|
90
|
+
# Generic alias over a real origin type (list[int], dict[str, int], ...).
|
|
91
|
+
if isinstance(origin, type):
|
|
92
|
+
origin_text = _render_plain_type(origin, imports)
|
|
93
|
+
if origin_text is None:
|
|
94
|
+
return None
|
|
95
|
+
arg_texts = [_render(a, imports) for a in args]
|
|
96
|
+
if any(part is None for part in arg_texts):
|
|
97
|
+
return None
|
|
98
|
+
return f"{origin_text}[{', '.join(part for part in arg_texts if part)}]"
|
|
99
|
+
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _render_plain_type(ann: type, imports: set[str]) -> str | None:
|
|
104
|
+
info = versions.VersionInfo.of(ann)
|
|
105
|
+
if info.is_local or info.is_lambda or info.in_main:
|
|
106
|
+
return None
|
|
107
|
+
if info.module != "builtins":
|
|
108
|
+
imports.add(info.module)
|
|
109
|
+
return info.findable_at
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _verifies(text: str, ann: Any, imports: set[str]) -> bool:
|
|
113
|
+
namespace: dict[str, Any] = {"__builtins__": builtins}
|
|
114
|
+
namespace["typing"] = importlib.import_module("typing")
|
|
115
|
+
for line in imports:
|
|
116
|
+
top = line.split(".")[0]
|
|
117
|
+
namespace[top] = importlib.import_module(top)
|
|
118
|
+
try:
|
|
119
|
+
evaluated = eval(text, namespace) # noqa: S307 - controlled codegen text
|
|
120
|
+
except Exception: # noqa: BLE001 # pragma: no cover - defensive verify net
|
|
121
|
+
return False
|
|
122
|
+
return evaluated == ann or evaluated is ann
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def render_default(value: Any) -> str | None:
|
|
126
|
+
"""Return source text for a *literal* default, or ``None``.
|
|
127
|
+
|
|
128
|
+
The text is accepted only when ``ast.literal_eval`` reproduces a value equal
|
|
129
|
+
to (and of the same type as) the input. This covers ints, floats, strs,
|
|
130
|
+
bytes, bools, ``None``, and nested tuple/list/dict/set of these, and rejects
|
|
131
|
+
sentinels, enums, ``nan``, ``frozenset`` (no literal syntax), and arbitrary
|
|
132
|
+
instances. No imports are required for any accepted value.
|
|
133
|
+
"""
|
|
134
|
+
try:
|
|
135
|
+
text = repr(value)
|
|
136
|
+
parsed = ast.literal_eval(text)
|
|
137
|
+
except (ValueError, SyntaxError, TypeError, MemoryError, RecursionError):
|
|
138
|
+
return None
|
|
139
|
+
if type(parsed) is type(value) and parsed == value:
|
|
140
|
+
return text
|
|
141
|
+
return None
|