Skip to content

Commit a18a396

Browse files
committed
Merge branch 'release/2.6.0'
2 parents d5006b5 + ac825d1 commit a18a396

62 files changed

Lines changed: 2174 additions & 405 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/python-package.yml

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
strategy:
1616
fail-fast: false
1717
matrix:
18-
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
18+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
1919

2020
steps:
2121
- uses: actions/checkout@v4
@@ -33,15 +33,11 @@ jobs:
3333
cache-suffix: "python${{ matrix.python-version }}"
3434
- name: Install the project
3535
run: uv sync --all-extras --dev
36-
- name: Install old pydot for 3.7 only
37-
if: matrix.python-version == 3.7
38-
run: |
39-
uv pip install pydot==2.0.0
4036
#----------------------------------------------
4137
# run ruff
4238
#----------------------------------------------
4339
- name: Linter with ruff
44-
if: matrix.python-version == 3.13
40+
if: matrix.python-version == 3.14
4541
run: |
4642
uv run ruff check .
4743
uv run ruff format --check .
@@ -57,7 +53,7 @@ jobs:
5753
#----------------------------------------------
5854
- name: Upload coverage to Codecov
5955
uses: codecov/codecov-action@v4
60-
if: matrix.python-version == 3.13
56+
if: matrix.python-version == 3.14
6157
with:
6258
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
6359
directory: .

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
- name: Setup Python
1919
uses: actions/setup-python@v5
2020
with:
21-
python-version: '3.13'
21+
python-version: '3.14'
2222

2323
- name: Setup Graphviz
2424
uses: ts-graphviz/setup-graphviz@v2

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ repos:
99
exclude: docs/auto_examples
1010
- repo: https://github.com/charliermarsh/ruff-pre-commit
1111
# Ruff version.
12-
rev: v0.8.1
12+
rev: v0.15.0
1313
hooks:
1414
# Run the linter.
1515
- id: ruff

.readthedocs.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ version: 2
88
build:
99
os: "ubuntu-22.04"
1010
tools:
11-
python: "3.12"
11+
python: "3.14"
1212
apt_packages:
1313
- graphviz
1414
jobs:

