Skip to content

Commit 27646a4

Browse files
akshayliveclaude
andauthored
fix(mocks): pass invocation as a tuple to avoid arg name collisions (#1585)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 9ef2936 commit 27646a4

8 files changed

Lines changed: 127 additions & 9 deletions

File tree

packages/uipath/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath"
3-
version = "2.10.52"
3+
version = "2.10.53"
44
description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools."
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"

packages/uipath/src/uipath/eval/mocks/_llm_mocker.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,11 +96,16 @@ def __init__(self, context: MockingContext):
9696

9797
@traced(name="__mocker__", recording=False)
9898
async def response(
99-
self, func: Callable[[T], R], params: dict[str, Any], *args: T, **kwargs
99+
self,
100+
func: Callable[[T], R],
101+
params: dict[str, Any],
102+
invocation: tuple[tuple[Any, ...], dict[str, Any]],
100103
) -> R:
101104
"""Respond with mocked response generated by an LLM."""
102105
assert isinstance(self.context.strategy, LLMMockingStrategy)
103106

107+
args, kwargs = invocation
108+
104109
function_name = params.get("name") or func.__name__
105110
if function_name in [x.name for x in self.context.strategy.tools_to_simulate]:
106111
uipath = UiPath()

packages/uipath/src/uipath/eval/mocks/_mock_context.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,13 @@ def is_tool_simulated(tool_name: str) -> bool:
6464

6565

6666
async def get_mocked_response(
67-
func: Callable[[Any], Any], params: dict[str, Any], *args, **kwargs
67+
func: Callable[[Any], Any],
68+
params: dict[str, Any],
69+
invocation: tuple[tuple[Any, ...], dict[str, Any]],
6870
) -> Any:
6971
"""Get a mocked response."""
7072
mocker = mocker_context.get()
7173
if mocker is None:
7274
raise UiPathNoMockFoundError()
7375
else:
74-
return await mocker.response(func, params, *args, **kwargs)
76+
return await mocker.response(func, params, invocation)

packages/uipath/src/uipath/eval/mocks/_mocker.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@ async def response(
1616
self,
1717
func: Callable[[T], R],
1818
params: dict[str, Any],
19-
*args: T,
20-
**kwargs,
19+
invocation: tuple[tuple[Any, ...], dict[str, Any]],
2120
) -> R:
2221
"""Respond with mocked response."""
2322
raise NotImplementedError()

packages/uipath/src/uipath/eval/mocks/_mockito_mocker.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,17 @@ def __init__(self, context: MockingContext):
9999
stubbed = stubbed.thenRaise(_resolve_value(answer_dict["value"]))
100100

101101
async def response(
102-
self, func: Callable[[T], R], params: dict[str, Any], *args: T, **kwargs
102+
self,
103+
func: Callable[[T], R],
104+
params: dict[str, Any],
105+
invocation: tuple[tuple[Any, ...], dict[str, Any]],
103106
) -> R:
104107
"""Return mocked response or raise appropriate errors."""
105108
if not isinstance(self.context.strategy, MockitoMockingStrategy):
106109
raise UiPathMockResponseGenerationError("Mocking strategy misconfigured.")
107110

111+
args, kwargs = invocation
112+
108113
# No behavior configured → call real function
109114
is_mocked = any(
110115
behavior.function == params["name"]

packages/uipath/src/uipath/eval/mocks/mockable.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def mocked_response_decorator(func, params: dict[str, Any]):
3939
"""Mocked response decorator."""
4040

4141
async def mock_response_generator(*args, **kwargs):
42-
mocked_response = await get_mocked_response(func, params, *args, **kwargs)
42+
mocked_response = await get_mocked_response(func, params, (args, kwargs))
4343

4444
# Mocking successful.
4545
context = UiPathSpanUtils.get_parent_context()
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
"""Regression tests: @mockable must not collide with user args named `func`/`params`."""
2+
3+
from typing import Any
4+
from unittest.mock import MagicMock, patch
5+
6+
import pytest
7+
8+
from uipath.eval.mocks import mockable
9+
from uipath.eval.mocks._mock_runtime import (
10+
clear_execution_context,
11+
set_execution_context,
12+
)
13+
from uipath.eval.mocks._types import MockingContext
14+
from uipath.eval.models.evaluation_set import EvaluationItem
15+
16+
_mock_span_collector = MagicMock()
17+
18+
19+
def _build_evaluation(
20+
function_name: str, kwargs: dict[str, Any], value: Any
21+
) -> EvaluationItem:
22+
evaluation_item: dict[str, Any] = {
23+
"id": "evaluation-id",
24+
"name": "Test evaluation",
25+
"inputs": {},
26+
"evaluationCriterias": {"ExactMatchEvaluator": None},
27+
"mockingStrategy": {
28+
"type": "mockito",
29+
"behaviors": [
30+
{
31+
"function": function_name,
32+
"arguments": {"args": [], "kwargs": kwargs},
33+
"then": [{"type": "return", "value": value}],
34+
}
35+
],
36+
},
37+
}
38+
return EvaluationItem(**evaluation_item)
39+
40+
41+
class TestMockableArgCollision:
42+
"""Ensure `@mockable` works when the wrapped function has args named `func` or `params`."""
43+
44+
def test_sync_function_with_func_and_params_args(self):
45+
"""A sync mockable function that takes `func` and `params` kwargs should not raise."""
46+
47+
@mockable()
48+
def test_function(func: str, params: dict[str, Any]) -> str:
49+
raise NotImplementedError()
50+
51+
evaluation = _build_evaluation(
52+
"test_function",
53+
kwargs={"func": "some_func", "params": {"k": "v"}},
54+
value="mocked_result",
55+
)
56+
57+
set_execution_context(
58+
MockingContext(
59+
strategy=evaluation.mocking_strategy,
60+
name=evaluation.name,
61+
inputs=evaluation.inputs,
62+
),
63+
_mock_span_collector,
64+
"test-execution-id",
65+
)
66+
67+
try:
68+
with patch("uipath.eval.mocks.mockable.UiPathSpanUtils"):
69+
with patch("uipath.eval.mocks.mockable.trace"):
70+
result = test_function(func="some_func", params={"k": "v"})
71+
72+
assert result == "mocked_result"
73+
finally:
74+
clear_execution_context()
75+
76+
@pytest.mark.asyncio
77+
async def test_async_function_with_func_and_params_args(self):
78+
"""An async mockable function that takes `func` and `params` kwargs should not raise."""
79+
80+
@mockable()
81+
async def test_function(func: str, params: dict[str, Any]) -> str:
82+
raise NotImplementedError()
83+
84+
evaluation = _build_evaluation(
85+
"test_function",
86+
kwargs={"func": "some_func", "params": {"k": "v"}},
87+
value="mocked_result",
88+
)
89+
90+
set_execution_context(
91+
MockingContext(
92+
strategy=evaluation.mocking_strategy,
93+
name=evaluation.name,
94+
inputs=evaluation.inputs,
95+
),
96+
_mock_span_collector,
97+
"test-execution-id",
98+
)
99+
100+
try:
101+
with patch("uipath.eval.mocks.mockable.UiPathSpanUtils"):
102+
with patch("uipath.eval.mocks.mockable.trace"):
103+
result = await test_function(func="some_func", params={"k": "v"})
104+
105+
assert result == "mocked_result"
106+
finally:
107+
clear_execution_context()

packages/uipath/uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)