Source code for flowrep.compiler.annotate

"""Render Python defaults and annotations into self-contained source text.

Each helper produces candidate source text and then verifies that the text
reconstructs the original object (render-then-verify). On any failure it returns
``None`` so the caller can fall back to binding the live object into a namespace.
"""

from __future__ import annotations

import ast
import builtins
import importlib
import types
import typing
from typing import Any

from pyiron_snippets import versions

_ANNOTATED_ORIGIN_SENTINEL = typing.get_origin(typing.Annotated[int, "x"])


[docs] def render_annotation(ann: Any, imports: set[str]) -> str | None: """Return verified source text for an annotation, or ``None``. Mutates ``imports`` with the bare module name for every non-builtin module the text references. Returns ``None`` (without leaving partial imports behind) when any part of the annotation cannot be rendered or the rendered text fails to evaluate back to an equal object. """ local_imports: set[str] = set() text = _render(ann, local_imports) if text is None: return None if text == "None": # None / NoneType both legal and normalized by get_type_hints return "None" if not _verifies(text, ann, local_imports): # pragma: no cover - defensive return None imports.update(local_imports) return text
def _render(ann: Any, imports: set[str]) -> str | None: if ann is None or ann is type(None): return "None" origin = typing.get_origin(ann) if origin is not None: return _render_subscripted(ann, origin, imports) if isinstance(ann, type): return _render_plain_type(ann, imports) return None def _render_subscripted(ann: Any, origin: Any, imports: set[str]) -> str | None: args = typing.get_args(ann) # Union / Optional: render members joined by ` | `. typing.Union[...] has # origin typing.Union; the PEP 604 `int | str` form has origin # types.UnionType. Both compare equal after eval, so render both as `A | B`. if origin is typing.Union or origin is types.UnionType: rendered = [_render(a, imports) for a in args] if any(part is None for part in rendered): return None return " | ".join(part for part in rendered if part is not None) # Annotated[T, *meta]: base via _render, metadata via render_default first # (literals) then _render (types). if origin is _ANNOTATED_ORIGIN_SENTINEL: base = _render(args[0], imports) if base is None: return None meta_texts = [] for meta in args[1:]: meta_text = render_default(meta) if meta_text is None: meta_text = _render(meta, imports) if meta_text is None: return None meta_texts.append(meta_text) return f"typing.Annotated[{', '.join([base, *meta_texts])}]" # Literal[...]: every arg must be a literal value. if origin is typing.Literal: arg_texts = [render_default(a) for a in args] if any(part is None for part in arg_texts): return None return f"typing.Literal[{', '.join(part for part in arg_texts if part)}]" # Generic alias over a real origin type (list[int], dict[str, int], ...). if isinstance(origin, type): origin_text = _render_plain_type(origin, imports) if origin_text is None: return None arg_texts = [_render(a, imports) for a in args] if any(part is None for part in arg_texts): return None return f"{origin_text}[{', '.join(part for part in arg_texts if part)}]" return None def _render_plain_type(ann: type, imports: set[str]) -> str | None: info = versions.VersionInfo.of(ann) if info.is_local or info.is_lambda or info.in_main: return None if info.module != "builtins": imports.add(info.module) return info.findable_at def _verifies(text: str, ann: Any, imports: set[str]) -> bool: namespace: dict[str, Any] = {"__builtins__": builtins} namespace["typing"] = importlib.import_module("typing") for line in imports: top = line.split(".")[0] namespace[top] = importlib.import_module(top) try: evaluated = eval(text, namespace) # noqa: S307 - controlled codegen text except Exception: # noqa: BLE001 # pragma: no cover - defensive verify net return False return evaluated == ann or evaluated is ann
[docs] def render_default(value: Any) -> str | None: """Return source text for a *literal* default, or ``None``. The text is accepted only when ``ast.literal_eval`` reproduces a value equal to (and of the same type as) the input. This covers ints, floats, strs, bytes, bools, ``None``, and nested tuple/list/dict/set of these, and rejects sentinels, enums, ``nan``, ``frozenset`` (no literal syntax), and arbitrary instances. No imports are required for any accepted value. """ try: text = repr(value) parsed = ast.literal_eval(text) except (ValueError, SyntaxError, TypeError, MemoryError, RecursionError): return None if type(parsed) is type(value) and parsed == value: return text return None