AGENTS.md

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# python-statemachine
2+
3+
Python Finite State Machines made easy.
4+
5+
## Project overview
6+
7+
A library for building finite state machines in Python, with support for sync and async engines,
8+
Django integration, diagram generation, and a flexible callback/listener system.
9+
10+
- **Source code:** `statemachine/`
11+
- **Tests:** `tests/`
12+
- **Documentation:** `docs/` (Sphinx + MyST Markdown, hosted on ReadTheDocs)
13+
14+
## Architecture
15+
16+
- `statemachine.py` — Core `StateMachine` class
17+
- `factory.py``StateMachineMetaclass` handles class construction, state/transition validation
18+
- `state.py` / `event.py` — Descriptor-based `State` and `Event` definitions
19+
- `transition.py` / `transition_list.py` — Transition logic and composition (`|` operator)
20+
- `callbacks.py` — Priority-based callback registry (`CallbackPriority`, `CallbackGroup`)
21+
- `dispatcher.py` — Listener/observer pattern, `callable_method` wraps callables with signature adaptation
22+
- `signature.py``SignatureAdapter` for dependency injection into callbacks
23+
- `engines/sync.py`, `engines/async_.py` — Sync and async run-to-completion engines
24+
- `registry.py` — Global state machine registry (used by `MachineMixin`)
25+
- `mixins.py``MachineMixin` for domain model integration (e.g., Django models)
26+
- `spec_parser.py` — Boolean expression parser for condition guards
27+
- `contrib/diagram.py` — Diagram generation via pydot/Graphviz
28+
29+
## Environment setup
30+
31+
```bash
32+
uv sync --all-extras --dev
33+
pre-commit install
34+
```
35+
36+
## Running tests
37+
38+
Always use `uv` to run commands:
39+
40+
```bash
41+
# Run all tests (parallel)
42+
uv run pytest -n auto
43+
44+
# Run a specific test file
45+
uv run pytest tests/test_signature.py
46+
47+
# Run a specific test
48+
uv run pytest tests/test_signature.py::TestSignatureAdapter::test_wrap_fn_single_positional_parameter
49+
50+
# Skip slow tests
51+
uv run pytest -m "not slow"
52+
```
53+
54+
Tests include doctests from both source modules (`--doctest-modules`) and markdown docs
55+
(`--doctest-glob=*.md`). Coverage is enabled by default.
56+
57+
## Linting and formatting
58+
59+
```bash
60+
# Lint
61+
uv run ruff check .
62+
63+
# Auto-fix lint issues
64+
uv run ruff check --fix .
65+
66+
# Format
67+
uv run ruff format .
68+
69+
# Type check
70+
uv run mypy statemachine/ tests/
71+
```
72+
73+
## Code style
74+
75+
- **Formatter/Linter:** ruff (line length 99, target Python 3.9)
76+
- **Rules:** pycodestyle, pyflakes, isort, pyupgrade, flake8-comprehensions, flake8-bugbear, flake8-pytest-style
77+
- **Imports:** single-line, sorted by isort
78+
- **Docstrings:** Google convention
79+
- **Naming:** PascalCase for classes, snake_case for functions/methods, UPPER_SNAKE_CASE for constants
80+
- **Type hints:** used throughout; `TYPE_CHECKING` for circular imports
81+
- Pre-commit hooks enforce ruff + mypy + pytest
82+
83+
## Design principles
84+
85+
- **Decouple infrastructure from domain:** Modules like `signature.py` and `dispatcher.py` are
86+
general-purpose (signature adaptation, listener/observer pattern) and intentionally not coupled
87+
to the state machine domain. Prefer this separation even for modules that are only used
88+
internally — it keeps responsibilities clear and the code easier to reason about.
89+
- **Favor small, focused modules:** When adding new functionality, consider whether it can live in
90+
its own module with a well-defined boundary, rather than growing an existing one.
91+
92+
## Building documentation
93+
94+
```bash
95+
# Build HTML docs
96+
uv run sphinx-build docs docs/_build/html
97+
98+
# Live reload for development
99+
uv run sphinx-autobuild docs docs/_build/html --re-ignore "auto_examples/.*"
100+
```
101+
102+
## Git workflow
103+
104+
- Main branch: `develop`
105+
- PRs target `develop`
106+
- Releases are tagged as `v*.*.*`
107+
- Signed commits preferred (`git commit -s`)
108+
- Use [Conventional Commits](https://www.conventionalcommits.org/) messages
109+
(e.g., `feat:`, `fix:`, `refactor:`, `test:`, `docs:`, `chore:`, `perf:`)
110+
111+
## Security
112+
113+
- Do not commit secrets, credentials, or `.env` files
114+
- Validate at system boundaries; trust internal code

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
AGENTS.md

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ You can now create an instance:
9999
This state machine can be represented graphically as follows:
100100

101101
```py
102+
>>> # This example will only run on automated tests if dot is present
103+
>>> getfixture("requires_dot_installed")
102104
>>> img_path = "docs/images/readme_trafficlightmachine.png"
103105
>>> sm._graph().write_png(img_path)
104106

conftest.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1+
import shutil
12
import sys
23

34
import pytest
45

56

67
@pytest.fixture(autouse=True, scope="session")
78
def add_doctest_context(doctest_namespace): # noqa: PT004
9+
from statemachine.utils import run_async_from_sync
10+
811
from statemachine import State
912
from statemachine import StateMachine
10-
from statemachine.utils import run_async_from_sync
1113

1214
class ContribAsyncio:
1315
"""
@@ -31,3 +33,14 @@ def pytest_ignore_collect(collection_path, path, config):
3133

3234
if "django_project" in str(path):
3335
return True
36+
37+
38+
@pytest.fixture(scope="session")
39+
def has_dot_installed():
40+
return bool(shutil.which("dot"))
41+
42+
43+
@pytest.fixture()
44+
def requires_dot_installed(request, has_dot_installed):
45+
if not has_dot_installed:
46+
pytest.skip(f"Test {request.node.nodeid} requires 'dot' that is not installed.")

docs/actions.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ There are several action callbacks that you can define to interact with a
1414
StateMachine in execution.
1515

1616
There are callbacks that you can specify that are generic and will be called
17-
when something changes and are not bounded to a specific state or event:
17+
when something changes, and are not bound to a specific state or event:
1818

1919
- `before_transition()`
2020

@@ -26,7 +26,7 @@ when something changes and are not bounded to a specific state or event:
2626

2727
- `after_transition()`
2828

29-
The following example can get you an overview of the "generic" callbacks available:
29+
The following example offers an overview of the "generic" callbacks available:
3030

3131
```py
3232
>>> from statemachine import StateMachine, State

docs/authors.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
* [Rafael Rêgo](mailto:crafards@gmail.com)
1111
* [Raphael Schrader](mailto:raphael@schradercloud.de)
1212
* [João S. O. Bueno](mailto:gwidion@gmail.com)
13+
* [Rodrigo Nogueira](mailto:rodrigo.b.nogueira@gmail.com)
1314

1415

1516
## Scaffolding

0 commit comments

Comments
 (0)