Skip to content

feat: export() writes DOT, Mermaid, GraphML, ELK JSON or a rendered image - #34

Merged
petercorke merged 4 commits into
mainfrom
feat/graph-export
Oct 11, 2026
Merged

petercorke merged 4 commits into
mainfrom
feat/graph-export

Conversation

@petercorke

Copy link
Copy Markdown
Owner

Summary

One method, g.export(filename=None, format=None, **options), writes the graph as DOT, Mermaid, GraphML, ELK JSON, or a rendered image.

g.export("graph.graphml")                    # format inferred from the extension
text = g.export(format="mermaid", direction="LR", fence=True)
g.export("graph.svg")                        # rendered with GraphViz
format Extensions Positions Options Needs
dot .dot .gv pos="x,y!" direction, colorcomponents, costs nothing
mermaid .mmd .mermaid no (not expressible) direction, colorcomponents, costs, fence nothing
graphml .graphml x, y, coord none nothing
elk .elk.json .elk x, y size, layout_options nothing
svg pdf png jpg gif ps same via neato engine, direction, colorcomponents, costs GraphViz executables
  • No new dependency. The text formats use only the standard library; the image formats run dot/neato directly and raise a clear RuntimeError if it is missing. (An earlier attempt imported the graphviz Python package at module top level, even for the text formats.)
  • Exporters are plain functions in pgraph.exporters, registered with @register_exporter(name, *extensions), so a format can be added without touching PGraph.py.
  • dotfile() and showgraph() are kept as thin wrappers, so this is additive.

Fixes over the old dotfile()

  • Reproducible output. It iterated graph.edges(), a set, so the order changed between runs. Edges are now written in the order of their vertices, and parallel edges and self-loops are all kept.
  • Escaping. Names containing ", \, a newline, <, & are escaped for each format; Mermaid, GraphML and ELK use generated ids so any name is valid.
  • Edge cost is carried (DOT/Mermaid label, GraphML cost, ELK properties.cost); dotfile() leaves it unlabelled as before.
  • showgraph() no longer uses shell=True, and reports a missing GraphViz instead of silently doing nothing.
  • A path is closed even if writing fails; a file object is never closed.

For review

  • export() returns the text when no filename is given and returns None when it writes, whereas dotfile() prints. Easy to change; I preferred composability.
  • showgraph(display=False) returns the PDF path (useful for tests and notebooks).
  • Mermaid cannot express positions, so coordinates are dropped there; this is documented.

Tests

tests/test_export.py, 50 tests: golden strings for dot and mermaid, parse-back for GraphML (ElementTree) and ELK (json), the multigraph and self-loop cases, escaping, reproducibility, dispatch and errors, a user-registered exporter, and real GraphViz rendering (skipped when dot is absent; the missing-program error path is tested without it). Full suite 213 passing, mypy and pyflakes clean on the changed files.

Recovered from an earlier uncommitted attempt (2026-09-02), then ported to the current code and redesigned; see claude-notes/export-formats-design.md locally.

🤖 Generated with Claude Code

petercorke and others added 2 commits October 11, 2026 14:32
…mage

One method, g.export(filename=None, format=None, **options), replaces the
idea of a method per format. The format is inferred from the extension
when not given (.dot .gv .mmd .graphml .elk.json .svg .pdf .png ...).
Exporters are plain functions in pgraph.exporters, registered by name, so
a user can add one with @register_exporter.

- text formats (dot, mermaid, graphml, elk) use only the standard library
- image formats (svg, pdf, png, ...) run the GraphViz dot/neato executable,
  neato when any vertex has a coordinate so that positions are honoured,
  and raise a clear RuntimeError if it is missing
- reproducible output: vertices in creation order, edges in the order of
  their vertices, rather than iterating the edge set
- names are escaped per format; Mermaid/GraphML/ELK use generated ids
- every edge is written, so parallel edges and self-loops are preserved
- a path is closed even on error; a file object is never closed

dotfile() and showgraph() are kept on top of it, with dotfile()'s output
unchanged apart from the escaping and ordering fixes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ring

Rendering tests use the real GraphViz executables and are skipped when
they are not installed; the missing-program error path is tested without.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@codacy-production

codacy-production Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 175 complexity · 0 duplication

Metric Results
Complexity 175
Duplication 0

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@codecov

codecov Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.58848% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 93.27%. Comparing base (7c68cab) to head (099c699).

Files with missing lines Patch % Lines
src/pgraph/exporters.py 99.47% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #34      +/-   ##
==========================================
+ Coverage   90.76%   93.27%   +2.50%     
==========================================
  Files           2        3       +1     
  Lines         845     1055     +210     
==========================================
+ Hits          767      984     +217     
+ Misses         78       71       -7     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

petercorke and others added 2 commits October 11, 2026 14:35
- build GraphML as escaped text rather than with xml.etree (we only ever
  generate XML, never parse it); characters XML 1.0 cannot represent are
  replaced with U+FFFD so the output is always well formed
- the GraphViz engine is checked against a fixed list of programs and the
  image format against a fixed list of formats; run with an argument list
  and no shell
- narrow edge endpoints in _ordered_edges instead of using assert
- do not shadow the builtins id / format in internal helpers

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@petercorke
petercorke merged commit d22408d into main Oct 11, 2026
14 checks passed
@petercorke
petercorke deleted the feat/graph-export branch October 11, 2026 04:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant