Skip to content

Commit 966500d

Browse files
authored
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
1 parent 4953985 commit 966500d

10 files changed

Lines changed: 76 additions & 66 deletions

.pre-commit-config.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,17 @@ 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+
language: system
41+
pass_filenames: false
42+
files: >-
43+
(statemachine/contrib/diagram/
44+
|tests/examples/traffic_light_machine\.py)
3445
- id: pytest
3546
name: Pytest
3647
entry: uv run pytest -n auto --cov-fail-under=100

README.md

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,8 @@ True
7979

8080
Generate a diagram:
8181

82-
```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)
87-
82+
```python
83+
sm._graph().write_png("traffic_light.png")
8884
```
8985

9086
![](https://raw.githubusercontent.com/fgmacedo/python-statemachine/develop/docs/images/readme_trafficlightmachine.png)

docs/diagram.md

Lines changed: 38 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -33,62 +33,52 @@ For other systems, see the [Graphviz downloads page](https://graphviz.org/downlo
3333
Every state machine instance exposes a `_graph()` method that returns a
3434
[pydot.Dot](https://github.com/pydot/pydot) graph object:
3535

36-
```py
37-
>>> from tests.examples.order_control_machine import OrderControl
38-
39-
>>> sm = OrderControl()
40-
41-
>>> sm._graph() # doctest: +ELLIPSIS
42-
<pydot.core.Dot ...
36+
```python
37+
from tests.examples.order_control_machine import OrderControl
4338

39+
sm = OrderControl()
40+
graph = sm._graph() # returns a pydot.Dot object
4441
```
4542

4643
### Highlighting the current state
4744

4845
The diagram automatically highlights the current state of the instance.
4946
Send events to advance the machine and see the active state change:
5047

51-
``` py
52-
>>> # This example will only run on automated tests if dot is present
53-
>>> getfixture("requires_dot_installed")
54-
55-
>>> from tests.examples.order_control_machine import OrderControl
56-
57-
>>> sm = OrderControl()
58-
59-
>>> sm.receive_payment(10)
60-
[10]
61-
62-
>>> sm._graph().write_png("docs/images/order_control_machine_processing.png")
48+
```python
49+
from tests.examples.traffic_light_machine import TrafficLightMachine
6350

51+
sm = TrafficLightMachine()
52+
sm.send("cycle")
53+
sm._graph().write_png("traffic_light_yellow.png")
6454
```
6555

66-
![OrderControl after receiving payment](images/order_control_machine_processing.png)
56+
```{statemachine-diagram} tests.examples.traffic_light_machine.TrafficLightMachine
57+
:events: cycle
58+
:caption: TrafficLightMachine after one cycle
59+
```
6760

6861

6962
### Exporting to a file
7063

7164
The `pydot.Dot` object supports writing to many formats — use
7265
`write_png()`, `write_svg()`, `write_pdf()`, etc.:
7366

74-
```py
75-
>>> from tests.examples.order_control_machine import OrderControl
76-
77-
>>> sm = OrderControl()
78-
79-
>>> sm._graph().write_png("docs/images/order_control_machine_initial.png")
80-
67+
```python
68+
sm = OrderControl()
69+
sm._graph().write_png("order_control.png")
8170
```
8271

83-
![OrderControl](images/order_control_machine_initial.png)
72+
```{statemachine-diagram} tests.examples.order_control_machine.OrderControl
73+
:caption: OrderControl
74+
```
8475

8576
For higher resolution PNGs, set the DPI before exporting:
8677

87-
```py
88-
>>> sm._graph().set_dpi(300)
89-
90-
>>> sm._graph().write_png("docs/images/order_control_machine_initial_300dpi.png")
91-
78+
```python
79+
graph = sm._graph()
80+
graph.set_dpi(300)
81+
graph.write_png("order_control_300dpi.png")
9282
```
9383

9484
```{note}
@@ -258,11 +248,9 @@ The `DotGraphMachine` class gives you control over the diagram's visual
258248
properties. Subclass it and override the class attributes to customize
259249
fonts, colors, and layout:
260250

261-
```py
262-
>>> from statemachine.contrib.diagram import DotGraphMachine
263-
264-
>>> from tests.examples.order_control_machine import OrderControl
265-
251+
```python
252+
from statemachine.contrib.diagram import DotGraphMachine
253+
from tests.examples.order_control_machine import OrderControl
266254
```
267255

268256
Available attributes:
@@ -279,34 +267,25 @@ Available attributes:
279267
For example, to generate a top-to-bottom diagram with a custom active
280268
state color:
281269

282-
```py
283-
>>> class CustomDiagram(DotGraphMachine):
284-
... graph_rankdir = "TB"
285-
... state_active_fillcolor = "lightyellow"
286-
287-
>>> sm = OrderControl()
288-
289-
>>> sm.receive_payment(10)
290-
[10]
291-
292-
>>> graph = CustomDiagram(sm)
293-
294-
>>> dot = graph()
270+
```python
271+
class CustomDiagram(DotGraphMachine):
272+
graph_rankdir = "TB"
273+
state_active_fillcolor = "lightyellow"
295274

296-
>>> dot.to_string() # doctest: +ELLIPSIS
297-
'digraph OrderControl {...
275+
sm = OrderControl()
276+
sm.receive_payment(10)
298277

278+
graph = CustomDiagram(sm)
279+
dot = graph()
280+
dot.write_svg("order_control_custom.svg")
299281
```
300282

301283
`DotGraphMachine` also works with **classes** (not just instances) to
302284
generate diagrams without an active state:
303285

304-
```py
305-
>>> dot = DotGraphMachine(OrderControl)()
306-
307-
>>> dot.to_string() # doctest: +ELLIPSIS
308-
'digraph OrderControl {...
309-
286+
```python
287+
dot = DotGraphMachine(OrderControl)()
288+
dot.write_png("order_control_class.png")
310289
```
311290

312291

-30.4 KB
Binary file not shown.
-30.4 KB
Binary file not shown.
-31 KB
Binary file not shown.
-28.9 KB
Binary file not shown.
-4.65 KB
Loading

docs/releases/1.0.1.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@ You can generate diagrams from your state machine.
4747

4848
Example:
4949

50-
![OrderControl](../images/order_control_machine_initial.png)
50+
```{statemachine-diagram} tests.examples.order_control_machine.OrderControl
51+
:caption: OrderControl
52+
```
5153

5254

5355
```{seealso}

tests/test_contrib_diagram.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1091,3 +1091,25 @@ def test_render_without_caption_uses_div(self, tmp_path):
10911091
html = result[0].astext()
10921092
assert "<figure" not in html
10931093
assert "<div" in html
1094+
1095+
1096+
class TestGraphMethod:
1097+
"""Test the ``_graph()`` convenience method on state machine instances."""
1098+
1099+
def test_graph_returns_pydot_dot(self):
1100+
import pydot
1101+
1102+
from tests.examples.traffic_light_machine import TrafficLightMachine
1103+
1104+
sm = TrafficLightMachine()
1105+
graph = sm._graph()
1106+
assert isinstance(graph, pydot.Dot)
1107+
1108+
def test_graph_reflects_active_state(self):
1109+
from tests.examples.traffic_light_machine import TrafficLightMachine
1110+
1111+
sm = TrafficLightMachine()
1112+
sm.send("cycle")
1113+
svg_root = _parse_svg(sm._graph())
1114+
yellow_node = _find_state_node(svg_root, "yellow")
1115+
assert yellow_node is not None

0 commit comments

Comments
 (0)