python-statemachine 2.5.0__tar.gz → 2.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/.github/workflows/python-package.yml +3 -7
  2. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/.github/workflows/release.yml +1 -1
  3. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/.pre-commit-config.yaml +1 -1
  4. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/.readthedocs.yaml +1 -1
  5. python_statemachine-2.6.0/AGENTS.md +114 -0
  6. python_statemachine-2.6.0/CLAUDE.md +1 -0
  7. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/PKG-INFO +6 -2
  8. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/README.md +2 -0
  9. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/conftest.py +14 -1
  10. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/actions.md +2 -2
  11. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/authors.md +1 -0
  12. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/diagram.md +14 -0
  13. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/guards.md +104 -1
  14. python_statemachine-2.6.0/docs/images/order_control_machine_initial.png +0 -0
  15. python_statemachine-2.6.0/docs/images/order_control_machine_initial_300dpi.png +0 -0
  16. python_statemachine-2.6.0/docs/images/order_control_machine_processing.png +0 -0
  17. python_statemachine-2.6.0/docs/images/readme_trafficlightmachine.png +0 -0
  18. python_statemachine-2.6.0/docs/images/test_state_machine_internal.png +0 -0
  19. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/integrations.md +19 -0
  20. python_statemachine-2.6.0/docs/releases/2.6.0.md +128 -0
  21. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/releases/index.md +1 -0
  22. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/transitions.md +6 -3
  23. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/pyproject.toml +10 -6
  24. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/statemachine/__init__.py +1 -1
  25. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/statemachine/engines/async_.py +28 -0
  26. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/statemachine/engines/sync.py +26 -0
  27. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/statemachine/event.py +2 -2
  28. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/statemachine/locale/en/LC_MESSAGES/statemachine.po +35 -20
  29. python_statemachine-2.6.0/statemachine/locale/hi_IN/LC_MESSAGES/statemachine.po +119 -0
  30. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/statemachine/locale/pt_BR/LC_MESSAGES/statemachine.po +53 -26
  31. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/statemachine/locale/zh_CN/LC_MESSAGES/statemachine.po +34 -20
  32. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/statemachine/mixins.py +11 -0
  33. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/statemachine/signature.py +79 -7
  34. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/statemachine/spec_parser.py +62 -8
  35. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/statemachine/statemachine.py +22 -3
  36. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/statemachine/transition_mixin.py +9 -2
  37. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/django_project/workflow/models.py +0 -1
  38. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/django_project/workflow/statemachines.py +2 -1
  39. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/examples/air_conditioner_machine.py +2 -1
  40. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/examples/enum_campaign_machine.py +2 -1
  41. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/examples/lor_machine.py +2 -1
  42. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/examples/order_control_rich_model_machine.py +2 -1
  43. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/examples/user_machine.py +3 -2
  44. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/scrape_images.py +3 -1
  45. python_statemachine-2.6.0/tests/test_async.py +279 -0
  46. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/test_contrib_diagram.py +2 -1
  47. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/test_copy.py +51 -2
  48. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/test_dispatcher.py +0 -1
  49. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/test_listener.py +0 -1
  50. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/test_mixins.py +16 -1
  51. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/test_registry.py +1 -1
  52. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/test_signature.py +129 -1
  53. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/test_signature_positional_only.py +0 -1
  54. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/test_spec_parser.py +96 -1
  55. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/test_statemachine.py +146 -0
  56. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/test_transitions.py +16 -2
  57. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/uv.lock +693 -232
  58. python_statemachine-2.5.0/docs/images/order_control_machine_initial.png +0 -0
  59. python_statemachine-2.5.0/docs/images/order_control_machine_processing.png +0 -0
  60. python_statemachine-2.5.0/docs/images/readme_trafficlightmachine.png +0 -0
  61. python_statemachine-2.5.0/docs/images/test_state_machine_internal.png +0 -0
  62. python_statemachine-2.5.0/statemachine/locale/hi_IN/LC_MESSAGES/statemachine.po +0 -93
  63. python_statemachine-2.5.0/tests/test_async.py +0 -121
  64. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/.git-blame-ignore-revs +0 -0
  65. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/.github/FUNDING.yml +0 -0
  66. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/.github/ISSUE_TEMPLATE.md +0 -0
  67. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/.gitignore +0 -0
  68. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/LICENSE +0 -0
  69. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/contributing.md +0 -0
  70. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/_static/custom_machine.css +0 -0
  71. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/api.md +0 -0
  72. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/async.md +0 -0
  73. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/conf.py +0 -0
  74. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/contributing.md +0 -0
  75. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/images/_oc_machine_processing.svg +0 -0
  76. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/images/lab_approval_machine_accepted.png +0 -0
  77. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/images/oc_machine_processing.svg +0 -0
  78. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/images/python-statemachine.png +0 -0
  79. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/images/traffic_light_machine.png +0 -0
  80. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/index.md +0 -0
  81. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/installation.md +0 -0
  82. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/listeners.md +0 -0
  83. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/mixins.md +0 -0
  84. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/models.md +0 -0
  85. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/processing_model.md +0 -0
  86. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/readme.md +0 -0
  87. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/releases/0.1.0.md +0 -0
  88. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/releases/0.2.0.md +0 -0
  89. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/releases/0.3.0.md +0 -0
  90. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/releases/0.4.2.md +0 -0
  91. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/releases/0.5.0.md +0 -0
  92. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/releases/0.5.1.md +0 -0
  93. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/releases/0.6.0.md +0 -0
  94. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/releases/0.6.1.md +0 -0
  95. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/releases/0.6.2.md +0 -0
  96. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/releases/0.7.0.md +0 -0
  97. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/releases/0.7.1.md +0 -0
  98. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/releases/0.8.0.md +0 -0
  99. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/releases/0.9.0.md +0 -0
  100. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/releases/1.0.0.md +0 -0
  101. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/releases/1.0.1.md +0 -0
  102. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/releases/1.0.2.md +0 -0
  103. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/releases/1.0.3.md +0 -0
  104. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/releases/2.0.0.md +0 -0
  105. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/releases/2.1.0.md +0 -0
  106. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/releases/2.1.1.md +0 -0
  107. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/releases/2.1.2.md +0 -0
  108. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/releases/2.2.0.md +0 -0
  109. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/releases/2.3.0.md +0 -0
  110. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/releases/2.3.1.md +0 -0
  111. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/releases/2.3.2.md +0 -0
  112. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/releases/2.3.3.md +0 -0
  113. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/releases/2.3.4.md +0 -0
  114. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/releases/2.3.5.md +0 -0
  115. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/releases/2.3.6.md +0 -0
  116. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/releases/2.4.0.md +0 -0
  117. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/releases/2.5.0.md +0 -0
  118. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/docs/states.md +0 -0
  119. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/statemachine/callbacks.py +0 -0
  120. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/statemachine/contrib/__init__.py +0 -0
  121. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/statemachine/contrib/diagram.py +0 -0
  122. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/statemachine/dispatcher.py +0 -0
  123. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/statemachine/engines/__init__.py +0 -0
  124. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/statemachine/engines/base.py +0 -0
  125. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/statemachine/event_data.py +0 -0
  126. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/statemachine/events.py +0 -0
  127. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/statemachine/exceptions.py +0 -0
  128. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/statemachine/factory.py +0 -0
  129. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/statemachine/graph.py +0 -0
  130. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/statemachine/i18n.py +0 -0
  131. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/statemachine/model.py +0 -0
  132. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/statemachine/py.typed +0 -0
  133. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/statemachine/registry.py +0 -0
  134. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/statemachine/state.py +0 -0
  135. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/statemachine/states.py +0 -0
  136. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/statemachine/transition.py +0 -0
  137. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/statemachine/transition_list.py +0 -0
  138. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/statemachine/utils.py +0 -0
  139. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/__init__.py +0 -0
  140. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/conftest.py +0 -0
  141. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/django_project/app.py +0 -0
  142. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/django_project/core/__init__,.py +0 -0
  143. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/django_project/core/settings.py +0 -0
  144. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/django_project/core/wsgi.py +0 -0
  145. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/django_project/manage.py +0 -0
  146. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/django_project/workflow/__init__.py +0 -0
  147. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/django_project/workflow/apps.py +0 -0
  148. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/django_project/workflow/tests.py +1 -1
  149. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/examples/README.rst +0 -0
  150. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/examples/__init__.py +0 -0
  151. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/examples/all_actions_machine.py +0 -0
  152. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/examples/async_guess_the_number_machine.py +0 -0
  153. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/examples/async_without_loop_machine.py +0 -0
  154. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/examples/guess_the_number_machine.py +0 -0
  155. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/examples/order_control_machine.py +0 -0
  156. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/examples/persistent_model_machine.py +0 -0
  157. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/examples/recursive_event_machine.py +0 -0
  158. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/examples/reusing_transitions_machine.py +0 -0
  159. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/examples/traffic_light_machine.py +0 -0
  160. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/helpers.py +0 -0
  161. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/models.py +0 -0
  162. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/test_actions.py +0 -0
  163. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/test_callbacks.py +3 -3
  164. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/test_callbacks_isolation.py +0 -0
  165. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/test_conditions_algebra.py +1 -1
  166. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/test_events.py +2 -2
  167. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/test_examples.py +0 -0
  168. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/test_mock_compatibility.py +0 -0
  169. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/test_multiple_destinations.py +0 -0
  170. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/test_profiling.py +0 -0
  171. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/test_rtc.py +2 -2
  172. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/test_state.py +0 -0
  173. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/test_state_callbacks.py +0 -0
  174. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/test_statemachine_bounded_transitions.py +0 -0
  175. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/test_statemachine_inheritance.py +0 -0
  176. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/test_threading.py +0 -0
  177. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/test_transition_list.py +2 -2
  178. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/testcases/issue308.md +0 -0
  179. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/testcases/issue384_multiple_observers.md +0 -0
  180. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/testcases/issue434.md +0 -0
  181. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/testcases/issue449.md +0 -0
  182. {python_statemachine-2.5.0 → python_statemachine-2.6.0}/tests/testcases/issue480.md +0 -0
