From c4b46424caa216b1d9cba87b0d83fed8035683fb Mon Sep 17 00:00:00 2001 From: Julian Kimmig Date: Wed, 24 Dec 2025 06:37:09 +0100 Subject: [PATCH 1/4] fix(eventmanager): prevent skipping listeners during event emission Modified the event listener handling to ensure that modifying the listener list during event emission does not skip any callbacks. Updated tests to verify the correct behavior when listeners are added or removed while events are being emitted. --- src/funcnodes_core/eventmanager.py | 4 +-- tests/test_eventmanager.py | 49 ++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/funcnodes_core/eventmanager.py b/src/funcnodes_core/eventmanager.py index 11880ff..e3f2328 100644 --- a/src/funcnodes_core/eventmanager.py +++ b/src/funcnodes_core/eventmanager.py @@ -336,12 +336,12 @@ def emit(self, event_name: str, msg: MessageInArgs | None = None) -> bool: raise ValueError("src is a reserved keyword") msg["src"] = self listened = False - if event_name in self._events: + if event_name in self._events[:]: for callback in self._events[event_name]: callback(**msg) listened = True if "*" in self._events: - for callback in self._events["*"]: + for callback in self._events["*"][:]: callback(event=event_name, **msg) listened = True diff --git a/tests/test_eventmanager.py b/tests/test_eventmanager.py index 7fba0a5..931d07e 100644 --- a/tests/test_eventmanager.py +++ b/tests/test_eventmanager.py @@ -597,3 +597,52 @@ async def async_test_function(self): {"src": emitter, "result": "async_function_result"}, ) assert result == "async_function_result" + + +class EmitterTest(EventEmitterMixin): + pass + + +@pytest.mark.asyncio +async def test_event_manager_modification_during_emit(): + """ + Test that modifying the event listener list (e.g., via once()) during + event emission does not cause listeners to be skipped. + """ + emitter = EmitterTest() + called = [] + + def cb1(**kwargs): + called.append(1) + + def cb2(**kwargs): + called.append(2) + + def cb3(**kwargs): + called.append(3) + + # Scenario: cb1 is 'once', cb2 is normal. + # When event fires: + # 1. cb1 is called. It calls 'off' which removes itself from the list. + # 2. cb2 should still be called. + + emitter.once("test", cb1) + emitter.on("test", cb2) + emitter.emit("test") + + assert 1 in called + assert 2 in called + + called.clear() + emitter.off("test") + + # Scenario: cb1 is normal, cb2 is once, cb3 is normal. + emitter.on("test", cb1) + emitter.once("test", cb2) + emitter.on("test", cb3) + + emitter.emit("test") + + assert 1 in called + assert 2 in called + assert 3 in called From 8c1b08ecedcbe5b33b3a203e2c8349052516f0e3 Mon Sep 17 00:00:00 2001 From: Julian Kimmig Date: Wed, 24 Dec 2025 06:37:23 +0100 Subject: [PATCH 2/4] chore(config): add Commitizen configuration for conventional commits --- cz.toml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 cz.toml diff --git a/cz.toml b/cz.toml new file mode 100644 index 0000000..e1d8a7b --- /dev/null +++ b/cz.toml @@ -0,0 +1,6 @@ +[tool.commitizen] +name = "cz_conventional_commits" +tag_format = "$version" +version_scheme = "pep440" +version_provider = "uv" +update_changelog_on_bump = true From 565a2d6ff4aa5b45008effdee6d495ceb219619e Mon Sep 17 00:00:00 2001 From: Julian Kimmig Date: Wed, 24 Dec 2025 06:38:10 +0100 Subject: [PATCH 3/4] =?UTF-8?q?bump:=20version=202.3.1=20=E2=86=92=202.3.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 ++++++ pyproject.toml | 2 +- uv.lock | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06ad824..d56108e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## v2.3.2 (2025-12-24) + +### Fix + +- **eventmanager**: prevent skipping listeners during event emission + ## v2.3.1 (2025-12-23) ### Fix diff --git a/pyproject.toml b/pyproject.toml index cb118cf..f991574 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "funcnodes-core" -version = "2.3.1" +version = "2.3.2" description = "core package for funcnodes" authors = [{name = "Julian Kimmig", email = "julian.kimmig@linkdlab.de"}] diff --git a/uv.lock b/uv.lock index 7997790..1014ded 100644 --- a/uv.lock +++ b/uv.lock @@ -457,7 +457,7 @@ wheels = [ [[package]] name = "funcnodes-core" -version = "2.3.1" +version = "2.3.2" source = { editable = "." } dependencies = [ { name = "dill" }, From 1e131c29c399158b57afe9a81ae6fd25373678c4 Mon Sep 17 00:00:00 2001 From: Julian Kimmig Date: Wed, 24 Dec 2025 06:41:13 +0100 Subject: [PATCH 4/4] chore(eventmanager): slice in wring line, detected on git action --- src/funcnodes_core/eventmanager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/funcnodes_core/eventmanager.py b/src/funcnodes_core/eventmanager.py index e3f2328..e459c3d 100644 --- a/src/funcnodes_core/eventmanager.py +++ b/src/funcnodes_core/eventmanager.py @@ -336,8 +336,8 @@ def emit(self, event_name: str, msg: MessageInArgs | None = None) -> bool: raise ValueError("src is a reserved keyword") msg["src"] = self listened = False - if event_name in self._events[:]: - for callback in self._events[event_name]: + if event_name in self._events: + for callback in self._events[event_name][:]: callback(**msg) listened = True if "*" in self._events: