Skip to content

Commit c6f6262

Browse files
committed
Merge branch 'release/3.1.0'
2 parents b31d830 + 586f8bc commit c6f6262

160 files changed

Lines changed: 11578 additions & 3009 deletions

File tree

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 & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ on:
99
pull_request:
1010
branches: [ "develop" ]
1111

12+
permissions:
13+
contents: read
14+
1215
jobs:
1316
build:
1417
runs-on: ubuntu-latest

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,9 @@ target/
7979
docs/auto_examples/sg_execution_times.*
8080
docs/auto_examples/*.pickle
8181
docs/sg_execution_times.rst
82+
83+
# Temporary files
84+
tmp/
85+
86+
# Local specs
87+
specs/

.pre-commit-config.yaml

Lines changed: 11 additions & 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.15.0
12+
rev: v0.15.8
1313
hooks:
1414
# Run the linter.
1515
- id: ruff
@@ -31,6 +31,16 @@ repos:
3131
types: [python]
3232
language: system
3333
pass_filenames: false
34+
- id: generate-images
35+
name: Generate README images
36+
entry: >-
37+
uv run python -m statemachine.contrib.diagram
38+
tests.examples.traffic_light_machine.TrafficLightMachine
39+
docs/images/readme_trafficlightmachine.png
40+
--events cycle cycle cycle
41+
language: system
42+
pass_filenames: false
43+
files: (statemachine/contrib/diagram/|tests/examples/traffic_light_machine\.py)
3444
- id: pytest
3545
name: Pytest
3646
entry: uv run pytest -n auto --cov-fail-under=100

AGENTS.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,16 @@ current event.
7777
- `on_error_execution()` works via naming convention but **only** when a transition for
7878
`error.execution` is declared — it is NOT a generic callback.
7979

80+
### Thread safety
81+
82+
- The sync engine is **thread-safe**: multiple threads can send events to the same SM instance
83+
concurrently. The processing loop uses a `threading.Lock` so at most one thread executes
84+
transitions at a time. Event queues use `PriorityQueue` (stdlib, thread-safe).
85+
- **Do not replace `PriorityQueue`** with non-thread-safe alternatives (e.g., `collections.deque`,
86+
plain `list`) — this would break concurrent access guarantees.
87+
- Stress tests in `tests/test_threading.py::TestThreadSafety` exercise real contention with
88+
barriers and multiple sender threads. Any change to queue or locking internals must pass these.
89+
8090
### Invoke (`<invoke>`)
8191

8292
- `invoke.py``InvokeManager` on the engine manages the lifecycle: `mark_for_invoke()`,
@@ -127,6 +137,16 @@ timeout 120 uv run pytest -n 4
127137

128138
Testes normally run under 60s (~40s on average), so take a closer look if they take longer, it can be a regression.
129139

140+
### Debug logging
141+
142+
`log_cli_level` defaults to `WARNING` in `pyproject.toml`. The engine caches a no-op
143+
for `logger.debug` at init time — running tests with `DEBUG` would bypass this
144+
optimization and inflate benchmark numbers. To enable debug logs for a specific run:
145+
146+
```bash
147+
uv run pytest -o log_cli_level=DEBUG tests/test_something.py
148+
```
149+
130150
When analyzing warnings or extensive output, run the tests **once** saving the output to a file
131151
(`> /tmp/pytest-output.txt 2>&1`), then analyze the file — instead of running the suite
132152
repeatedly with different greps.
@@ -160,6 +180,26 @@ async def test_something(self, sm_runner):
160180

161181
Do **not** manually add async no-op listeners or duplicate test classes — prefer `sm_runner`.
162182

183+
### TDD and coverage requirements
184+
185+
Follow a **test-driven development** approach: tests are not an afterthought — they are a
186+
first-class requirement that must be part of every implementation plan.
187+
188+
- **Planning phase:** every plan must include test tasks as explicit steps, not a final
189+
"add tests" bullet. Identify what needs to be tested (new branches, edge cases, error
190+
paths) while designing the implementation.
191+
- **100% branch coverage is mandatory.** The pre-commit hook enforces `--cov-fail-under=100`
192+
with branch coverage enabled. Code that drops coverage will not pass CI.
193+
- **Verify coverage before committing:** after writing tests, run coverage on the affected
194+
modules and check for missing lines/branches:
195+
```bash
196+
timeout 120 uv run pytest tests/<test_file>.py --cov=statemachine.<module> --cov-report=term-missing --cov-branch
197+
```
198+
- **Use pytest fixtures** (`tmp_path`, `monkeypatch`, etc.) — never hardcode paths or
199+
use mutable global state when a fixture exists.
200+
- **Unreachable defensive branches** (e.g., `if` guards that can never be True given the
201+
type system) may be marked with `pragma: no cover`, but prefer writing a test first.
202+
163203
## Linting and formatting
164204

165205
```bash

README.md

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,16 +77,23 @@ True
7777

7878
```
7979

80-
Generate a diagram:
80+
Generate a diagram or get a text representation with f-strings:
8181

8282
```py
83-
>>> # This example will only run on automated tests if dot is present
84-
>>> getfixture("requires_dot_installed")
85-
>>> img_path = "docs/images/readme_trafficlightmachine.png"
86-
>>> sm._graph().write_png(img_path)
83+
>>> print(f"{sm:md}")
84+
| State | Event | Guard | Target |
85+
| ------ | ----- | ----- | ------ |
86+
| Green | Cycle | | Yellow |
87+
| Yellow | Cycle | | Red |
88+
| Red | Cycle | | Green |
89+
<BLANKLINE>
8790

8891
```
8992

93+
```python
94+
sm._graph().write_png("traffic_light.png")
95+
```
96+
9097
![](https://raw.githubusercontent.com/fgmacedo/python-statemachine/develop/docs/images/readme_trafficlightmachine.png)
9198

9299
Parameters are injected into callbacks automatically — the library inspects the
@@ -345,7 +352,7 @@ There's a lot more to explore:
345352
- **`prepare_event`** callback — inject custom data into all callbacks
346353
- **Observer pattern** — register external listeners to watch events and state changes
347354
- **Django integration** — auto-discover state machines in Django apps with `MachineMixin`
348-
- **Diagram generation**from the CLI, at runtime, or in Jupyter notebooks
355+
- **Diagram generation**via f-strings (`f"{sm:mermaid}"`), CLI, Sphinx directive, or Jupyter
349356
- **Dictionary-based definitions** — create state machines from data structures
350357
- **Internationalization** — error messages in multiple languages
351358

docs/async.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ the initial state:
7575
... print(list(sm.configuration_values))
7676

7777
>>> asyncio.run(show_problem())
78-
[None]
78+
[]
7979

8080
```
8181

docs/conf.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@
5151
"sphinx.ext.autosectionlabel",
5252
"sphinx_gallery.gen_gallery",
5353
"sphinx_copybutton",
54+
"statemachine.contrib.diagram.sphinx_ext",
55+
"sphinxcontrib.mermaid",
5456
]
5557

5658
autosectionlabel_prefix_document = True

0 commit comments

Comments
 (0)