@@ -15,7 +15,7 @@ jobs:
15
15
  strategy:
16
16
  fail-fast: false
17
17
  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"]
19
19
 
20
20
  steps:
21
21
  - uses: actions/checkout@v4
@@ -33,15 +33,11 @@ jobs:
33
33
  cache-suffix: "python${{ matrix.python-version }}"
34
34
  - name: Install the project
35
35
  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
40
36
  #----------------------------------------------
41
37
  # run ruff
42
38
  #----------------------------------------------
43
39
  - name: Linter with ruff
44
- if: matrix.python-version == 3.13
40
+ if: matrix.python-version == 3.14
45
41
  run: |
46
42
  uv run ruff check .
47
43
  uv run ruff format --check .
@@ -57,7 +53,7 @@ jobs:
57
53
  #----------------------------------------------
58
54
  - name: Upload coverage to Codecov
59
55
  uses: codecov/codecov-action@v4
60
- if: matrix.python-version == 3.13
56
+ if: matrix.python-version == 3.14
61
57
  with:
62
58
  token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
63
59
  directory: .
@@ -18,7 +18,7 @@ jobs:
18
18
  - name: Setup Python
19
19
  uses: actions/setup-python@v5
20
20
  with:
21
- python-version: '3.13'
21
+ python-version: '3.14'
22
22
 
