Skip to content

Commit 03abbd6

Browse files
Austin Vargaclaude
andcommitted
fix: use regex to match method definitions in execution orderer
Replaces str.find(f"def {name}") with a compiled regex r"def {name}\s*\(" so that a method named e.g. "run" no longer falsely matches a line containing "def run_all(...)". Adds ActionStepPrefixMethods mock and test_execution_orderer_prefix_methods to catch this regression. Closes #134 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 2a42355 commit 03abbd6

4 files changed

Lines changed: 48 additions & 1 deletion

File tree

dotflow/core/execution.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Execution module"""
22

3+
import re
34
from collections.abc import Callable
45
from datetime import datetime
56
from inspect import getsourcelines
@@ -74,8 +75,9 @@ def _execution_orderer(
7475
inside_code = getsourcelines(class_instance.__class__)[0]
7576

7677
for callable_name in callable_list:
78+
pattern = re.compile(rf"def {re.escape(callable_name)}\s*\(")
7779
for index, code in enumerate(inside_code):
78-
if code.find(f"def {callable_name}") != -1:
80+
if pattern.search(code):
7981
ordered_list.append((index, callable_name))
8082

8183
ordered_list.sort()

tests/core/test_execution.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from tests.mocks import (
1414
ActionStep,
1515
ActionStepExecutionOrderer,
16+
ActionStepPrefixMethods,
1617
ActionStepWithContexts,
1718
ActionStepWithError,
1819
ActionStepWithInitialContext,
@@ -292,6 +293,35 @@ def test_execution_orderer(self):
292293
expected_value,
293294
)
294295

296+
def test_execution_orderer_prefix_methods(self):
297+
"""_execution_orderer must not match 'run' against a 'def run_all' line."""
298+
controller = Execution(
299+
task=self.task,
300+
workflow_id=self.workflow_id,
301+
previous_context=Context(),
302+
)
303+
304+
class_instance = ActionStepPrefixMethods(task=controller.task).storage
305+
callable_list = [
306+
func
307+
for func in dir(class_instance)
308+
if controller._is_action(class_instance, func)
309+
]
310+
311+
result = controller._execution_orderer(
312+
callable_list=callable_list, class_instance=class_instance
313+
)
314+
result_names = [name for _, name in result]
315+
316+
# Both methods must appear exactly once
317+
self.assertEqual(result_names.count("run"), 1)
318+
self.assertEqual(result_names.count("run_all"), 1)
319+
320+
# 'run' must appear at an earlier line than 'run_all'
321+
line_run = next(idx for idx, name in result if name == "run")
322+
line_run_all = next(idx for idx, name in result if name == "run_all")
323+
self.assertLess(line_run, line_run_all)
324+
295325
def test_valid_objects(self):
296326
valid_objects = [
297327
"",

tests/mocks/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from tests.mocks.step_class import (
66
ActionStep,
77
ActionStepExecutionOrderer,
8+
ActionStepPrefixMethods,
89
ActionStepWithContexts,
910
ActionStepWithError,
1011
ActionStepWithInitialContext,
@@ -37,6 +38,7 @@
3738
"ActionStepWithoutInit",
3839
"SimpleStep",
3940
"ActionStepExecutionOrderer",
41+
"ActionStepPrefixMethods",
4042
"action_step",
4143
"action_step_valid_object",
4244
"action_step_with_initial_context",

tests/mocks/step_class.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,16 @@ def run() -> None:
8989
class SimpleStep:
9090
def run() -> None:
9191
return {"foo": "bar"}
92+
93+
94+
@action
95+
class ActionStepPrefixMethods:
96+
"""Methods where one name is a prefix of another (e.g. run vs run_all)."""
97+
98+
@action
99+
def run() -> None:
100+
return {"result": "run"}
101+
102+
@action
103+
def run_all() -> None:
104+
return {"result": "run_all"}

0 commit comments

Comments
 (0)