Skip to content

Commit fe73613

Browse files
author
rodrigo.nogueira
committed
fix: Ensure **kwargs only contains unmatched arguments during signature binding by filtering out named parameters.
1 parent 277271d commit fe73613

3 files changed

Lines changed: 39 additions & 1 deletion

File tree

docs/authors.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
* [Rafael Rêgo](mailto:crafards@gmail.com)
1111
* [Raphael Schrader](mailto:raphael@schradercloud.de)
1212
* [João S. O. Bueno](mailto:gwidion@gmail.com)
13+
* [Rodrigo Nogueira](mailto:rodrigo.b.nogueira@gmail.com)
1314

1415

1516
## Scaffolding

statemachine/signature.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,8 @@ def _fast_bind(
9999
arguments[name] = kwargs.get(name)
100100

101101
if kwargs_param_name is not None:
102-
arguments[kwargs_param_name] = kwargs
102+
matched = set(param_names)
103+
arguments[kwargs_param_name] = {k: v for k, v in kwargs.items() if k not in matched}
103104

104105
return BoundArguments(self, arguments) # type: ignore[arg-type]
105106

tests/test_signature.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import pytest
55

66
from statemachine.dispatcher import callable_method
7+
from statemachine.signature import SignatureAdapter
78

89

910
def single_positional_param(a):
@@ -162,3 +163,38 @@ def test_support_for_partial(self):
162163

163164
assert wrapped_func("A", "B") == ("A", "B", "activated")
164165
assert wrapped_func.__name__ == positional_and_kw_arguments.__name__
166+
167+
168+
def named_and_kwargs(source, **kwargs):
169+
return source, kwargs
170+
171+
172+
class TestCachedBindExpected:
173+
"""Tests that exercise the cache fast-path by calling the same
174+
wrapped function twice with the same argument shape."""
175+
176+
def setup_method(self):
177+
SignatureAdapter.from_callable.clear_cache()
178+
179+
def test_named_param_not_leaked_into_kwargs(self):
180+
"""Named params should not appear in the **kwargs dict on cache hit."""
181+
wrapped = callable_method(named_and_kwargs)
182+
183+
# 1st call: cache miss -> _full_bind
184+
result1 = wrapped(source="A", target="B", event="go")
185+
assert result1 == ("A", {"target": "B", "event": "go"})
186+
187+
# 2nd call: cache hit -> _fast_bind
188+
result2 = wrapped(source="X", target="Y", event="stop")
189+
assert result2 == ("X", {"target": "Y", "event": "stop"})
190+
191+
def test_kwargs_only_receives_unmatched_keys_with_positional(self):
192+
"""When mixing positional and keyword args with **kwargs."""
193+
wrapped = callable_method(named_and_kwargs)
194+
195+
result1 = wrapped("A", target="B")
196+
assert result1 == ("A", {"target": "B"})
197+
198+
result2 = wrapped("X", target="Y")
199+
assert result2 == ("X", {"target": "Y"})
200+

0 commit comments

Comments
 (0)