flowrep 0.5.0__tar.gz → 0.5.2__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.
Files changed (50) hide show
  1. {flowrep-0.5.0 → flowrep-0.5.2}/.gitignore +1 -1
  2. {flowrep-0.5.0 → flowrep-0.5.2}/PKG-INFO +11 -13
  3. {flowrep-0.5.0 → flowrep-0.5.2}/docs/README.md +7 -9
  4. {flowrep-0.5.0 → flowrep-0.5.2}/pyproject.toml +3 -3
  5. {flowrep-0.5.0 → flowrep-0.5.2}/src/flowrep/__init__.py +6 -4
  6. {flowrep-0.5.0 → flowrep-0.5.2}/src/flowrep/_version.py +2 -2
  7. flowrep-0.5.2/src/flowrep/api/__init__.py +15 -0
  8. flowrep-0.5.2/src/flowrep/api/schemas.py +51 -0
  9. flowrep-0.5.2/src/flowrep/api/tools.py +17 -0
  10. flowrep-0.5.2/src/flowrep/compiler/__init__.py +4 -0
  11. flowrep-0.5.2/src/flowrep/compiler/annotate.py +141 -0
  12. flowrep-0.5.2/src/flowrep/compiler/flow_control.py +384 -0
  13. flowrep-0.5.2/src/flowrep/compiler/function.py +362 -0
  14. flowrep-0.5.2/src/flowrep/compiler/source.py +270 -0
  15. flowrep-0.5.2/src/flowrep/compiler/statements.py +345 -0
  16. {flowrep-0.5.0 → flowrep-0.5.2}/src/flowrep/converters/python_workflow_definition.py +1 -1
  17. {flowrep-0.5.0 → flowrep-0.5.2}/src/flowrep/parsers/atomic_parser.py +14 -8
  18. {flowrep-0.5.0 → flowrep-0.5.2}/src/flowrep/parsers/case_helpers.py +1 -1
  19. {flowrep-0.5.0 → flowrep-0.5.2}/src/flowrep/parsers/for_parser.py +1 -1
  20. {flowrep-0.5.0 → flowrep-0.5.2}/src/flowrep/parsers/if_parser.py +1 -1
  21. {flowrep-0.5.0 → flowrep-0.5.2}/src/flowrep/parsers/parser_helpers.py +1 -1
  22. {flowrep-0.5.0 → flowrep-0.5.2}/src/flowrep/parsers/parser_protocol.py +1 -1
  23. {flowrep-0.5.0 → flowrep-0.5.2}/src/flowrep/parsers/symbol_scope.py +1 -1
  24. {flowrep-0.5.0 → flowrep-0.5.2}/src/flowrep/parsers/try_parser.py +1 -1
  25. {flowrep-0.5.0 → flowrep-0.5.2}/src/flowrep/parsers/while_parser.py +1 -1
  26. {flowrep-0.5.0 → flowrep-0.5.2}/src/flowrep/parsers/workflow_parser.py +1 -1
  27. {flowrep-0.5.0/src/flowrep/nodes → flowrep-0.5.2/src/flowrep/prospective}/__init__.py +2 -2
  28. {flowrep-0.5.0/src/flowrep/nodes → flowrep-0.5.2/src/flowrep/prospective}/for_recipe.py +2 -2
  29. {flowrep-0.5.0/src/flowrep/nodes → flowrep-0.5.2/src/flowrep/prospective}/helper_models.py +1 -1
  30. {flowrep-0.5.0/src/flowrep/nodes → flowrep-0.5.2/src/flowrep/prospective}/if_recipe.py +2 -2
  31. {flowrep-0.5.0/src/flowrep/nodes → flowrep-0.5.2/src/flowrep/prospective}/try_recipe.py +2 -2
  32. {flowrep-0.5.0/src/flowrep/nodes → flowrep-0.5.2/src/flowrep/prospective}/union_types.py +1 -1
  33. {flowrep-0.5.0/src/flowrep/nodes → flowrep-0.5.2/src/flowrep/prospective}/while_recipe.py +2 -2
  34. {flowrep-0.5.0/src/flowrep/nodes → flowrep-0.5.2/src/flowrep/prospective}/workflow_recipe.py +1 -1
  35. flowrep-0.5.0/src/flowrep/retrospective.py → flowrep-0.5.2/src/flowrep/retrospective/datastructures.py +32 -6
  36. {flowrep-0.5.0/src/flowrep → flowrep-0.5.2/src/flowrep/retrospective}/storage.py +29 -19
  37. {flowrep-0.5.0/src/flowrep → flowrep-0.5.2/src/flowrep/retrospective}/storage_widget.py +1 -1
  38. {flowrep-0.5.0 → flowrep-0.5.2}/src/flowrep/subgraph_validation.py +66 -29
  39. {flowrep-0.5.0 → flowrep-0.5.2}/src/flowrep/wfms.py +40 -55
  40. flowrep-0.5.0/src/flowrep/api/schemas.py +0 -50
  41. flowrep-0.5.0/src/flowrep/api/tools.py +0 -32
  42. {flowrep-0.5.0 → flowrep-0.5.2}/LICENSE +0 -0
  43. {flowrep-0.5.0 → flowrep-0.5.2}/src/flowrep/base_models.py +0 -0
  44. {flowrep-0.5.0/src/flowrep/api → flowrep-0.5.2/src/flowrep/converters}/__init__.py +0 -0
  45. {flowrep-0.5.0 → flowrep-0.5.2}/src/flowrep/edge_models.py +0 -0
  46. {flowrep-0.5.0/src/flowrep/converters → flowrep-0.5.2/src/flowrep/parsers}/__init__.py +0 -0
  47. {flowrep-0.5.0 → flowrep-0.5.2}/src/flowrep/parsers/label_helpers.py +0 -0
  48. {flowrep-0.5.0 → flowrep-0.5.2}/src/flowrep/parsers/object_scope.py +0 -0
  49. {flowrep-0.5.0/src/flowrep/nodes → flowrep-0.5.2/src/flowrep/prospective}/atomic_recipe.py +0 -0
  50. {flowrep-0.5.0/src/flowrep/parsers → flowrep-0.5.2/src/flowrep/retrospective}/__init__.py +0 -0