23
23
  - name: Setup Graphviz
24
24
  uses: ts-graphviz/setup-graphviz@v2
@@ -9,7 +9,7 @@ repos:
9
9
  exclude: docs/auto_examples
10
10
  - repo: https://github.com/charliermarsh/ruff-pre-commit
11
11
  # Ruff version.
12
- rev: v0.8.1
12
+ rev: v0.15.0
13
13
  hooks:
14
14
  # Run the linter.
15
15
  - id: ruff
@@ -8,7 +8,7 @@ version: 2
8
8
  build:
9
9
  os: "ubuntu-22.04"
10
10
  tools:
11
- python: "3.12"
11
+ python: "3.14"
12
12
  apt_packages:
13
13
  - graphviz
14
14
  jobs:
@@ -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
@@ -0,0 +1 @@
1
+ AGENTS.md
@@ -1,11 +1,12 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: python-statemachine
3
- Version: 2.5.0
3
+ Version: 2.6.0
4
4
  Summary: Python Finite State Machines made easy.
5
5
  Project-URL: homepage, https://github.com/fgmacedo/python-statemachine
6
6
  Author-email: Fernando Macedo <fgmacedo@gmail.com>
7
7
  Maintainer-email: Fernando Macedo <fgmacedo@gmail.com>
8
8
  License: MIT License
