Skip to content

feat: Mermaid diagrams, transition tables, and format() support#595

Merged
fgmacedo merged 10 commits intodevelopfrom
feat/mermaid-diagrams
Mar 8, 2026
Merged

feat: Mermaid diagrams, transition tables, and format() support#595
fgmacedo merged 10 commits intodevelopfrom
feat/mermaid-diagrams

Conversation

@fgmacedo
Copy link
Owner

@fgmacedo fgmacedo commented Mar 8, 2026

Summary

  • Mermaid renderer — new MermaidRenderer produces stateDiagram-v2 source from the diagram IR, with a workaround for mermaid-js/mermaid#4052 (compound states inside parallel regions). Cross-boundary transitions (e.g., targeting a history pseudo-state inside a compound) are correctly rendered at the parent scope.

  • Transition table rendererTransitionTableRenderer produces Markdown or reStructuredText tables from the diagram IR.

  • Formatter facade — decorator-based format registry with built-in formats: mermaid, md/markdown, rst, dot, svg. Powers both format(sm, "mermaid") / f"{sm:mermaid}" and the CLI.

  • StateChart.__format__ — delegates to Formatter, enabling f-string and format() usage.

  • Docstring auto-expansion{statechart:FORMAT} placeholders in class docstrings are replaced at class creation time (via the metaclass) with the rendered diagram text.

  • Sphinx directive:format: mermaid option on statemachine-diagram emits a Mermaid node (via sphinxcontrib-mermaid).

  • CLI enhancements--format mermaid|md|rst and stdout output (-).

  • Visual showcase — every showcase section in docs/diagram.md now includes both Graphviz and Mermaid renderings side by side, plus a new "Parallel with cross-boundary transitions" section.

  • Documentation — revised diagram.md, tutorial, and 3.1.0 release notes.

Examples

f-string / format()

from tests.machines.showcase_simple import SimpleSC

sm = SimpleSC()

print(f"{sm:mermaid}")
# stateDiagram-v2
#     direction LR
#     state "Idle" as idle
#     state "Running" as running
#     state "Done" as done
#     [*] --> idle
#     done --> [*]
#     idle --> running : start
#     running --> done : finish
#
#     classDef active fill:#40E0D0,stroke:#333
#     idle:::active

print(format(SimpleSC, "md"))
# | State   | Event  | Guard | Target  |
# | ------- | ------ | ----- | ------- |
# | Idle    | start  |       | Running |
# | Running | finish |       | Done    |

Docstring auto-expansion

class SimpleSC(StateChart):
    """A simple three-state machine.

    {statechart:rst}
    """
    idle = State(initial=True)
    running = State()
    done = State(final=True)

    start = idle.to(running)
    finish = running.to(done)

At class creation time, {statechart:rst} is replaced with the rendered table:

>>> print(SimpleSC.__doc__)
A simple three-state machine.

+---------+--------+-------+---------+
| State   | Event  | Guard | Target  |
+=========+========+=======+=========+
| Idle    | start  |       | Running |
+---------+--------+-------+---------+
| Running | finish |       | Done    |
+---------+--------+-------+---------+

Mermaid workaround for parallel regions

