From a8c6af3e4ed6e02d42fe156f64e6c01ecb9a87c4 Mon Sep 17 00:00:00 2001 From: Fernando Macedo Date: Wed, 14 Oct 2026 23:56:06 -0300 Subject: [PATCH] docs: 3.3.0 release notes for the merged fixes Lay out the 3.3.0 notes in the structure later PRs extend and document the history default entry fix (#693), the done.invoke error reporting fix (#698) and the PSSM-based UML test cases (#697). Refs #692 Refs #713 Signed-off-by: Fernando Macedo --- docs/releases/3.3.0.md | 124 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/docs/releases/3.3.0.md b/docs/releases/3.3.0.md index 3a4ff022..163556be 100644 --- a/docs/releases/3.3.0.md +++ b/docs/releases/3.3.0.md @@ -2,6 +2,8 @@ *Unreleased* +% Intro paragraph: written by the maintainer at release time. + ## What's new in 3.3.0 ### Extension keys in native documents @@ -10,3 +12,125 @@ A JSON or YAML document can now carry top-level keys prefixed with `x-`, such as `x-review: {owner: ops}`. The loader and the JSON Schema accept them and ignore their content, so other tools can keep their data in the same file as the statechart. Other unknown keys are still rejected. See [](../io/formats.md#extension-keys). + +[#697](https://github.com/fgmacedo/python-statemachine/pull/697). + +### UML test cases based on PSSM + +The test suite now runs 103 cases generated from the test suite of the OMG Precise Semantics +of UML State Machines (PSSM) 1.0 specification, on both the sync and the async engine. Each +case is a native YAML statechart, with the stimulus and the expected traces kept under an +`x-pssm` extension key. The cases the library cannot pass yet are listed with a reason code, +so each later fix shows up as a case that starts passing, and the cases that pass only +because the converter lowers a construct the library cannot spell yet are listed apart. The OMG file is never committed: a +converter generates the cases from it, and a CI job checks that the committed cases match +its output. See [](../contributing.md#uml-cases-based-on-pssm). + +[#697](https://github.com/fgmacedo/python-statemachine/pull/697). + +## Behavior changes to check when upgrading + +Each item below fixes behavior that did not match the documented contract. Code that relied +on the old behavior can still behave differently or fail after upgrading: + +- The actions of a transition that targets a `HistoryState` now run once. They used to run + twice whenever the history state had a recorded value or a default transition. See {ref}`release-3.3.0-history`. +- Entering a `HistoryState` that has recorded nothing and declares no default transition now + enters its compound state through its initial state. The machine used to end up with an + empty configuration. See {ref}`release-3.3.0-history`. +- On a `StateMachine`, when an invoke handler finishes while the machine is idle and + processing its `done.invoke` raises, the error is now reported through `threading.excepthook` on the sync + engine and through the event loop's exception handler on the async engine. It used to be + sent back as `error.execution`, or dropped when the transition left the invoking state. See {ref}`release-3.3.0-done-invoke`. +- A `StateMachine` with an `error_execution` transition and no transition for + `done.invoke.` now reports `TransitionNotAllowed` for that event, through the same + channels. It used to turn it into `error.execution`. See {ref}`release-3.3.0-done-invoke`. +- On a `StateChart`, an error raised by an invoke handler's `on_finalize` now becomes an + internal `error.execution`, processed after the transition of the event that ran the + finalize. That transition used to be skipped, and the error could reach the caller of an + unrelated `send()`. On a `StateMachine` the error propagates: to the caller driving the + loop, as before, or, when the invocation itself processes `done.invoke`, through the + channels above, where it used to become `error.execution`. See + {ref}`release-3.3.0-done-invoke`. + +## Deprecations in 3.3.0 + +Deprecated APIs are removed in 4.0. Nothing is deprecated in 3.3.0 yet. + +## Bug fixes in 3.3.0 + +(release-3.3.0-history)= + +### Entering a history state by default + +A transition to a `HistoryState` that had a recorded value or a default transition ran its +actions twice: once as the transition's own actions, and again on entry of the compound +state, as if they were the history's default. +And a history state with nothing recorded and no default transition (`h.to(...)`) entered +nothing at all: the machine was left with an empty configuration. + +The default of a history state is now only its own transition, used only when nothing is +recorded. Without one, the compound state is entered through its initial state: + +```py +>>> from statemachine import HistoryState, State, StateChart + +>>> class Player(StateChart): +... class playing(State.Compound): +... track_1 = State(initial=True) +... track_2 = State() +... h = HistoryState() +... skip = track_1.to(track_2) +... stopped = State(initial=True) +... resume = stopped.to(playing.h, on="count_resume") +... stop = playing.to(stopped) +... +... resumes = 0 +... +... def count_resume(self): +... self.resumes += 1 + +>>> sm = Player() +>>> sm.send("resume") +>>> sorted(sm.configuration_values) +['playing', 'track_1'] +>>> sm.resumes +1 + +>>> sm.send("skip") +>>> sm.send("stop") +>>> sm.send("resume") +>>> sorted(sm.configuration_values) +['playing', 'track_2'] +>>> sm.resumes +2 + +``` + +Before this fix, the first `resume` left `Player` with an empty configuration. See +{ref}`history-states`. + +[#693](https://github.com/fgmacedo/python-statemachine/pull/693). + +(release-3.3.0-done-invoke)= + +### Errors raised while an invocation processes its `done.invoke` + +When an invoke handler finishes while the machine is idle, the handler's own thread (sync +engine) or task (async engine) sends `done.invoke.` and runs that macrostep. The same +`except` clause covered the handler's `run()` and that send, so an error raised while +processing `done.invoke` was treated as an error of the handler: it was dropped when the +transition had already left the invoking state, and sent back as `error.execution` +otherwise, even on a `StateMachine`, whose `catch_errors_as_events` is `False`. On the sync +engine, the error surfaced only when another thread happened to be driving the loop. + +Now the invocation turns only an error from the handler's own `run()` into +`error.execution`. An error raised while +processing `done.invoke` is handled like an error in any other step: it becomes +`error.execution` when `catch_errors_as_events=True`, and propagates otherwise. With no +caller waiting for it, the sync engine lets it propagate out of the handler's thread, to +`threading.excepthook`, and the async engine passes it to the event loop's exception +handler. An error raised by a handler's `on_finalize` goes through the same error handling. +See [](../invoke.md#error-handling). + +[#698](https://github.com/fgmacedo/python-statemachine/pull/698).