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/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 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/src/funcnodes_core/eventmanager.py b/src/funcnodes_core/eventmanager.py index 11880ff..e459c3d 100644 --- a/src/funcnodes_core/eventmanager.py +++ b/src/funcnodes_core/eventmanager.py @@ -337,11 +337,11 @@ def emit(self, event_name: str, msg: MessageInArgs | None = None) -> bool: msg["src"] = self listened = False if event_name in self._events: - for callback in self._events[event_name]: + 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 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" },