Exporting standalone Python
The exporter turns a DependencyGraph into a standalone Python module:
from excel_grapher.exporter import CodeGenerator
code = CodeGenerator(graph).generate(targets)
print("\n".join(code.splitlines()[:120]))When graph nodes are target-marked (Node.is_target=True), generate() and generate_modules() can infer export targets directly from the graph:
with CodeGenerator(graph) as gen:
code = gen.generate() # defaults to graph.target_keys()If neither explicit targets nor target-marked nodes are available, code generation raises a ValueError.
For a multi-file package, generate_modules() returns a dict[str, str] keyed by module filename (__init__.py, api.py, data.py, runtime.py, internals.py). Write those files into a package directory of your choice, then import the package as usual.
Series bindings
Optional sidecar manifests (.bindings.yaml) declare structured input/output APIs on exported code. See the dedicated Series bindings guide for the JSON Schema, authoring conventions, validation rules, and Python API. Minimal codegen hook:
from pathlib import Path
from excel_grapher.exporter import CodeGenerator
from excel_grapher.series_bindings import load_series_bindings
workbook_path = Path("lic_inputs.xlsx")
bindings = load_series_bindings(workbook_path.with_suffix(".bindings.yaml"))
code = CodeGenerator(graph).generate(
targets,
series_bindings=bindings,
bindings_workbook=workbook_path,
)Series docstring callbacks
When exporting series bindings, you can attach structured docstrings to generated set_* and series output compute_* functions by passing a callback directly or by registering a reusable callback name for project-wide use:
from excel_grapher.exporter import (
CodeGenerator,
FieldDoc,
GoogleSeriesDocstringRenderer,
SeriesFunctionDoc,
register_series_docstring_callback,
)
def my_series_docstring(ctx):
# ctx.contract contains deterministic binding facts (fields, examples, layout, etc.)
return SeriesFunctionDoc(
summary=f"Set {ctx.contract.series_id}.",
purpose="Updates workbook inputs from Records.",
record_matching="Records match by key fields.",
field_descriptions={
field: FieldDoc(description=f"Describe {field}.")
for field in ctx.contract.required_fields
},
)
register_series_docstring_callback("my_docs", my_series_docstring)
def compact_google_renderer(contract, doc, *, series=None):
# Simple callables are valid custom renderers.
return GoogleSeriesDocstringRenderer().render(contract, doc, series=series)
code = CodeGenerator(graph).generate(
targets,
series_bindings=bindings,
bindings_workbook=workbook_path,
series_docstring_callback="my_docs", # or my_series_docstring
docstring_renderer=compact_google_renderer, # or "plain" | "rst" | "google" | "numpy"
)Callbacks return SeriesFunctionDoc prose; the exporter renders deterministic sections such as accepted fields, source binding metadata, and example calls. Return None to omit a docstring.
Callback contract highlights:
- The callback is invoked once per generated series API function (
set_*and series outputcompute_*). - Use
ctx.function_kind("setter"or"compute") andctx.function_nameto branch wording per function. ctx.contractcontains deterministic binding facts for that specific function (required fields, field dtypes, examples, layout, and data range).- Docstring callbacks may be direct callables or registered names (
series_docstring_callback="..."). - Docstring renderers can be built-in names (
"plain","rst","google","numpy"), renderer objects implementing SeriesDocstringRenderer, or simple callables(contract, doc, *, series=None) -> str. - If callback rendering is requested, codegen requires full series context (
series_bindings+bindings_workbook).
Graph projection for export
Identity transit nodes (pure mirrors such as Outputs!B12 = Engine!C6) can be collapsed in projected export artifacts while the canonical workbook graph stays unchanged. Pass a ProjectionResult to CodeGenerator so public targets and series bindings keep their workbook-facing addresses:
from excel_grapher.exporter import CodeGenerator, IdentityTransitCompression
projection = IdentityTransitCompression().project(graph)
modules = CodeGenerator(projection).generate_modules(
targets,
series_bindings=series_bindings,
bindings_workbook=workbook_path,
)ProjectionResult behaves like the projected graph for dependency reads, and exposes original_graph plus a serializable manifest. The manifest separates address projection (map_to_projected, used by codegen to keep removed public addresses resolving) from collapse lineage (retained_to_collapsed_sources, removed_node_snapshots with original formulas and metadata, collapsed_groups, formula_rewrites) for audit and refactoring. Visualization can use the same facade:
from excel_grapher.grapher import to_networkx
from excel_grapher.exporter import to_web_viz_payload
payload = to_web_viz_payload(to_networkx(projection))Projection is an extension point: implement the ProjectionStep protocol to build your own projected graph (see the contributing guide), and compose steps with apply_projection. In-place DependencyGraph.compress_identity_transits() remains available for destructive graph simplification; prefer projections for export and viz workflows.
Exported code sketch
A (truncated) sketch of the exported code:
"""Standalone runtime for generated Excel formula code."""
from __future__ import annotations
from enum import Enum
class XlError(str, Enum):
"""Excel error values."""
VALUE = "#VALUE!"
REF = "#REF!"
DIV = "#DIV/0!"
NA = "#N/A"
NAME = "#NAME?"
NUM = "#NUM!"
NULL = "#NULL!"
def to_number(value) -> float | XlError:
...
def xl_mul(left, right) -> float | XlError:
...
from functools import lru_cache
# --- Cell functions ---
@lru_cache(maxsize=None)
def cell_s_a1():
"""Leaf cell: S!A1"""
return 10
@lru_cache(maxsize=None)
def cell_s_b1():
"""Formula: =A1*2"""
return xl_mul(cell_s_a1(), 2.0)
def compute_all():
"""Compute all target cells and return results."""
return {
"S!B1": cell_s_b1(),
}Exported code results
namespace: dict = {}
exec(code, namespace)
generated_results = namespace["compute_all"]()
print(generated_results)
# {'S!B1': 20.0}Tradeoffs for the exported-code approach:
- Advantages
- Standalone artifact: output is plain Python; no need to distribute
excel_grapheror the evaluator with it. - Partial obfuscation: does not expose the extraction engine directly.
- Minimal runtime surface: embeds only the Excel-equivalent
xl_*helpers actually needed by the exported graph. - Repeatable execution: freezes workbook logic at a point in time; downstream runs are deterministic and Excel-free.
- Standalone artifact: output is plain Python; no need to distribute
- Disadvantages
- Still Excel-shaped: the structure is still cell-centric and Excel-like; interpretability gains are incremental.
- Regeneration required: changes to the workbook require re-extracting and re-exporting.