diff --git a/dotflow/core/execution.py b/dotflow/core/execution.py index 3eadaf00..40d99949 100644 --- a/dotflow/core/execution.py +++ b/dotflow/core/execution.py @@ -1,5 +1,6 @@ """Execution module""" +import re from collections.abc import Callable from datetime import datetime from inspect import getsourcelines @@ -74,8 +75,9 @@ def _execution_orderer( inside_code = getsourcelines(class_instance.__class__)[0] for callable_name in callable_list: + pattern = re.compile(rf"def {re.escape(callable_name)}\s*\(") for index, code in enumerate(inside_code): - if code.find(f"def {callable_name}") != -1: + if pattern.search(code): ordered_list.append((index, callable_name)) ordered_list.sort() diff --git a/tests/core/test_execution.py b/tests/core/test_execution.py index c31ba43c..4b130936 100644 --- a/tests/core/test_execution.py +++ b/tests/core/test_execution.py @@ -13,6 +13,7 @@ from tests.mocks import ( ActionStep, ActionStepExecutionOrderer, + ActionStepPrefixMethods, ActionStepWithContexts, ActionStepWithError, ActionStepWithInitialContext, @@ -292,6 +293,35 @@ def test_execution_orderer(self): expected_value, ) + def test_execution_orderer_prefix_methods(self): + """_execution_orderer must not match 'run' against a 'def run_all' line.""" + controller = Execution( + task=self.task, + workflow_id=self.workflow_id, + previous_context=Context(), + ) + + class_instance = ActionStepPrefixMethods(task=controller.task).storage + callable_list = [ + func + for func in dir(class_instance) + if controller._is_action(class_instance, func) + ] + + result = controller._execution_orderer( + callable_list=callable_list, class_instance=class_instance + ) + result_names = [name for _, name in result] + + # Both methods must appear exactly once + self.assertEqual(result_names.count("run"), 1) + self.assertEqual(result_names.count("run_all"), 1) + + # 'run' must appear at an earlier line than 'run_all' + line_run = next(idx for idx, name in result if name == "run") + line_run_all = next(idx for idx, name in result if name == "run_all") + self.assertLess(line_run, line_run_all) + def test_valid_objects(self): valid_objects = [ "", diff --git a/tests/mocks/__init__.py b/tests/mocks/__init__.py index 3cb764ef..67a7e86c 100644 --- a/tests/mocks/__init__.py +++ b/tests/mocks/__init__.py @@ -5,6 +5,7 @@ from tests.mocks.step_class import ( ActionStep, ActionStepExecutionOrderer, + ActionStepPrefixMethods, ActionStepWithContexts, ActionStepWithError, ActionStepWithInitialContext, @@ -37,6 +38,7 @@ "ActionStepWithoutInit", "SimpleStep", "ActionStepExecutionOrderer", + "ActionStepPrefixMethods", "action_step", "action_step_valid_object", "action_step_with_initial_context", diff --git a/tests/mocks/step_class.py b/tests/mocks/step_class.py index 90cc370e..31af4c56 100644 --- a/tests/mocks/step_class.py +++ b/tests/mocks/step_class.py @@ -89,3 +89,16 @@ def run() -> None: class SimpleStep: def run() -> None: return {"foo": "bar"} + + +@action +class ActionStepPrefixMethods: + """Methods where one name is a prefix of another (e.g. run vs run_all).""" + + @action + def run() -> None: + return {"result": "run"} + + @action + def run_all() -> None: + return {"result": "run_all"}