Commit 4953985
authored
refactor: restructure diagram module and add Sphinx directive (#589)
* refactor: extract diagram module into package with IR + renderer separation
Refactor `statemachine/contrib/diagram.py` (single file) into a package
with separated concerns:
- `model.py`: intermediate representation (DiagramGraph, DiagramState,
DiagramTransition) as pure dataclasses
- `extract.py`: state machine → IR extraction logic
- `renderers/dot.py`: IR → pydot.Dot rendering with UML-inspired styling
- `__init__.py`: backwards-compatible facade (DotGraphMachine, etc.)
Key improvements:
- States with actions render as HTML TABLE labels with UML compartments
(name + separator + entry/exit/actions), inspired by state-machine-cat
- States without actions use native rounded rectangles
- Active/inactive states use consistent rounded shapes (no polygon fill
regression)
- Class diagrams no longer highlight initial state as active
- SVG shape consistency tests catch visual regressions automatically
- Visual showcase section in docs/diagram.md demonstrates all features
The public API is fully preserved: DotGraphMachine, quickchart_write_svg,
write_image, main, import_sm, and `python -m statemachine.contrib.diagram`.
* fix: avoid instantiating StateChart class in diagram extraction
The extract function now uses the class directly (never instantiates it)
since all structural metadata (states, transitions, name) is already
available on the class thanks to the metaclass. Active-state highlighting
is only produced when an instance is passed.
* feat: set default graph DPI to 200 for sharper diagram images
Increase the default Graphviz DPI from 96 to 200, producing noticeably
sharper PNG output. The setting is customizable via
DotGraphMachine.graph_dpi. Regenerate all documentation images at the
new resolution.
* refactor: improve diagram layout and edge clipping
- Use native shape for all states (with and without actions) so
Graphviz clips edges at the actual rounded border
- States with actions embed a border=0 HTML TABLE inside the native
shape for UML compartment layout
- Transition labels use HTML tables for better spacing
- Escape guard conditions with HTML entities
- Increase ranksep to 0.3, enable forcelabels, add labeldistance
- Regenerate all documentation images
* refactor: use is_initial flag and hide implicit initial transitions
- Add is_initial field to DiagramState IR, extracted from state metadata
- Use is_initial to determine which state gets the black-dot initial
arrow instead of relying on document order heuristics
- Skip rendering implicit initial transitions from compound/parallel
states to their initial child — these are already represented by the
black-dot initial node inside the cluster
- Skip initial arrows for parallel area children (all are auto-initial)
- Regenerate affected documentation images
* refactor: improve compound state edge routing with bidirectional anchors
- Compound states with both incoming and outgoing transitions get
separate _anchor_in/_anchor_out nodes for cleaner edge routing
- Move anchor nodes into the atomic subgraph so they share the same
rank region as real states, avoiding blank space in clusters
- Place inner initial dots in the atomic cluster for shorter arrows
- Extract _render_initial_arrow helper to reduce _render_states complexity
- Regenerate affected documentation images
* refactor: reduce compound state padding and fix integrations doctest
- Add margin="4" to compound/parallel subgraphs to reduce default
Graphviz cluster padding (was 8pt default)
- Place anchor nodes directly on parent graph when no atomic states
exist at that level, avoiding empty cluster whitespace
- Remove graph_dpi config (use Graphviz default)
- Fix integrations.md doctest: register CampaignMachine in registry
and skip Django autodiscovery to prevent ImproperlyConfigured error
- Merge doctest blocks so context carries across examples
- Regenerate all documentation images
* refactor: enrich diagram IR to separate extraction from rendering
Move domain analysis logic from the renderer (dot.py) into the
extractor (extract.py) so the renderer becomes a pure IR→pydot mapping:
- Add ActionType enum replacing free strings for DiagramAction.type
- Add compound_state_ids and bidirectional_compound_ids to DiagramGraph
- Add DiagramTransition.is_initial flag for implicit initial transitions
- Remove redundant DiagramTransition.target (use targets list)
- Move _collect_compound_ids, _collect_compound_bidir_ids from renderer
to extractor
- Add _mark_initial_transitions and _resolve_initial_states in extractor
- Remove _is_initial_candidate from renderer (use state.is_initial)
- Remove implicit transition filtering logic from renderer (use
transition.is_initial)
* feat: add statemachine-diagram Sphinx directive for inline diagram rendering
Add a Sphinx extension that renders state machine diagrams inline in docs
from an importable class path, eliminating manual DotGraphMachine/write_png
boilerplate. Supports standard image/figure options (width, height, scale,
align, target, class, name) and state-machine-specific options (events,
caption, figclass).
Key features:
- Inline SVG rendering (no intermediate files)
- :events: option to instantiate and advance the machine before rendering
- :target: (empty) auto-generates a standalone SVG for full-size zoom
- Responsive sizing: intrinsic width becomes max-width, scales down on
narrow viewports
- Compatible with any Sphinx theme (uses native align-center/left/right
classes)
Refactor diagram.md to use the directive for the visual showcase, replacing
doctest+write_png workflow. Extract showcase machines to tests/machines/
and use literalinclude with :pyobject: to display source code. Reorganize
the page narrative: _graph() as primary entry point, DotGraphMachine for
advanced customization.
* docs: add Sphinx directive release notes to 3.1.0
* test: add comprehensive tests for sphinx_ext directive (100% coverage)
Cover all untested code paths: _prepare_svg, _build_svg_styles,
_resolve_target, _build_wrapper_classes, _split_length, _align_spec,
setup, and the full run() method with various option combinations
(caption, events, alt, width, height, scale, align, target, class,
figclass, name) plus error handling for invalid imports and render
failures.
* test: reach 100% branch coverage for diagram package
Add tests for extract.py (deep history type, internal transitions with
and without actions, bidirectional compound detection, invalid input,
initial state fallback), dot.py (compound labels with actions, internal
action format, targetless transitions, non-bidirectional anchors), and
__main__.py (via runpy).
Mark unreachable branch in extract.py (history states never have
substates) with pragma: no cover, and add `if __name__` to coverage
exclusions in pyproject.toml.
* docs: add TDD and branch coverage requirements to AGENTS.md
Document that tests are first-class planning requirements, not
afterthoughts. 100% branch coverage is mandatory (enforced by CI),
coverage must be verified before committing, and pytest fixtures
should be used instead of hardcoded paths.
* refactor: fix SonarCloud maintainability issues in diagram package
- Use pytest.approx for float comparisons in _split_length tests
- Rename unused variables to _ in _prepare_svg tests
- Remove unused 'getter' parameter from _extract_transitions_from_state
and _extract_all_transitions
- Reduce cognitive complexity of _create_edges (30→~10) by extracting
_create_single_edge, _resolve_edge_endpoints, and _build_edge_label
- Reduce cognitive complexity of _render_states (17→~12) by extracting
_place_extra_nodes static method
* docs: replace inline diagram generation with statemachine-diagram directive
Convert tutorial.md and transitions.md to use the directive instead of
doctest write_png + image references. Extract CoffeeOrder, OrderWorkflow,
and OrderWorkflowCompound to tests/machines/ for importability.
Add tip about Graphviz requirement and seealso linking to diagram.md
for deeper coverage (Sphinx directive, Jupyter, QuickChart, etc.).
Delete the 3 PNG files that are no longer referenced.1 parent cac607a commit 4953985
36 files changed
Lines changed: 2757 additions & 528 deletions
File tree
- docs
- images
- releases
- statemachine/contrib
- diagram
- renderers
- tests
- machines
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
160 | 160 | | |
161 | 161 | | |
162 | 162 | | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
163 | 183 | | |
164 | 184 | | |
165 | 185 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
51 | 51 | | |
52 | 52 | | |
53 | 53 | | |
| 54 | + | |
54 | 55 | | |
55 | 56 | | |
56 | 57 | | |
| |||
0 commit comments