diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 5c496053..fdc758d6 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4 diff --git a/README.md b/README.md index eb93ce9f..7e9274fa 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,8 @@ You can now create an instance: This state machine can be represented graphically as follows: ```py +>>> # This example will only run on automated tests if dot is present +>>> getfixture("requires_dot_installed") >>> img_path = "docs/images/readme_trafficlightmachine.png" >>> sm._graph().write_png(img_path) diff --git a/conftest.py b/conftest.py index fcdcaf2c..4c2c0c63 100644 --- a/conftest.py +++ b/conftest.py @@ -1,3 +1,4 @@ +import shutil import sys import pytest @@ -31,3 +32,14 @@ def pytest_ignore_collect(collection_path, path, config): if "django_project" in str(path): return True + + +@pytest.fixture(scope="session") +def has_dot_installed(): + return bool(shutil.which("dot")) + + +@pytest.fixture() +def requires_dot_installed(request, has_dot_installed): + if not has_dot_installed: + pytest.skip(f"Test {request.node.nodeid} requires 'dot' that is not installed.") diff --git a/docs/actions.md b/docs/actions.md index ef8844be..f1c10c52 100644 --- a/docs/actions.md +++ b/docs/actions.md @@ -14,7 +14,7 @@ There are several action callbacks that you can define to interact with a StateMachine in execution. There are callbacks that you can specify that are generic and will be called -when something changes and are not bounded to a specific state or event: +when something changes, and are not bound to a specific state or event: - `before_transition()` @@ -26,7 +26,7 @@ when something changes and are not bounded to a specific state or event: - `after_transition()` -The following example can get you an overview of the "generic" callbacks available: +The following example offers an overview of the "generic" callbacks available: ```py >>> from statemachine import StateMachine, State diff --git a/docs/diagram.md b/docs/diagram.md index 2238bf46..8dae0aee 100644 --- a/docs/diagram.md +++ b/docs/diagram.md @@ -59,9 +59,23 @@ As this one: ![OrderControl](images/order_control_machine_initial.png) +If you find the resolution of the image lacking, you can + +```py +>>> dot.set_dpi(300) + +>>> dot.write_png("docs/images/order_control_machine_initial_300dpi.png") + +``` + +![OrderControl](images/order_control_machine_initial_300dpi.png) + + The current {ref}`state` is also highlighted: ``` py +>>> # This example will only run on automated tests if dot is present +>>> getfixture("requires_dot_installed") >>> from statemachine.contrib.diagram import DotGraphMachine diff --git a/docs/images/order_control_machine_initial.png b/docs/images/order_control_machine_initial.png index bd5cf06d..23f35e6a 100644 Binary files a/docs/images/order_control_machine_initial.png and b/docs/images/order_control_machine_initial.png differ diff --git a/docs/images/order_control_machine_initial_300dpi.png b/docs/images/order_control_machine_initial_300dpi.png new file mode 100644 index 00000000..ac76af90 Binary files /dev/null and b/docs/images/order_control_machine_initial_300dpi.png differ diff --git a/docs/images/order_control_machine_processing.png b/docs/images/order_control_machine_processing.png index 5355f078..a8e23fa9 100644 Binary files a/docs/images/order_control_machine_processing.png and b/docs/images/order_control_machine_processing.png differ diff --git a/docs/images/readme_trafficlightmachine.png b/docs/images/readme_trafficlightmachine.png index f52ea2cc..85f38f45 100644 Binary files a/docs/images/readme_trafficlightmachine.png and b/docs/images/readme_trafficlightmachine.png differ diff --git a/docs/images/test_state_machine_internal.png b/docs/images/test_state_machine_internal.png index 77806cdb..bbe8fb48 100644 Binary files a/docs/images/test_state_machine_internal.png and b/docs/images/test_state_machine_internal.png differ diff --git a/docs/transitions.md b/docs/transitions.md index 1752224c..32d17236 100644 --- a/docs/transitions.md +++ b/docs/transitions.md @@ -131,6 +131,9 @@ Example: Usage: ```py +>>> # This example will only run on automated tests if dot is present +>>> getfixture("requires_dot_installed") + >>> sm = TestStateMachine() >>> sm._graph().write_png("docs/images/test_state_machine_internal.png") @@ -163,7 +166,7 @@ the event name is used to describe the transition. ## Events An event is an external signal that something has happened. -They are send to a state machine and allow the state machine to react. +They are sent to a state machine and allow the state machine to react. An event starts a {ref}`transition`, which can be thought of as a "cause" that 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 ### Declaring events -The simplest way to declare an {ref}`event` is by assiging a transitions list to a name at the +The simplest way to declare an {ref}`event` is by assigning a transitions list to a name at the State machine class level. The name will be converted to an {ref}`Event`: ```py @@ -193,7 +196,7 @@ True ``` ```{versionadded} 2.4.0 -You can also explict declare an {ref}`Event` instance, this helps IDEs to know that the event is callable and also with transtation strings. +You can also explictly declare an {ref}`Event` instance, this helps IDEs to know that the event is callable, and also with translation strings. ``` To declare an explicit event you must also import the {ref}`Event`: diff --git a/pyproject.toml b/pyproject.toml index 83ab3fd8..b324b715 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,8 +2,8 @@ name = "python-statemachine" version = "2.5.0" description = "Python Finite State Machines made easy." -authors = [{ name = "Fernando Macedo", email = "fgmacedo@gmail" }] -maintainers = [{ name = "Fernando Macedo", email = "fgmacedo@gmail" }] +authors = [{ name = "Fernando Macedo", email = "fgmacedo@gmail.com" }] +maintainers = [{ name = "Fernando Macedo", email = "fgmacedo@gmail.com" }] license = { text = "MIT License" } readme = "README.md" classifiers = [ diff --git a/statemachine/event.py b/statemachine/event.py index d8fa511d..a82d9186 100644 --- a/statemachine/event.py +++ b/statemachine/event.py @@ -28,9 +28,9 @@ class Event(AddCallbacksMixin, str): - """An event is triggers a signal that something has happened. + """An event triggers a signal that something has happened. - They are send to a state machine and allow the state machine to react. + They are sent to a state machine and allow the state machine to react. An event starts a :ref:`Transition`, which can be thought of as a “cause” that initiates a change in the state of the system. diff --git a/statemachine/statemachine.py b/statemachine/statemachine.py index 378d55b5..e5fe2628 100644 --- a/statemachine/statemachine.py +++ b/statemachine/statemachine.py @@ -75,7 +75,7 @@ def __init__( allow_event_without_transition: bool = False, listeners: "List[object] | None" = None, ): - self.model = model if model else Model() + self.model = model if model is not None else Model() self.state_field = state_field self.start_value = start_value self.allow_event_without_transition = allow_event_without_transition diff --git a/tests/scrape_images.py b/tests/scrape_images.py index 1f57b6aa..2547b536 100644 --- a/tests/scrape_images.py +++ b/tests/scrape_images.py @@ -1,3 +1,4 @@ +import os import re from statemachine.contrib.diagram import DotGraphMachine @@ -13,7 +14,8 @@ class MachineScraper: def __init__(self, project_root): self.project_root = project_root - self.re_machine_module_name = re.compile(f"{self.project_root}/(.*).py$") + sanitized_path = re.escape(os.path.abspath(self.project_root)) + self.re_machine_module_name = re.compile(f"{sanitized_path}/(.*)\\.py$") self.seen = set() def __repr__(self): diff --git a/tests/test_contrib_diagram.py b/tests/test_contrib_diagram.py index b54a43d3..3b2516b3 100644 --- a/tests/test_contrib_diagram.py +++ b/tests/test_contrib_diagram.py @@ -7,6 +7,8 @@ from statemachine.contrib.diagram import main from statemachine.contrib.diagram import quickchart_write_svg +pytestmark = pytest.mark.usefixtures("requires_dot_installed") + @pytest.fixture( params=[ diff --git a/tests/test_statemachine.py b/tests/test_statemachine.py index 9337e025..9ff82897 100644 --- a/tests/test_statemachine.py +++ b/tests/test_statemachine.py @@ -488,3 +488,18 @@ class TrapStateMachine(StateMachine): start = started.to(producing) close = started.to(closed) add_job = producing.to.itself(internal=True) + + +def test_model_with_custom_bool_is_not_replaced(campaign_machine): + class FalseyModel(MyModel): + def __bool__(self): + return False + + model = FalseyModel() + machine = campaign_machine(model) + + assert machine.model is model + assert model.state == "draft" + + machine.produce() + assert model.state == "producing"