From 0850f866dcdd1abc18a0588345e7fc12e3ed25df Mon Sep 17 00:00:00 2001 From: Sebastian Tramp Date: Sat, 5 Sep 2026 16:39:05 +0200 Subject: [PATCH] fix two Validate execution phase bugs and two false claims The action crashed with an AttributeError when the execution code assigned result = None, although the documentation calls that the way to hand nothing to the next task, and it passed the constructor's data dict rather than the one the fresh initialization run builds, so data mutations accumulated across clicks. Both are covered by tests which fail without the fix. The dependency caveat claimed a failed installation lets execution continue and fail later at the import. It does not: the deployment answers with 400, cmem-client raises, and the run stops before the execution code. The caveat now says so, and the changelog records the breaking side of the cmempy removal - task code calling cmem.cmempy.* used to inherit process wide credentials from setup_cmempy_user_access and now has to authenticate itself. The README quick start called task plugin:install, which no longer exists since the template update flattened that include. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Q2PdymJgUZnA8KZKMEiUvm --- CHANGELOG.md | 12 ++++++++++++ README.md | 2 +- cmem_plugin_python/workflow_task.py | 15 ++++++++++----- tests/test_workflow_task.py | 30 +++++++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc5c9a0..91db238 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,11 +22,23 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - fixed the stale link to the context object documentation - the plugin talks to the deployment through `cmem-client` only - the deprecated `cmem-cmempy` dependency and all `setup_cmempy_user_access` calls are gone + - **breaking:** `setup_cmempy_user_access` also set `OAUTH_GRANT_TYPE` and + `OAUTH_ACCESS_TOKEN` in the environment of the whole process, which task code + calling `cmem.cmempy.*` could rely on without setting up access itself. That side + effect is gone, so such code now has to authenticate on its own - the documented + way is `get_client(context)` ### Fixed - the **Install missing dependencies** action reports the installed version of an already installed package instead of repeating its name +- the **Validate execution phase** action no longer fails with an `AttributeError` + when the execution code assigns `result = None`, which is the documented way of + handing nothing to the next task +- the **Validate execution phase** action starts from the `data` its initialization + code builds, instead of carrying the mutations of earlier action runs +- the documented behavior of a failed dependency installation: it stops the task + before the execution code runs, rather than letting the import fail later ## [1.2.1] 2025-09-18 diff --git a/README.md b/README.md index 05ab9ee..899b382 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Do not use in production! ``` eval $(cmemc -c my-cmem config eval) -task clean check plugin:install +task clean check install ``` 😈 diff --git a/cmem_plugin_python/workflow_task.py b/cmem_plugin_python/workflow_task.py index caa9e38..a9049ba 100644 --- a/cmem_plugin_python/workflow_task.py +++ b/cmem_plugin_python/workflow_task.py @@ -249,8 +249,11 @@ Dependencies are matched by package name only. A package which is already installed is left at the version which is there, and a version specifier is not understood and leads to a fresh installation attempt on every run. -A failed installation does not stop the task: execution continues and the code fails later, at -the import. + +An installation which fails stops the task before the execution code runs at all, because the +deployment rejects the request and the error is not caught. +A misspelled dependency therefore fails the workflow run rather than the import, and the +**Install missing dependencies** action reports the error instead of its per package output. """ @@ -376,9 +379,11 @@ def validate_execute_action(self) -> str: """Run the execute code and report results.""" init_scope = self.do_init() test_inputs = init_scope.get("test_inputs", []) - scope = self.do_execute(test_inputs, None, self.data) - if "result" in scope: - result: Entities = scope["result"] + # the data of the fresh init scope, not self.data: the constructor's dict is mutated by + # every run of the execution code, so re-using it accumulates across action calls + scope = self.do_execute(test_inputs, None, init_scope.get("data", {})) + result: Entities | None = scope.get("result") + if result is not None: output = "# Schema\n" output += "``` python\n" output += f"{result.schema!r}\n" diff --git a/tests/test_workflow_task.py b/tests/test_workflow_task.py index bbea2e3..4b0b3e9 100644 --- a/tests/test_workflow_task.py +++ b/tests/test_workflow_task.py @@ -173,6 +173,36 @@ def test_validate_execute_action() -> None: ) +def test_validate_execute_action_with_none_result() -> None: + """Test Validate execute action with an explicit result of None""" + assert ( + "No result provided" + in PythonCodeWorkflowPlugin( + init_code=PythonCode(""), + execute_code=PythonCode("result = None"), + ).validate_execute_action() + ) + + +def test_validate_execute_action_resets_data() -> None: + """Test that the action starts from the data of a fresh initialization run""" + counting_code = PythonCode( + """from cmem_plugin_base.dataintegration import entity +data["count"] += 1 +result = entity.Entities( + entities=[entity.Entity(uri="urn:x-example:run", values=[[str(data["count"])]])], + schema=entity.EntitySchema( + type_uri="urn:x-example:count", paths=[entity.EntityPath("count")] + ), +)""" + ) + plugin = PythonCodeWorkflowPlugin( + init_code=PythonCode('data["count"] = 0'), execute_code=counting_code + ) + for _ in range(3): + assert "['1']" in plugin.validate_execute_action() + + def test_validate_execute_action_fail() -> None: """Test Validate execute action fails""" with pytest.raises(SyntaxError, match=r"'\[' was never closed"):