Transitions targeting a compound state inside a parallel region crash Mermaid (mermaid-js/mermaid#4052). The renderer redirects them to the compound's initial child:

class ParallelCompoundSC(StateChart):
    class pipeline(State.Parallel, name="Pipeline"):
        class build(State.Compound, name="Build"):
            compile = State(initial=True)
            link = State(final=True)
            do_build = compile.to(link)

        class test(State.Compound, name="Test"):
            unit = State(initial=True)
            e2e = State(final=True)
            do_test = unit.to(e2e)

    idle = State(initial=True)
    review = State()

    start = idle.to(pipeline)
    done_state_pipeline = pipeline.to(review)
    rebuild = review.to(pipeline.build)   # targets compound inside parallel!
    accept = review.to(idle)

In the Mermaid output, rebuild is redirected from the compound to its initial child:

review --> compile : rebuild   # redirected: build -> compile (initial child)

Graphviz renders the arrow to the Build compound border; Mermaid redirects it to Compile.

CLI

# Mermaid to stdout
python -m statemachine.contrib.diagram myapp.MyMachine - --format mermaid

# Markdown table to file
python -m statemachine.contrib.diagram myapp.MyMachine table.md --format md

Sphinx directive

.. statemachine-diagram:: myapp.MyMachine
   :format: mermaid
   :caption: State diagram (Mermaid)

fgmacedo added 9 commits March 8, 2026 12:07
- Add MermaidRenderer that converts DiagramGraph IR to Mermaid
  stateDiagram-v2 source (compound, parallel, history, guards, etc.)
- Add TransitionTableRenderer for markdown and RST table output
- Add MermaidGraphMachine facade mirroring DotGraphMachine
- Add __format__ to StateChart and StateMachineMetaclass supporting
  dot, mermaid, md/markdown, and rst format specs
- Extend CLI with --format option (mermaid, md, rst) and stdout support
- Add :format: option to Sphinx statemachine-diagram directive with
  sphinxcontrib-mermaid integration
- Update docs with new sections and doctests
…stry

Replace duplicated if/elif chains in StateChart.__format__ and
StateMachineMetaclass.__format__ with a Formatter class that uses
a decorator-based registry following the Open/Closed Principle.

Adding a new format now requires only a decorated function — no
changes to __format__, factory.py, or statemachine.py.
- Register "svg" format in Formatter (DOT → SVG decoded as str)
- Refactor Sphinx directive to use formatter.render() for both SVG
  and Mermaid instead of calling DotGraphMachine/MermaidGraphMachine
  directly
- Update _prepare_svg and _resolve_target to work with str (not bytes)
The metaclass now detects {statechart:FORMAT} placeholders in docstrings
and replaces them at class definition time with the rendered output.
The docstring always stays in sync with the actual states and transitions.

Any registered format works: md, rst, mermaid, dot, etc.
Indentation of the placeholder line is preserved in the output.
- Reorganize into unified "Text representations" section with format
  table (name, aliases, description, dependencies)
- Add formatter API section with render(), supported_formats(), and
  custom format registration example
- Add live Mermaid directive example in Sphinx section
- Add --format dot to CLI examples
- Replace MermaidGraphMachine usage with formatter
- Add autodoc integration example with SimpleSC
- Add auto-expanding docstrings section with format recommendations
- Update release notes
Mermaid's stateDiagram-v2 crashes when a transition targets or originates
from a compound state inside a parallel region (mermaid-js/mermaid#4052).
The MermaidRenderer now redirects such endpoints to the compound's initial
child state.

Also filter dot-form event aliases (e.g. done.invoke.X) from diagram
output — the fix lives in the extractor so all renderers benefit.

Closes #594
…workaround to parallel regions

The Mermaid renderer had two issues:

1. Cross-scope transitions (e.g., an outer state targeting a history
   pseudo-state inside a compound) were silently dropped because
   `_render_scope_transitions` only rendered transitions where both
   endpoints were direct members of the same scope. Now the scope check
   expands to include descendants of compound states, while skipping
   transitions fully internal to a single compound (handled by the
   inner scope).

2. The compound→initial-child redirect (workaround for
   mermaid-js/mermaid#4052) was applied universally, but the bug only
   affects compound states inside parallel regions. Now the redirect is
   restricted to parallel descendants, leaving compound states outside
   parallel regions unchanged.

Adds a ParallelCompoundSC showcase that exercises the Mermaid bug
pattern (transition targeting a compound inside a parallel region),
with Graphviz vs Mermaid comparison in the visual showcase docs.
@codecov
Copy link

codecov bot commented Mar 8, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (19556ce) to head (fc082f2).
⚠️ Report is 1 commits behind head on develop.

Additional details and impacted files
@@            Coverage Diff             @@
##           develop      #595    +/-   ##
==========================================
  Coverage   100.00%   100.00%            
==========================================
  Files           39        42     +3     
  Lines         4597      5007   +410     
  Branches       734       813    +79     
==========================================
+ Hits          4597      5007   +410     
Flag Coverage Δ
unittests 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 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.

Add tests for the :name: directive option on Mermaid format (with and
without caption).  Mark the defensive dedup guard in _format_event_names
as pragma: no branch since Events already deduplicates at the container
level.
@sonarqubecloud
Copy link

sonarqubecloud bot commented Mar 8, 2026

@fgmacedo fgmacedo merged commit 2476d20 into develop Mar 8, 2026
14 checks passed
@fgmacedo fgmacedo deleted the feat/mermaid-diagrams branch March 8, 2026 20:21
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