diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml
index 616fbf02..35912b19 100644
--- a/.github/workflows/pages.yml
+++ b/.github/workflows/pages.yml
@@ -31,7 +31,7 @@ jobs:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v7
- name: Assemble site
run: |
mkdir -p _site/schemas/statechart
@@ -44,8 +44,8 @@ jobs:
See the statechart JSON Schema or the
documentation.
HTML
- - uses: actions/upload-pages-artifact@v3
+ - uses: actions/upload-pages-artifact@v5
with:
path: _site
- id: deployment
- uses: actions/deploy-pages@v4
+ uses: actions/deploy-pages@v5
diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml
index ac417f8d..6f6dfe0c 100644
--- a/.github/workflows/python-package.yml
+++ b/.github/workflows/python-package.yml
@@ -21,16 +21,16 @@ jobs:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v7
- run: git fetch origin develop
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v6
+ uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python-version }}
- name: Setup Graphviz
uses: ts-graphviz/setup-graphviz@v2
- name: Install uv
- uses: astral-sh/setup-uv@v8.1.0
+ uses: astral-sh/setup-uv@v10.1.0
with:
enable-cache: true
cache-suffix: "python${{ matrix.python-version }}"
@@ -55,7 +55,7 @@ jobs:
# upload coverage
#----------------------------------------------
- name: Upload coverage to Codecov
- uses: codecov/codecov-action@v6
+ uses: codecov/codecov-action@v7
if: matrix.python-version == 3.14
with:
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index baf3fd1e..c571da18 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -11,12 +11,12 @@ jobs:
permissions:
id-token: write
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v7
- run: git fetch origin develop
- name: Setup Python
- uses: actions/setup-python@v6
+ uses: actions/setup-python@v7
with:
python-version: '3.14'
@@ -24,7 +24,7 @@ jobs:
uses: ts-graphviz/setup-graphviz@v2
- name: Install uv
- uses: astral-sh/setup-uv@v8.1.0
+ uses: astral-sh/setup-uv@v10.1.0
with:
enable-cache: true
@@ -40,7 +40,7 @@ jobs:
uv build
- name: Upload dists
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v7
with:
name: release-dists
path: dist/
@@ -59,7 +59,7 @@ jobs:
steps:
- name: Retrieve release distributions
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@v8
with:
name: release-dists
path: dist/
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 8b2119bf..9f96d446 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -9,7 +9,7 @@ repos:
exclude: docs/auto_examples
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
- rev: v0.15.17
+ rev: v0.16.7
hooks:
# Run the linter.
- id: ruff
diff --git a/docs/diagram.md b/docs/diagram.md
index d48443d3..a6512a75 100644
--- a/docs/diagram.md
+++ b/docs/diagram.md
@@ -214,6 +214,7 @@ Sphinx directive):
```python
from statemachine.contrib.diagram import formatter
+
@formatter.register_format("plantuml", "puml")
def _render_plantuml(machine_or_class):
# your PlantUML renderer here
@@ -525,6 +526,7 @@ class CustomDiagram(DotGraphMachine):
graph_rankdir = "TB"
state_active_fillcolor = "lightyellow"
+
sm = OrderControl()
sm.receive_payment(10)
diff --git a/docs/guards.md b/docs/guards.md
index 8ce5a0ec..35455604 100644
--- a/docs/guards.md
+++ b/docs/guards.md
@@ -254,7 +254,7 @@ listeners. They can point to properties, attributes, or methods:
**Parentheses** control evaluation order:
```python
-cond="(is_admin or is_moderator) and not is_banned"
+cond = "(is_admin or is_moderator) and not is_banned"
```
#### Expression examples
diff --git a/docs/how-to/coming_from_state_pattern.md b/docs/how-to/coming_from_state_pattern.md
index eccc6688..e04296a7 100644
--- a/docs/how-to/coming_from_state_pattern.md
+++ b/docs/how-to/coming_from_state_pattern.md
@@ -28,16 +28,13 @@ class OrderState(ABC):
"""Abstract base for all order states."""
@abstractmethod
- def confirm(self, order):
- ...
+ def confirm(self, order): ...
@abstractmethod
- def ship(self, order):
- ...
+ def ship(self, order): ...
@abstractmethod
- def deliver(self, order):
- ...
+ def deliver(self, order): ...
class DraftState(OrderState):
diff --git a/docs/how-to/coming_from_transitions.md b/docs/how-to/coming_from_transitions.md
index 30e8f1e2..791c4765 100644
--- a/docs/how-to/coming_from_transitions.md
+++ b/docs/how-to/coming_from_transitions.md
@@ -233,7 +233,7 @@ that reads SCXML, JSON and YAML *documents* straight into a `StateChart`:
```python
from statemachine.io import load
-Machine = load("traffic_light.scxml") # or .json / .yaml; format detected from the extension
+Machine = load("traffic_light.scxml") # or .json / .yaml; format detected from the extension
```
Expressions in the document (guards, datamodel) are evaluated by a restricted allowlist —
@@ -306,8 +306,8 @@ True
In *transitions*, events are called as methods on the model:
```python
-machine.produce() # triggers the "produce" event
-machine.deliver() # triggers the "deliver" event
+machine.produce() # triggers the "produce" event
+machine.deliver() # triggers the "deliver" event
```
python-statemachine supports both styles:
@@ -342,13 +342,15 @@ Callbacks are specified as strings (method names) or callables:
```python
machine = Machine(
states=states,
- transitions=[{
- "trigger": "produce",
- "source": "draft",
- "dest": "producing",
- "before": "validate_job",
- "after": "notify_team",
- }],
+ transitions=[
+ {
+ "trigger": "produce",
+ "source": "draft",
+ "dest": "producing",
+ "before": "validate_job",
+ "after": "notify_team",
+ }
+ ],
initial="draft",
)
```
@@ -454,7 +456,9 @@ In *transitions*:
```python
machine.add_transition(
- "produce", "draft", "producing",
+ "produce",
+ "draft",
+ "producing",
conditions=["is_valid", "has_resources"],
unless=["is_locked"],
)
@@ -588,10 +592,12 @@ See {ref}`invoke` for full documentation.
```python
from transitions.extensions import AsyncMachine
+
class AsyncModel:
async def on_enter_producing(self):
await some_async_operation()
+
machine = AsyncMachine(model=AsyncModel(), states=states, initial="draft")
await machine.produce()
```
@@ -752,6 +758,7 @@ No class swapping, no feature matrices to consult — just `StateChart`.
class MyModel:
pass
+
model = MyModel()
machine = Machine(model=model, states=states, transitions=transitions, initial="draft")
model.produce() # events are added to the model
diff --git a/docs/integrations.md b/docs/integrations.md
index fd362cee..ed1ffa38 100644
--- a/docs/integrations.md
+++ b/docs/integrations.md
@@ -120,10 +120,11 @@ from statemachine import State
class CampaignMachine(StateChart):
"A workflow machine"
- draft = State('Draft', initial=True, value=1)
- producing = State('Being produced', value=2)
- closed = State('Closed', value=3)
- cancelled = State('Cancelled', value=4)
+
+ draft = State("Draft", initial=True, value=1)
+ producing = State("Being produced", value=2)
+ closed = State("Closed", value=3)
+ cancelled = State("Cancelled", value=4)
add_job = draft.to.itself() | producing.to.itself()
produce = draft.to(producing)
@@ -142,9 +143,9 @@ from statemachine.mixins import MachineMixin
class Campaign(models.Model, MachineMixin):
- state_machine_name = 'campaign.statemachines.CampaignMachine'
- state_machine_attr = 'sm'
- state_field_name = 'step'
+ state_machine_name = "campaign.statemachines.CampaignMachine"
+ state_machine_attr = "sm"
+ state_field_name = "step"
name = models.CharField(max_length=30)
step = models.IntegerField()
diff --git a/docs/io/security.md b/docs/io/security.md
index a121fceb..8984b5bd 100644
--- a/docs/io/security.md
+++ b/docs/io/security.md
@@ -201,17 +201,17 @@ every format (SCXML, JSON and YAML).
A set of follow-up advisories hardened the restricted mode further and prompted the
confidentiality/integrity vs availability framing above:
-- [GHSA-fj3w-533r-fvf6](https://github.com/fgmacedo/python-statemachine/security/advisories/GHSA-fj3w-533r-fvf6)
- — `` and `` read local files during loading, regardless of
+- [GHSA-fj3w-533r-fvf6](https://github.com/fgmacedo/python-statemachine/security/advisories/GHSA-fj3w-533r-fvf6):
+ `` and `` read local files during loading, regardless of
`trusted`. Loading now rejects external `src` references unless `trusted=True`, and refuses
``/DTD to block XML entity-expansion bombs.
- [GHSA-v3qq-3xvg-m77g](https://github.com/fgmacedo/python-statemachine/security/advisories/GHSA-v3qq-3xvg-m77g)
- / [GHSA-4857-ggqc-p3jc](https://github.com/fgmacedo/python-statemachine/security/advisories/GHSA-4857-ggqc-p3jc)
- — a document could write to a dunder/private/protected attribute (notably traversing
+ / [GHSA-4857-ggqc-p3jc](https://github.com/fgmacedo/python-statemachine/security/advisories/GHSA-4857-ggqc-p3jc):
+ a document could write to a dunder/private/protected attribute (notably traversing
`__class__`) and corrupt the shared model class process-wide. Write targets are now confined
to public model attributes on every path segment.
-- [GHSA-r8gj-366q-cgvj](https://github.com/fgmacedo/python-statemachine/security/advisories/GHSA-r8gj-366q-cgvj)
- — `**`/`*` in the restricted evaluator had no magnitude bound, so a tiny expression could
+- [GHSA-r8gj-366q-cgvj](https://github.com/fgmacedo/python-statemachine/security/advisories/GHSA-r8gj-366q-cgvj):
+ `**`/`*` in the restricted evaluator had no magnitude bound, so a tiny expression could
exhaust CPU or memory. They are now magnitude-capped.
These were released together in 3.2.1.
diff --git a/docs/releases/1.0.1.md b/docs/releases/1.0.1.md
index d32b6fcb..e497bf64 100644
--- a/docs/releases/1.0.1.md
+++ b/docs/releases/1.0.1.md
@@ -27,14 +27,15 @@ Transitions now support `cond` and `unless` parameters, to restrict
the execution.
```python
- class ApprovalMachine(StateMachine):
- "A workflow machine"
- requested = State("Requested", initial=True)
- accepted = State("Accepted")
- rejected = State("Rejected")
- completed = State("Completed")
+class ApprovalMachine(StateMachine):
+ "A workflow machine"
+
+ requested = State("Requested", initial=True)
+ accepted = State("Accepted")
+ rejected = State("Rejected")
+ completed = State("Completed")
- validate = requested.to(accepted, cond="is_ok") | requested.to(rejected)
+ validate = requested.to(accepted, cond="is_ok") | requested.to(rejected)
```
```{seealso}
@@ -114,9 +115,10 @@ So, the previous code (not valid anymore):
```py
class ApprovalMachine(StateMachine):
"A workflow machine"
- requested = State('Requested', initial=True)
- accepted = State('Accepted')
- rejected = State('Rejected')
+
+ requested = State("Requested", initial=True)
+ accepted = State("Accepted")
+ rejected = State("Rejected")
validate = requested.to(accepted, rejected)
@@ -133,6 +135,7 @@ Should be rewritten to use {ref}`guards`, like this:
``` py
class ApprovalMachine(StateMachine):
"A workflow machine"
+
requested = State("Requested", initial=True)
accepted = State("Accepted")
rejected = State("Rejected")
@@ -168,14 +171,16 @@ This was the previous behavior, you only got an error when trying to instantiate
```py
class CampaignMachine(StateMachine):
"A workflow machine"
- draft = State('Draft', initial=True)
- producing = State('Being produced')
- closed = State('Closed', initial=True) # Should raise an Exception when instantiated
+
+ draft = State("Draft", initial=True)
+ producing = State("Being produced")
+ closed = State("Closed", initial=True) # Should raise an Exception when instantiated
add_job = draft.to(draft) | producing.to(producing)
produce = draft.to(producing)
deliver = producing.to(closed)
+
with pytest.raises(exceptions.InvalidDefinition):
CampaignMachine()
```
@@ -187,6 +192,7 @@ with pytest.raises(exceptions.InvalidDefinition):
class CampaignMachine(StateMachine):
"A workflow machine"
+
draft = State("Draft", initial=True)
producing = State("Being produced")
closed = State(
diff --git a/docs/releases/2.0.0.md b/docs/releases/2.0.0.md
index fbc433f1..0d2c48e3 100644
--- a/docs/releases/2.0.0.md
+++ b/docs/releases/2.0.0.md
@@ -291,7 +291,6 @@ from tests.examples.traffic_light_machine import TrafficLightMachine
sm = TrafficLightMachine()
sm.run("cycle")
-
```
Should become:
@@ -313,7 +312,6 @@ from tests.examples.traffic_light_machine import TrafficLightMachine
sm = TrafficLightMachine()
assert [t.name for t in sm.allowed_transitions] == ["cycle"]
-
```
Should become:
@@ -333,7 +331,6 @@ from tests.examples.traffic_light_machine import TrafficLightMachine
sm = TrafficLightMachine()
assert sm.is_green
-
```
Should become:
@@ -355,7 +352,6 @@ from tests.examples.traffic_light_machine import TrafficLightMachine
sm = TrafficLightMachine()
assert sm.current_state.identification == "green"
-
```
Should become:
diff --git a/docs/releases/2.2.0.md b/docs/releases/2.2.0.md
index 9ac3e83a..84f22105 100644
--- a/docs/releases/2.2.0.md
+++ b/docs/releases/2.2.0.md
@@ -42,16 +42,19 @@ This will currently issue a warning, but can be turned into an exception by sett
```python
from statemachine import StateMachine, State
+
class TrafficLightMachine(StateMachine, strict_states=True):
"A workflow machine"
- red = State('Red', initial=True, value=1)
- green = State('Green', value=2)
- orange = State('Orange', value=3)
- hazard = State('Hazard', value=4)
+
+ red = State("Red", initial=True, value=1)
+ green = State("Green", value=2)
+ orange = State("Orange", value=3)
+ hazard = State("Hazard", value=4)
cycle = red.to(green) | green.to(orange) | orange.to(red)
fault = red.to(hazard) | green.to(hazard) | orange.to(hazard)
+
# InvalidDefinition: All non-final states should have at least one outgoing transition.
# These states have no outgoing transition: ['hazard']
```
diff --git a/docs/releases/2.3.0.md b/docs/releases/2.3.0.md
index 83bb0901..08ff10c9 100644
--- a/docs/releases/2.3.0.md
+++ b/docs/releases/2.3.0.md
@@ -28,8 +28,8 @@ async code with a state machine.
```python
class AsyncStateMachine(StateMachine):
- initial = State('Initial', initial=True)
- final = State('Final', final=True)
+ initial = State("Initial", initial=True)
+ final = State("Final", final=True)
advance = initial.to(final)
@@ -42,6 +42,7 @@ async def run_sm():
res = await sm.advance()
return (42, sm.current_state.name)
+
asyncio.run(run_sm())
# (42, 'Final')
```
diff --git a/docs/releases/2.4.0.md b/docs/releases/2.4.0.md
index 8054af36..2979cc1d 100644
--- a/docs/releases/2.4.0.md
+++ b/docs/releases/2.4.0.md
@@ -19,6 +19,7 @@ Example (with a spoiler of the next highlight):
```python
from statemachine import StateMachine, State, Event
+
class AnyConditionSM(StateMachine):
start = State(initial=True)
end = State(final=True)
@@ -31,6 +32,7 @@ class AnyConditionSM(StateMachine):
used_money: bool = False
used_credit: bool = False
+
sm = AnyConditionSM()
sm.submit()
# TransitionNotAllowed: Can't finish order when in Start.
diff --git a/docs/releases/2.5.0.md b/docs/releases/2.5.0.md
index 8cfe5840..babede62 100644
--- a/docs/releases/2.5.0.md
+++ b/docs/releases/2.5.0.md
@@ -66,6 +66,7 @@ listing the current state allowed events and executing the simulated user choice
```python
import random
+
random.seed("15")
sm = AccountStateMachine()
@@ -76,7 +77,7 @@ while not sm.current_state.final:
for idx, event in enumerate(allowed_events):
print(f"{idx} - {event.name}")
- user_input = random.randint(0, len(allowed_events)-1)
+ user_input = random.randint(0, len(allowed_events) - 1)
print(f"User input: {user_input}")
event = allowed_events[user_input]
@@ -104,6 +105,7 @@ Example:
```python
from statemachine import StateMachine, State, Event
+
class AnyConditionSM(StateMachine):
start = State(initial=True)
end = State(final=True)
@@ -115,6 +117,7 @@ class AnyConditionSM(StateMachine):
order_value: float = 0
+
sm = AnyConditionSM()
sm.submit()
# TransitionNotAllowed: Can't finish order when in Start.
diff --git a/docs/releases/3.1.0.md b/docs/releases/3.1.0.md
index 7e518612..8557268c 100644
--- a/docs/releases/3.1.0.md
+++ b/docs/releases/3.1.0.md
@@ -41,9 +41,9 @@ from statemachine.contrib.diagram import formatter
formatter.render(sm, "mermaid")
formatter.supported_formats()
+
@formatter.register_format("custom")
-def _render_custom(machine_or_class):
- ...
+def _render_custom(machine_or_class): ...
```
See {ref}`formatter-api` for details.
@@ -77,6 +77,7 @@ class TrafficLight(StateChart):
{statechart:md}
"""
+
green = State(initial=True)
yellow = State()
red = State()
@@ -163,6 +164,7 @@ async def fetch_data():
resp = await session.get("https://api.example.com/data")
return await resp.json()
+
class Loader(StateChart):
loading = State(initial=True, invoke=fetch_data)
ready = State(final=True)
diff --git a/docs/releases/3.2.0.md b/docs/releases/3.2.0.md
index 85f72e33..9b3cd135 100644
--- a/docs/releases/3.2.0.md
+++ b/docs/releases/3.2.0.md
@@ -68,7 +68,7 @@ conformance suite), opt back into full Python with `trusted=True`:
```python
from statemachine.io.scxml.processor import SCXMLProcessor
-SCXMLProcessor() # safe default: restricted evaluator,