9
+ License-File: LICENSE
9
10
  Classifier: Development Status :: 5 - Production/Stable
10
11
  Classifier: Framework :: AsyncIO
11
12
  Classifier: Framework :: Django
@@ -19,6 +20,7 @@ Classifier: Programming Language :: Python :: 3.10
19
20
  Classifier: Programming Language :: Python :: 3.11
20
21
  Classifier: Programming Language :: Python :: 3.12
21
22
  Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
22
24
  Classifier: Topic :: Home Automation
23
25
  Classifier: Topic :: Software Development :: Libraries
24
26
  Requires-Python: >=3.7
@@ -127,6 +129,8 @@ You can now create an instance:
127
129
  This state machine can be represented graphically as follows:
128
130
 
129
131
  ```py
132
+ >>> # This example will only run on automated tests if dot is present
133
+ >>> getfixture("requires_dot_installed")
130
134
  >>> img_path = "docs/images/readme_trafficlightmachine.png"
131
135
  >>> sm._graph().write_png(img_path)
132
136
 
@@ -99,6 +99,8 @@ You can now create an instance:
99
99
  This state machine can be represented graphically as follows:
100
100
 
101
101
  ```py
102
+ >>> # This example will only run on automated tests if dot is present
103
+ >>> getfixture("requires_dot_installed")
102
104
  >>> img_path = "docs/images/readme_trafficlightmachine.png"
103
105
  >>> sm._graph().write_png(img_path)
104
106
 
@@ -1,3 +1,4 @@
1
+ import shutil
1
2
  import sys
2
3
 
3
4
  import pytest
@@ -5,9 +6,10 @@ import pytest
5
6
 
6
7
  @pytest.fixture(autouse=True, scope="session")
7
8
  def add_doctest_context(doctest_namespace): # noqa: PT004
9
+ from statemachine.utils import run_async_from_sync
10
+
8
11
  from statemachine import State
9
12
  from statemachine import StateMachine
10
- from statemachine.utils import run_async_from_sync
11
13
 
12
14
  class ContribAsyncio:
