Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: fgmacedo/python-statemachine
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: main
Choose a base ref
...
head repository: fgmacedo/python-statemachine
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: develop
Choose a head ref
Checking mergeability… Don’t worry, you can still create the pull request.
  • 13 commits
  • 153 files changed
  • 2 contributors

Commits on Feb 24, 2026

  1. Configuration menu
    Copy the full SHA
    d1b712d View commit details
    Browse the repository at this point in the history

Commits on Mar 1, 2026

  1. Configuration menu
    Copy the full SHA
    b4897d7 View commit details
    Browse the repository at this point in the history
  2. fix: validate Event() id parameter to prevent silent transition loss (#…

    …588)
    
    * fix: validate Event() id parameter to prevent silent transition loss
    
    Passing multiple transitions as positional args to Event() (e.g.,
    Event(t1, t2)) caused the second argument to be silently interpreted
    as the event id, leaving those transitions eventless (auto-firing).
    
    Now raises InvalidDefinition with a message suggesting the | operator.
    fgmacedo authored Mar 1, 2026
    Configuration menu
    Copy the full SHA
    cac607a View commit details
    Browse the repository at this point in the history

Commits on Mar 3, 2026

  1. 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.
    fgmacedo authored Mar 3, 2026
    Configuration menu
    Copy the full SHA
    4953985 View commit details
    Browse the repository at this point in the history
  2. refactor: replace diagram doctests with directives and add pre-commit…

    … hook (#590)
    
    - Replace inline doctests in docs/diagram.md and README.md with plain
      python code blocks and statemachine-diagram Sphinx directives
    - Add generate-images pre-commit hook to regenerate README PNG via CLI
    - Remove TestReadmeImageGeneration (side-effect replaced by hook)
    - Delete obsolete PNG images no longer referenced by docs
    - Update release notes to use directive instead of static image
    fgmacedo authored Mar 3, 2026
    Configuration menu
    Copy the full SHA
    966500d View commit details
    Browse the repository at this point in the history
  3. refactor: extract test StateChart definitions to tests/machines/ (#591)

    Move reusable StateChart class definitions from inline test code to
    dedicated modules under tests/machines/, organized by feature:
    
    - workflow/: CampaignMachine variants, ReverseTrafficLightMachine
    - error/: ErrorInGuardSC, ErrorInActionSC, ErrorInErrorHandlerSC, etc.
    - validators/: OrderValidation, MultiValidator, ValidatorWithCond, etc.
    - compound/: ShireToRivendell, MoriaExpedition, MiddleEarthJourney, etc.
    - history/: GollumPersonality variants, DeepMemoryOfMoria, ShallowMoria
    - parallel/: WarOfTheRing, TwoTowers, Session variants
    - eventless/: RingCorruption variants, BeaconChain, AutoAdvance, etc.
    - in_condition/: Fellowship, GateOfMoria, CombinedGuard, etc.
    - donedata/: DestroyTheRing, NestedQuestDoneData, QuestForErebor variants
    
    Test files now import from these modules instead of defining classes
    inline. Machines that test invalid definitions or use closures remain
    inline in test files.
    fgmacedo authored Mar 3, 2026
    Configuration menu
    Copy the full SHA
    11ffa95 View commit details
    Browse the repository at this point in the history

Commits on Mar 8, 2026

  1. Configuration menu
    Copy the full SHA
    5a85209 View commit details
    Browse the repository at this point in the history
  2. Configuration menu
    Copy the full SHA
    19556ce View commit details
    Browse the repository at this point in the history
  3. feat: Mermaid diagrams, transition tables, and format() support (#595)

    * feat: add Mermaid diagrams, transition tables, and __format__ support
    
    - 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
    
    * refactor: introduce Formatter facade with decorator-based format registry
    
    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.
    
    * feat: add SVG text format and use formatter in Sphinx directive
    
    - 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)
    
    * feat: auto-expand {statechart:FORMAT} placeholders in class docstrings
    
    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.
    
    * docs: revise diagram.md for new features and narrative coherence
    
    - 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
    
    * docs: mention f-string/format() text representations in README and tutorial
    
    * docs: revise 3.1.0 release notes and fix broken cross-references
    
    * fix: work around Mermaid crash for transitions inside parallel regions
    
    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
    
    * fix: render cross-boundary transitions and restrict Mermaid compound 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.
    
    * test: cover missing branches in sphinx_ext.py and extract.py
    
    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.
    fgmacedo authored Mar 8, 2026
    Configuration menu
    Copy the full SHA
    2476d20 View commit details
    Browse the repository at this point in the history

Commits on Mar 11, 2026

  1. fix: Configuration.add()/discard() write through model setter for per…

    …sistence (#596)
    
    In-place mutation of the OrderedSet returned by self.value lost state changes
    when the model uses a deserializing property (e.g., Django model field backed
    by a DB column). Each getter call returned a fresh object, the in-place
    mutation modified a temporary, and the model was never updated.
    
    Changed add() and discard() to create a new OrderedSet and assign via
    self.value = new, which calls setattr on the model. This only affects
    atomic_configuration_update=False (StateChart default); the atomic path
    was never affected.
    
    Benchmark impact: ~4-5% on parallel region events (~2µs per event),
    negligible vs. callback execution in the same microstep.
    
    Signed-off-by: Fernando Macedo <[email protected]>
    fgmacedo authored Mar 11, 2026
    Configuration menu
    Copy the full SHA
    cd05c6e View commit details
    Browse the repository at this point in the history
  2. perf: eliminate allocation overhead in Configuration.add()/discard()

    Mutate the OrderedSet in place and write back via the setter instead of
    creating a new OrderedSet on every call. This removes the ~4-5% overhead
    introduced by the persistence bugfix while preserving correctness for
    both plain-attribute and deserializing-property models.
    fgmacedo committed Mar 11, 2026
    Configuration menu
    Copy the full SHA
    8d17ba9 View commit details
    Browse the repository at this point in the history

Commits on Mar 16, 2026

  1. Refactor/configuration normalize orderedset (#599)

    * test: add API contract tests for Configuration across all topologies
    
    Systematic test matrix covering the observable behavior of all public
    Configuration APIs (current_state_value, configuration_values,
    configuration, current_state, model.state) across flat, compound,
    parallel, and complex parallel StateCharts.
    
    Verifies type contracts (scalar vs OrderedSet), value correctness,
    model identity, and the uninitialized async lifecycle. Uses
    pytest.parametrize over 14 topology x lifecycle scenarios x sync/async
    engines, plus setter and uninitialized edge cases (39 tests total).
    
    * refactor: normalize Configuration internals to always use OrderedSet
    
    Introduce two boundary functions (_read_from_model / _write_to_model)
    that confine the None|scalar|OrderedSet trichotomy to the model edge.
    All other methods (add, discard, states setter, values) now operate on
    a uniform OrderedSet, eliminating per-method type branching.
    
    The model still receives the same denormalized values as before
    (None for empty, scalar for single state, OrderedSet for multiple).
    
    Fixes edge case: configuration_values now returns OrderedSet() for
    uninitialized config instead of OrderedSet([None]), since None is not
    a valid state value.
    
    Prepares the codebase for the persistence protocol work (#597).
    fgmacedo authored Mar 16, 2026
    Configuration menu
    Copy the full SHA
    67f2b0d View commit details
    Browse the repository at this point in the history

Commits on Mar 21, 2026

  1. fix: render human-readable Event name in diagrams (#601)

    * chore: add tmp/ to .gitignore
    
    * fix: render human-readable Event name in diagrams (#600)
    
    Event.name is now auto-generated as a humanized form of the id (split
    by _ and . separators, first word capitalized) instead of echoing the
    raw identifier. Explicit name= passed by the user is preserved.
    
    - Add humanize_id() in utils.py (compiled regex, shared by Event and
      State)
    - Remove redundant name= from Event call sites in factory, events,
      and SCXML actions so auto-generation kicks in
    - Use event.name in diagram labels (_format_event_names)
    - Update docs and tests to reflect humanized names
    
    Closes #600
    
    * docs: add event humanized name to 3.1.0 release notes
    fgmacedo authored Mar 21, 2026
    Configuration menu
    Copy the full SHA
    ee3607a View commit details
    Browse the repository at this point in the history
Loading