|
24 | 24 | import inspect |
25 | 25 | import operator |
26 | 26 | from collections import deque |
| 27 | +from functools import cached_property |
27 | 28 | from typing import TYPE_CHECKING |
28 | 29 |
|
29 | 30 | from . import matchers, signature |
@@ -114,12 +115,11 @@ def __call__(self, *params: Any, **named_params: Any) -> Any | None: |
114 | 115 | self._remember_params(params_without_first_arg, named_params) |
115 | 116 | self.mock.remember(self) |
116 | 117 |
|
117 | | - for matching_invocation in self.mock.stubbed_invocations: |
118 | | - if matching_invocation.matches(self): |
119 | | - matching_invocation.should_answer(self) |
120 | | - matching_invocation.capture_arguments(self) |
121 | | - return matching_invocation.answer_first( |
122 | | - *params, **named_params) |
| 118 | + matching_invocation = self._find_best_matching_stubbed_invocation() |
| 119 | + if matching_invocation is not None: |
| 120 | + matching_invocation.should_answer(self) |
| 121 | + matching_invocation.capture_arguments(self) |
| 122 | + return matching_invocation.answer_first(*params, **named_params) |
123 | 123 |
|
124 | 124 | if self.strict: |
125 | 125 | stubbed_invocations = [ |
@@ -148,6 +148,21 @@ def __call__(self, *params: Any, **named_params: Any) -> Any | None: |
148 | 148 |
|
149 | 149 | return None |
150 | 150 |
|
| 151 | + def _find_best_matching_stubbed_invocation(self) -> StubbedInvocation | None: |
| 152 | + candidates = [ |
| 153 | + candidate |
| 154 | + for candidate in self.mock.stubbed_invocations |
| 155 | + if candidate.matches(self) |
| 156 | + ] |
| 157 | + |
| 158 | + if not candidates: |
| 159 | + return None |
| 160 | + |
| 161 | + if len(candidates) == 1: |
| 162 | + return candidates[0] |
| 163 | + |
| 164 | + return max(candidates, key=lambda candidate: candidate.specificity_score) |
| 165 | + |
151 | 166 |
|
152 | 167 | class RememberedPropertyAccess(RememberedInvocation): |
153 | 168 | def ensure_mocked_object_has_method(self, method_name): |
@@ -478,6 +493,33 @@ def __call__(self, *params: Any, **named_params: Any) -> AnswerSelector: |
478 | 493 | self.mock.finish_stubbing(self) |
479 | 494 | return AnswerSelector(self, self.refers_coroutine, self.discard_first_arg) |
480 | 495 |
|
| 496 | + @cached_property |
| 497 | + def specificity_score(self) -> tuple[int, int]: |
| 498 | + quality = 0 |
| 499 | + |
| 500 | + for value in self.params: |
| 501 | + if value is not matchers.ARGS_SENTINEL: |
| 502 | + quality += self._specificity_score(value) |
| 503 | + |
| 504 | + for key, value in self.named_params.items(): |
| 505 | + if key is not matchers.KWARGS_SENTINEL: |
| 506 | + quality += self._specificity_score(value) |
| 507 | + |
| 508 | + coverage = len(self.params) + len(self.named_params) |
| 509 | + return coverage, quality |
| 510 | + |
| 511 | + def _specificity_score(self, value: object) -> int: |
| 512 | + if value is Ellipsis: |
| 513 | + return 0 |
| 514 | + |
| 515 | + if isinstance(value, matchers.Any) and value.wanted_type is None: |
| 516 | + return 0 |
| 517 | + |
| 518 | + if isinstance(value, matchers.Matcher): |
| 519 | + return 1 |
| 520 | + |
| 521 | + return 3 |
| 522 | + |
481 | 523 | def forget_self(self) -> None: |
482 | 524 | if self in self.mock.stubbed_invocations: |
483 | 525 | self.mock.forget_stubbed_invocation(self) |
|
0 commit comments