13
15
  """
@@ -31,3 +33,14 @@ def pytest_ignore_collect(collection_path, path, config):
31
33
 
32
34
  if "django_project" in str(path):
33
35
  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.")
@@ -14,7 +14,7 @@ There are several action callbacks that you can define to interact with a
14
14
  StateMachine in execution.
15
15
 
16
16
  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:
18
18
 
19
19
  - `before_transition()`
20
20
 
@@ -26,7 +26,7 @@ when something changes and are not bounded to a specific state or event:
26
26
 
27
27
  - `after_transition()`
28
28
 
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:
30
30
 
31
31
  ```py
32
32
  >>> from statemachine import StateMachine, State
@@ -10,6 +10,7 @@
10
10
  * [Rafael Rêgo](mailto:crafards@gmail.com)
11
11
  * [Raphael Schrader](mailto:raphael@schradercloud.de)
12
12
  * [João S. O. Bueno](mailto:gwidion@gmail.com)
13
+ * [Rodrigo Nogueira](mailto:rodrigo.b.nogueira@gmail.com)
13
14
 
14
15
 
15
16
  ## Scaffolding
@@ -59,9 +59,23 @@ As this one:
59
59
  ![OrderControl](images/order_control_machine_initial.png)
60
60
 
61
61
 
62
+ If you find the resolution of the image lacking, you can
63
+
64
+ ```py
65
+ >>> dot.set_dpi(300)
66
+
67
+ >>> dot.write_png("docs/images/order_control_machine_initial_300dpi.png")
68
+
69
+ ```
70
+
71
+ ![OrderControl](images/order_control_machine_initial_300dpi.png)
72
+
73
+
62
74
  The current {ref}`state` is also highlighted:
63
75
 
64
76
  ``` py
77
+ >>> # This example will only run on automated tests if dot is present
78
+ >>> getfixture("requires_dot_installed")
65
79
 
66
80
  >>> from statemachine.contrib.diagram import DotGraphMachine
67
81
 
@@ -22,7 +22,37 @@ A conditional transition occurs only if specific conditions or criteria are met.
22
22
 
23
23
  When a transition is conditional, it includes a condition (also known as a _guard_) that must be satisfied for the transition to take place. If the condition is not met, the transition does not occur, and the state machine remains in its current state or follows an alternative path.
24
24
 
25
- This feature allows for multiple transitions on the same {ref}`event`, with each {ref}`transition` checked in the order they are declared. A condition acts like a predicate (a function that evaluates to true/false) and is checked when a {ref}`statemachine` handles an {ref}`event` with a transition from the current state bound to this event. The first transition that meets the conditions (if any) is executed. If none of the transitions meet the conditions, the state machine either raises an exception or does nothing (see the `allow_event_without_transition` parameter of {ref}`StateMachine`).
25
+ This feature allows for multiple transitions on the same {ref}`event`, with each {ref}`transition` checked in **declaration order** — that is, the order in which the transitions themselves were created using `state.to()`. A condition acts like a predicate (a function that evaluates to true/false) and is checked when a {ref}`statemachine` handles an {ref}`event` with a transition from the current state bound to this event. The first transition that meets the conditions (if any) is executed. If none of the transitions meet the conditions, the state machine either raises an exception or does nothing (see the `allow_event_without_transition` parameter of {ref}`StateMachine`).
26
+
27
+ ````{important}
28
+ **Evaluation order is based on declaration order, not composition order.**
29
+
30
+ When using conditional transitions, the order of evaluation is determined by **when each transition was created** (the order of `state.to()` calls), **not** by the order they appear when combined with the `|` operator.
31
+
32
+ For example:
33
+
34
+ ```python
35
+ # These are evaluated in DECLARATION ORDER (when state.to() was called):
36
+ created_first = state_a.to(state_x) # Created FIRST → Checked FIRST
37
+ created_second = state_a.to(state_y) # Created SECOND → Checked SECOND
38
+ created_third = state_a.to(state_z) # Created THIRD → Checked THIRD
39
+
40
+ # The | operator does NOT change evaluation order:
41
+ my_event = created_third | created_second | created_first
42
+ # Evaluation order is still: created_first → created_second → created_third
43
+ ```
44
+
45
+ To control the evaluation order, declare transitions in the desired order:
46
+
47
+ ```python
48
+ # Declare in the order you want them checked:
49
+ first = state_a.to(state_b, cond="check1") # Checked FIRST
50
+ second = state_a.to(state_c, cond="check2") # Checked SECOND
51
+ third = state_a.to(state_d, cond="check3") # Checked THIRD
52
+
53
+ my_event = first | second | third # Order matches declaration
54
+ ```
55
+ ````
26
56
 
27
57
  When {ref}`transitions` have guards, it is possible to define two or more transitions for the same {ref}`event` from the same {ref}`state`. When the {ref}`event` occurs, the guarded transitions are checked one by one, and the first transition whose guard is true will be executed, while the others will be ignored.
28
58
 
@@ -129,6 +159,79 @@ So, a condition `s1.to(s2, cond=lambda: [])` will evaluate as `False`, as an emp
129
159
  **falsy** value.
130
160
  ```
131
161
 
162
+ ### Checking enabled events
163
+
164
+ The {ref}`StateMachine.allowed_events` property returns events reachable from the current state,
165
+ but it does **not** evaluate `cond`/`unless` guards. To check which events actually have their
166
+ conditions satisfied, use {ref}`StateMachine.enabled_events`.
167
+
168
+ ```{testsetup}
169
+
170
+ >>> from statemachine import StateMachine, State
171
+
172
+ ```
173
+
174
+ ```py
175
+ >>> class ApprovalMachine(StateMachine):
176
+ ... pending = State(initial=True)
177
+ ... approved = State(final=True)
178
+ ... rejected = State(final=True)
179
+ ...
180
+ ... approve = pending.to(approved, cond="is_manager")
181
+ ... reject = pending.to(rejected)
182
+ ...
183
+ ... is_manager = False
184
+
185
+ >>> sm = ApprovalMachine()
186
+
187
+ >>> [e.id for e in sm.allowed_events]
188
+ ['approve', 'reject']
189
+
190
+ >>> [e.id for e in sm.enabled_events()]
191
+ ['reject']
192
+
193
+ >>> sm.is_manager = True
194
+
195
+ >>> [e.id for e in sm.enabled_events()]
196
+ ['approve', 'reject']
197
+
198
+ ```
199
+
200
+ `enabled_events` is a method (not a property) because conditions may depend on runtime
201
+ arguments. Any `*args`/`**kwargs` passed to `enabled_events()` are forwarded to the
202
+ condition callbacks, just like when triggering an event:
203
+
204
+ ```py
205
+ >>> class TaskMachine(StateMachine):
206
+ ... idle = State(initial=True)
207
+ ... running = State(final=True)
208
+ ...
209
+ ... start = idle.to(running, cond="has_enough_resources")
210
+ ...
211
+ ... def has_enough_resources(self, cpu=0):
212
+ ... return cpu >= 4
213
+
214
+ >>> sm = TaskMachine()
215
+
216
+ >>> sm.enabled_events()
217
+ []
218
+
219
+ >>> [e.id for e in sm.enabled_events(cpu=8)]
220
+ ['start']
221
+
222
+ ```
223
+
224
+ ```{tip}
225
+ This is useful for UI scenarios where you want to show or hide buttons based on whether
226
+ an event's conditions are currently satisfied.
227
+ ```
228
+
229
+ ```{note}
230
+ An event is considered **enabled** if at least one of its transitions from the current state
231
+ has all conditions satisfied. If a condition raises an exception, the event is treated as
232
+ enabled (permissive behavior).
233
+ ```
234
+
132
235
  ## Validators
133
236
 
134
237
 
@@ -69,3 +69,22 @@ class Campaign(models.Model, MachineMixin):
69
69
  ```{seealso}
70
70
  Learn more about using the [](mixins.md#machinemixin).
71
71
  ```
72
+
73
+ ### Data migrations
74
+
75
+ Django's `apps.get_model()` returns **historical model** classes that are dynamically created
76
+ and don't carry user-defined class attributes like `state_machine_name`. Since version 2.6.0,
77
+ `MachineMixin` detects these historical models and gracefully skips state machine
78
+ initialization, so data migrations that use `apps.get_model()` work without errors.
79
+
80
+ ```{note}
81
+ The state machine instance will **not** be available on historical model objects.
82
+ If your data migration needs to interact with the state machine, set the attributes
83
+ manually on the historical model class:
84
+
85
+ def backfill_data(apps, schema_editor):
86
+ MyModel = apps.get_model("myapp", "MyModel")
87
+ MyModel.state_machine_name = "myapp.statemachines.MyStateMachine"
88
+ for obj in MyModel.objects.all():
89
+ obj.statemachine # now available
90
+ ```
@@ -0,0 +1,128 @@
1
+ # StateMachine 2.6.0
2
+
3
+ *February 2026*
4
+
5
+ ## What's new in 2.6.0
6
+
7
+ This release adds the {ref}`StateMachine.enabled_events` method, Python 3.14 support,
8
+ a significant performance improvement for callback dispatch, and several bugfixes
9
+ for async condition expressions, type checker compatibility, and Django integration.
10
+
11
+ ### Python compatibility in 2.6.0
12
+
13
+ StateMachine 2.6.0 supports Python 3.7, 3.8, 3.9, 3.10, 3.11, 3.12, 3.13, and 3.14.
14
+
15
+ ### Checking enabled events
16
+
17
+ A new {ref}`StateMachine.enabled_events` method lets you query which events have their
18
+ `cond`/`unless` guards currently satisfied, going beyond {ref}`StateMachine.allowed_events`
19
+ which only checks reachability from the current state.
20
+
21
+ This is particularly useful for **UI scenarios** where you want to enable or disable buttons
22
+ based on whether an event's conditions are met at runtime.
23
+
24
+ ```{testsetup}
25
+
26
+ >>> from statemachine import StateMachine, State
27
+
28
+ ```
29
+
30
+ ```py
31
+ >>> class ApprovalMachine(StateMachine):
32
+ ... pending = State(initial=True)
33
+ ... approved = State(final=True)
34
+ ... rejected = State(final=True)
35
+ ...
36
+ ... approve = pending.to(approved, cond="is_manager")
37
+ ... reject = pending.to(rejected)
38
+ ...
39
+ ... is_manager = False
40
+
41
+ >>> sm = ApprovalMachine()
42
+
43
+ >>> [e.id for e in sm.allowed_events]
44
+ ['approve', 'reject']
45
+
46
+ >>> [e.id for e in sm.enabled_events()]
47
+ ['reject']
48
+
49
+ >>> sm.is_manager = True
50
+
51
+ >>> [e.id for e in sm.enabled_events()]
52
+ ['approve', 'reject']
53
+
54
+ ```
55
+
56
+ Since conditions may depend on runtime arguments, any `*args`/`**kwargs` passed to
57
+ `enabled_events()` are forwarded to the condition callbacks:
58
+
59
+ ```py
60
+ >>> class TaskMachine(StateMachine):
61
+ ... idle = State(initial=True)
62
+ ... running = State(final=True)
63
+ ...
64
+ ... start = idle.to(running, cond="has_enough_resources")
65
+ ...
66
+ ... def has_enough_resources(self, cpu=0):
67
+ ... return cpu >= 4
68
+
69
+ >>> sm = TaskMachine()
70
+
71
+ >>> sm.enabled_events()
72
+ []
73
+
74
+ >>> [e.id for e in sm.enabled_events(cpu=8)]
75
+ ['start']
76
+
77
+ ```
78
+
79
+ ```{seealso}
80
+ See {ref}`Checking enabled events` in the Guards documentation for more details.
81
+ ```
82
+
83
+ ### Performance: cached signature binding
84
+
85
+ Callback dispatch is now significantly faster thanks to cached signature binding in
86
+ `SignatureAdapter`. The first call to a callback computes the argument binding and
87
+ caches a fast-path template; subsequent calls with the same argument shape skip the
88
+ full binding logic.
89
+
90
+ This results in approximately **60% faster** `bind_expected()` calls and
91
+ around **30% end-to-end improvement** on hot transition paths.
92
+
93
+ See [#548](https://github.com/fgmacedo/python-statemachine/issues/548) for benchmarks.
94
+
95
+
96
+ ## Bugfixes in 2.6.0
97
+
98
+ - Fixes [#531](https://github.com/fgmacedo/python-statemachine/issues/531) domain model
99
+ with falsy `__bool__` was being replaced by the default `Model()`.
100
+ - Fixes [#535](https://github.com/fgmacedo/python-statemachine/issues/535) async predicates
101
+ in condition expressions (`not`, `and`, `or`) were not being awaited, causing guards to
102
+ silently return incorrect results.
103
+ - Fixes [#548](https://github.com/fgmacedo/python-statemachine/issues/548)
104
+ `VAR_POSITIONAL` and kwargs precedence bugs in the signature binding cache introduced
105
+ by the performance optimization.
106
+ - Fixes [#511](https://github.com/fgmacedo/python-statemachine/issues/511) Pyright/Pylance
107
+ false positive "Argument missing for parameter f" when calling events. Static analyzers
108
+ could not follow the metaclass transformation from `TransitionList` to `Event`.
109
+ - Fixes [#551](https://github.com/fgmacedo/python-statemachine/issues/551) `MachineMixin`
110
+ now gracefully skips state machine initialization for Django historical models in data
111
+ migrations, instead of raising `ValueError`.
112
+ - Fixes [#526](https://github.com/fgmacedo/python-statemachine/issues/526) sanitize project
113
+ path on Windows for documentation builds.
114
+
115
+
116
+ ## Misc in 2.6.0
117
+
118
+ - Added Python 3.14 support [#552](https://github.com/fgmacedo/python-statemachine/pull/552).
119
+ - Upgraded dev dependencies: ruff to 0.15.0, mypy to 1.14.1
120
+ [#552](https://github.com/fgmacedo/python-statemachine/pull/552).
121
+ - Clarified conditional transition evaluation order in documentation
122
+ [#546](https://github.com/fgmacedo/python-statemachine/pull/546).
123
+ - Added pydot DPI resolution settings to diagram documentation
124
+ [#514](https://github.com/fgmacedo/python-statemachine/pull/514).
125
+ - Fixed miscellaneous typos in documentation
126
+ [#522](https://github.com/fgmacedo/python-statemachine/pull/522).
127
+ - Removed Python 3.7 from CI build matrix
128
+ [ef351d5](https://github.com/fgmacedo/python-statemachine/commit/ef351d5).
@@ -15,6 +15,7 @@ Below are release notes through StateMachine and its patch releases.
15
15
  ```{toctree}
16
16
  :maxdepth: 2
17
17
 
18
+ 2.6.0
18
19
  2.5.0
19
20
  2.4.0
20
21
  2.3.6
@@ -131,6 +131,9 @@ Example:
131
131
  Usage:
132
132
 
133
133
  ```py
134
+ >>> # This example will only run on automated tests if dot is present
135
+ >>> getfixture("requires_dot_installed")
136
+
134
137
  >>> sm = TestStateMachine()
135
138
 
136
139
  >>> sm._graph().write_png("docs/images/test_state_machine_internal.png")
@@ -163,7 +166,7 @@ the event name is used to describe the transition.
163
166
  ## Events
164
167
 
165
168
  An event is an external signal that something has happened.
166
- They are send to a state machine and allow the state machine to react.
169
+ They are sent to a state machine and allow the state machine to react.
167
170
 
168
171
  An event starts a {ref}`transition`, which can be thought of as a "cause" that
169
172
  initiates a change in the state of the system.
@@ -173,7 +176,7 @@ In `python-statemachine`, an event is specified as an attribute of the state mac
173
176
 
174
177
  ### Declaring events
175
178
 
176
- The simplest way to declare an {ref}`event` is by assiging a transitions list to a name at the
179
+ The simplest way to declare an {ref}`event` is by assigning a transitions list to a name at the
177
180
  State machine class level. The name will be converted to an {ref}`Event`:
178
181
 
179
182
  ```py
@@ -193,7 +196,7 @@ True
193
196
  ```
194
197
 
195
198
  ```{versionadded} 2.4.0
196
- You can also explict declare an {ref}`Event` instance, this helps IDEs to know that the event is callable and also with transtation strings.
199
+ You can also explictly declare an {ref}`Event` instance, this helps IDEs to know that the event is callable, and also with translation strings.
197
200
  ```
198
201
 
199
202
  To declare an explicit event you must also import the {ref}`Event`: