From e633508da16b9ede57d0846d7a95780622aa0049 Mon Sep 17 00:00:00 2001 From: Vladimir Babin Date: Thu, 24 Sep 2026 09:53:46 +0300 Subject: [PATCH] fix: honor Event(internal=True) when the event is triggered --- docs/processing_model.md | 2 +- docs/releases/3.2.2.md | 11 +++++++++-- statemachine/event.py | 4 +++- statemachine/statemachine.py | 1 + tests/test_events.py | 25 +++++++++++++++++++++++++ 5 files changed, 39 insertions(+), 4 deletions(-) diff --git a/docs/processing_model.md b/docs/processing_model.md index d8181316..16bceb03 100644 --- a/docs/processing_model.md +++ b/docs/processing_model.md @@ -80,7 +80,7 @@ The engine maintains two separate FIFO queues: | Queue | How to enqueue | When processed | |---|---|---| -| **Internal** | {func}`raise_() ` or `send(..., internal=True)` | Within the current macrostep | +| **Internal** | {func}`raise_() `, `send(..., internal=True)`, or an event declared with `Event(..., internal=True)` | Within the current macrostep | | **External** | {func}`send() ` | After the current macrostep ends | This distinction matters when you trigger events from inside callbacks. diff --git a/docs/releases/3.2.2.md b/docs/releases/3.2.2.md index b0a6405e..5febf082 100644 --- a/docs/releases/3.2.2.md +++ b/docs/releases/3.2.2.md @@ -80,8 +80,7 @@ Reported by [@Dolecor](https://github.com/Dolecor). `Event(dark.to(lit), delay=50)` rebuilt the event without its `delay`, so `BeaconsOfGondor.light.delay` was `0` and the event fired immediately instead of being queued. -The same happened to `internal`. Both are now preserved, although `internal` still has no -effect at trigger time: +The same happened to `internal`. Both are now preserved: ```py >>> class BeaconsOfGondor(StateChart): @@ -94,3 +93,11 @@ effect at trigger time: 50 ``` + +### `internal` declared on an `Event` ignored at trigger time + +`Event(a.to(b), internal=True)` kept `internal` on the declaration, but neither `sm.go()` nor +`sm.send("go")` carried it to the queue, so the event always went to the external queue. Both +paths now honor the declaration and put the event on the internal queue. + +[#655](https://github.com/fgmacedo/python-statemachine/issues/655). diff --git a/statemachine/event.py b/statemachine/event.py index 2c23f920..50470280 100644 --- a/statemachine/event.py +++ b/statemachine/event.py @@ -146,7 +146,9 @@ def __get__(self, instance, owner): """ if instance is None: return self - return BoundEvent(id=self.id, name=self.name, delay=self.delay, _sm=instance) + return BoundEvent( + id=self.id, name=self.name, delay=self.delay, internal=self.internal, _sm=instance + ) def put(self, *args, send_id: "str | None" = None, **kwargs): # The `__call__` is declared here to help IDEs knowing that an `Event` diff --git a/statemachine/statemachine.py b/statemachine/statemachine.py index 5277c4fc..8e536e79 100644 --- a/statemachine/statemachine.py +++ b/statemachine/statemachine.py @@ -483,6 +483,7 @@ def send( delay = ( delay if delay else know_event and know_event.delay or 0 ) # first the param, then the event, or 0 + internal = internal or bool(know_event and know_event.internal) event_instance = BoundEvent( id=event, name=event_name, delay=delay, internal=internal, _sm=self ) diff --git a/tests/test_events.py b/tests/test_events.py index fc00d675..2440bb29 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -71,6 +71,31 @@ class BeaconsOfGondor(StateChart): assert BeaconsOfGondor.light.delay == 50 assert BeaconsOfGondor().light.delay == 50 + @pytest.mark.parametrize("trigger", ["method", "send"]) + def test_declared_internal_puts_on_the_internal_queue(self, trigger): + class BeaconsOfGondor(StateChart): + dark = State(initial=True) + lit = State(final=True) + + light = Event(dark.to(lit), internal=True) + + sm = BeaconsOfGondor() + queued = [] + put = sm._engine.put + + def spy(trigger_data, internal=False, **kwargs): + queued.append(internal) + return put(trigger_data, internal=internal, **kwargs) + + sm._engine.put = spy + if trigger == "method": + sm.light() + else: + sm.send("light") + + assert queued == [True] + assert sm.lit.is_active + def test_derive_name_from_id(self): class StartMachine(StateChart): created = State(initial=True)