Skip to content

Commit 187a675

Browse files
authored
Merge pull request #121 from kaste/too-demanding-arg-that
2 parents 182b916 + d736acc commit 187a675

File tree

2 files changed

+51
-11
lines changed

2 files changed

+51
-11
lines changed

mockito/mocking.py

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -399,16 +399,45 @@ def set_continuation(self, continuation: invocation.ConfiguredContinuation) -> N
399399
def _sameish_invocations(
400400
self, same: invocation.StubbedInvocation
401401
) -> list[invocation.StubbedInvocation]:
402-
return [
403-
invoc
404-
for invoc in self.stubbed_invocations
405-
if (
406-
invoc is not same
407-
and invoc.method_name == same.method_name
408-
and invoc.matches(same)
409-
and same.matches(invoc)
410-
)
411-
]
402+
"""Find prior stubs that are *mutually* signature-compatible.
403+
404+
This is used only for continuation bookkeeping (value-vs-chain mode),
405+
not for runtime call dispatch. We intentionally do a symmetric check
406+
(`a.matches(b)` and `b.matches(a)`) to approximate "same signature"
407+
despite one-way matchers like `any()`.
408+
409+
Why this exists: repeated selectors such as
410+
411+
when(cat).meow().purr()
412+
when(cat).meow().roll()
413+
414+
should share the same root continuation for `meow()`.
415+
"""
416+
sameish: list[invocation.StubbedInvocation] = []
417+
for invoc in self.stubbed_invocations:
418+
if invoc is same:
419+
continue
420+
421+
if invoc.method_name != same.method_name:
422+
continue
423+
424+
if self._invocations_are_sameish(invoc, same):
425+
sameish.append(invoc)
426+
427+
return sameish
428+
429+
def _invocations_are_sameish(
430+
self,
431+
left: invocation.StubbedInvocation,
432+
right: invocation.StubbedInvocation,
433+
) -> bool:
434+
# Be conservative in internal equivalence probing: user predicates from
435+
# `arg_that` can throw when evaluated against matcher/sentinel objects.
436+
# In this phase, exceptions should mean "not equivalent", not failure.
437+
try:
438+
return left.matches(right) and right.matches(left)
439+
except Exception:
440+
return False
412441

413442
def get_original_method(self, method_name: str) -> object | None:
414443
return self._original_methods.get(method_name, None)

tests/chaining_test.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import pytest
22

3-
from mockito import expect, mock, verify, unstub, when
3+
from mockito import any as any_
4+
from mockito import arg_that, expect, mock, verify, unstub, when
45
from mockito.invocation import AnswerError, InvocationError
56

67

@@ -469,6 +470,16 @@ def test_chain_matching_requires_candidate_matches_existing_direction():
469470
assert cat.meow(2).purr() == "two"
470471

471472

473+
def test_sameish_matching_does_not_evaluate_arg_that_predicate_on_matchers():
474+
cat = mock()
475+
476+
when(cat).meow(any_()).thenReturn("any")
477+
when(cat).meow(arg_that(lambda value: value > 0)).thenReturn("positive")
478+
479+
assert cat.meow(1) == "positive"
480+
assert cat.meow(-1) == "any"
481+
482+
472483
def test_unexpected_chain_segment_arguments_raise_invocation_error_early():
473484
cat = mock()
474485

0 commit comments

Comments
 (0)