@@ -11,6 +11,6 @@ _build/
11
11
  apidoc/
12
12
  .ipynb_checkpoints/
13
13
  test_times.dat
14
- CLAUD.md
14
+ CLAUDE.md
15
15
  core.*
16
16
  src/flowrep/_version.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flowrep
3
- Version: 0.5.0
3
+ Version: 0.5.2
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.2.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.11; extra == 'storage'
57
+ Requires-Dist: bagofholding==0.1.12; extra == 'storage'
58
58
  Provides-Extra: storage-widget
59
- Requires-Dist: bagofholding==0.1.11; extra == 'storage-widget'
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 API:
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.api.tools.recipe2data`** converts a recipe from the prospective "class view"
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 = frt.recipe2data(double_and_add.flowrep_recipe)
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 = frt.run_recipe(
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 API:
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.api.tools.recipe2data`** converts a recipe from the prospective "class view"
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 = frt.recipe2data(double_and_add.flowrep_recipe)
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 = frt.run_recipe(
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.29.0", "hatch-vcs==0.5.0",]
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.2.1,<2.0.0",]
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.11",]
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.tools import atomic as atomic
10
- from flowrep.api.tools import parse_atomic as parse_atomic
11
- from flowrep.api.tools import parse_workflow as parse_workflow
12
- from flowrep.api.tools import workflow as workflow
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.0'
22
- __version_tuple__ = version_tuple = (0, 5, 0)
21
+ __version__ = version = '0.5.2'
22
+ __version_tuple__ = version_tuple = (0, 5, 2)
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,4 @@
1
+ """
2
+ This module handles the reverse transformation of flowrep objects back to python whence
3
+ they can be parsed.
4
+ """
@@ -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