From e02b97cc8ae4926d72110bcb24fb1d4b31811bfa Mon Sep 17 00:00:00 2001 From: herr kaste Date: Wed, 11 Mar 2026 17:54:52 +0100 Subject: [PATCH 1/4] Improve matcher repr output for clearer diagnostics Use repr-style formatting for value-like matchers so string values are quoted consistently in diagnostics and nested matcher output. Also make Matches report only explicitly requested regex flags and render patterns via repr. Add matcher_repr_test.py with coverage for the updated repr behavior. --- mockito/matchers.py | 14 +++++++------- tests/matcher_repr_test.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 7 deletions(-) create mode 100644 tests/matcher_repr_test.py diff --git a/mockito/matchers.py b/mockito/matchers.py index 1eb951c..8d5ac22 100644 --- a/mockito/matchers.py +++ b/mockito/matchers.py @@ -137,7 +137,7 @@ def matches(self, arg): return True def __repr__(self): - return "" % self.wanted_type + return "" % self.wanted_type class ValueMatcher(Matcher): @@ -145,7 +145,7 @@ def __init__(self, value): self.value = value def __repr__(self): - return "<%s: %s>" % (self.__class__.__name__, self.value) + return "<%s: %r>" % (self.__class__.__name__, self.value) class Eq(ValueMatcher): @@ -236,12 +236,13 @@ def matches(self, arg): return self.sub and len(self.sub) > 0 and arg.find(self.sub) > -1 def __repr__(self): - return "" % self.sub + return "" % self.sub class Matches(Matcher): def __init__(self, regex, flags=0): self.regex = re.compile(regex, flags) + self.flags = flags def matches(self, arg): if not isinstance(arg, str): @@ -249,11 +250,10 @@ def matches(self, arg): return self.regex.match(arg) is not None def __repr__(self): - if self.regex.flags: - return "" % (self.regex.pattern, - self.regex.flags) + if self.flags: + return "" % (self.regex.pattern, self.flags) else: - return "" % self.regex.pattern + return "" % self.regex.pattern class ArgumentCaptor(Matcher, Capturing): diff --git a/tests/matcher_repr_test.py b/tests/matcher_repr_test.py new file mode 100644 index 0000000..abe3cec --- /dev/null +++ b/tests/matcher_repr_test.py @@ -0,0 +1,28 @@ +import re + +from mockito import and_, any as any_, contains, eq, gt, matches, not_, or_ + + +def test_value_matchers_use_repr_for_string_values(): + assert repr(eq("foo")) == "" + + +def test_composed_matchers_include_quoted_nested_values(): + assert repr(not_(eq("foo"))) == ">" + assert repr(and_(eq("foo"), gt(1))) == ", ]>" + assert repr(or_(eq("foo"), gt(1))) == ", ]>" + + +def test_any_repr_quotes_non_type_values(): + assert repr(any_("foo")) == "" + + +def test_contains_repr_uses_safe_quoted_substring(): + assert repr(contains("a'b")) == "" + + +def test_matches_repr_shows_only_explicit_flags(): + assert repr(matches("f..")) == "" + assert repr(matches("f..", re.IGNORECASE)) == ( + f"" + ) From c4d239da670330cea390704067278e7d31853662 Mon Sep 17 00:00:00 2001 From: herr kaste Date: Wed, 11 Mar 2026 18:01:10 +0100 Subject: [PATCH 2/4] Improve ArgThat repr with resilient predicate labeling Make ArgThat repr informative by labeling predicate kind and optional source line, e.g. "def is_positive at line N", "lambda at line N", and callable instance labels. This improves diagnostics without requiring custom ArgThat subclasses. Add defensive introspection fallbacks so odd/broken callables do not break repr generation. Handle functools.partial explicitly, and add regression tests covering builtins, numpy ufuncs, partial numpy functions, and broken __name__ introspection. --- mockito/matchers.py | 79 ++++++++++++++++++++++++++++++++++- tests/matcher_repr_test.py | 85 +++++++++++++++++++++++++++++++++++++- 2 files changed, 162 insertions(+), 2 deletions(-) diff --git a/mockito/matchers.py b/mockito/matchers.py index 8d5ac22..8ada28f 100644 --- a/mockito/matchers.py +++ b/mockito/matchers.py @@ -60,6 +60,7 @@ """ from abc import ABC, abstractmethod +import functools import re builtin_any = any @@ -223,7 +224,83 @@ def matches(self, arg): return self.predicate(arg) def __repr__(self): - return "" + return "" % _arg_that_predicate_label(self.predicate) + + +def _arg_that_predicate_label(predicate): + try: + return _arg_that_predicate_label_unchecked(predicate) + except Exception: + predicate_class = _safe_getattr( + _safe_getattr(predicate, '__class__'), + '__name__', + ) + if predicate_class is None: + return 'callable' + + return 'callable %s' % predicate_class + + +def _arg_that_predicate_label_unchecked(predicate): + if isinstance(predicate, functools.partial): + return _arg_that_partial_label(predicate) + + function_line = _line_of_callable(predicate) + function_name = _safe_getattr(predicate, '__name__') + if function_name is not None: + if function_name == '': + return _label_with_line('lambda', function_line) + return _label_with_line('def %s' % function_name, function_line) + + predicate_class = _safe_getattr( + _safe_getattr(predicate, '__class__'), + '__name__', + ) + if predicate_class is None: + predicate_class = 'object' + + call = _safe_getattr(predicate, '__call__') + call_line = _line_of_callable(call) + return _label_with_line( + 'callable %s.__call__' % predicate_class, + call_line, + ) + + +def _arg_that_partial_label(predicate): + partial_func = _safe_getattr(predicate, 'func') + partial_name = _safe_getattr(partial_func, '__name__') + + if partial_name is not None: + return 'partial %s' % partial_name + + return 'partial' + + +def _line_of_callable(value): + if value is None: + return None + + func = _safe_getattr(value, '__func__', value) + code = _safe_getattr(func, '__code__') + if code is None: + return None + + return _safe_getattr(code, 'co_firstlineno') + + +def _safe_getattr(value, name, default=None): + try: + return getattr(value, name) + except Exception: + return default + + +def _label_with_line(label, line_number): + if line_number is None: + return label + + return '%s at line %s' % (label, line_number) class Contains(Matcher): diff --git a/tests/matcher_repr_test.py b/tests/matcher_repr_test.py index abe3cec..4fa3a7c 100644 --- a/tests/matcher_repr_test.py +++ b/tests/matcher_repr_test.py @@ -1,6 +1,9 @@ +from functools import partial import re -from mockito import and_, any as any_, contains, eq, gt, matches, not_, or_ +import numpy as np + +from mockito import and_, any as any_, arg_that, contains, eq, gt, matches, not_, or_ def test_value_matchers_use_repr_for_string_values(): @@ -26,3 +29,83 @@ def test_matches_repr_shows_only_explicit_flags(): assert repr(matches("f..", re.IGNORECASE)) == ( f"" ) + + +def test_arg_that_repr_includes_named_function_name(): + # Predicate display name: "def is_positive" + def is_positive(value): + return value > 0 + + matcher = arg_that(is_positive) + + assert repr(matcher) == ( + f"" + ) + + +def test_arg_that_repr_includes_lambda_name(): + # Predicate display name: "lambda" + predicate = lambda value: value > 0 + matcher = arg_that(predicate) + + assert repr(matcher) == ( + f"" + ) + + +def test_arg_that_repr_for_callable_instance_includes_class_name(): + # Predicate display name: "callable IsPositive.__call__" + class IsPositive: + def __call__(self, value): + return value > 0 + + predicate = IsPositive() + matcher = arg_that(predicate) + + assert repr(matcher) == ( + "" + ) + + +def test_arg_that_repr_for_builtin_callable_has_no_line_number(): + matcher = arg_that(len) + + assert repr(matcher) == "" + + +def test_arg_that_repr_for_partial_uses_underlying_function_name(): + predicate = partial(pow, exp=2) + matcher = arg_that(predicate) + + assert repr(matcher) == "" + + +def test_arg_that_repr_for_numpy_ufunc_uses_function_name_without_line(): + matcher = arg_that(np.isfinite) + + assert repr(matcher) == "" + + +def test_arg_that_repr_for_partial_numpy_function_uses_wrapped_name(): + predicate = partial(np.allclose, b=0.0) + matcher = arg_that(predicate) + + assert repr(matcher) == "" + + +def test_arg_that_repr_handles_callables_with_broken_name_introspection(): + class BrokenNameCallable: + def __getattribute__(self, name): + if name == '__name__': + raise RuntimeError("boom") + return super().__getattribute__(name) + + def __call__(self, value): + return value > 0 + + matcher = arg_that(BrokenNameCallable()) + + matcher_repr = repr(matcher) + assert matcher_repr.startswith(" Date: Wed, 11 Mar 2026 18:13:13 +0100 Subject: [PATCH 3/4] Harden Any repr formatting and add edge-case coverage Improve Any repr readability for type constraints while preserving robustness for unusual objects. Type constraints now render as concise names (e.g. "", "") and still fall back safely for non-type values. --- mockito/matchers.py | 40 +++++++++++++++++++++++++++++++++++++- tests/matcher_repr_test.py | 30 ++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/mockito/matchers.py b/mockito/matchers.py index 8ada28f..2980c13 100644 --- a/mockito/matchers.py +++ b/mockito/matchers.py @@ -138,7 +138,35 @@ def matches(self, arg): return True def __repr__(self): - return "" % self.wanted_type + return "" % _any_wanted_type_label(self.wanted_type) + + +def _any_wanted_type_label(wanted_type): + if isinstance(wanted_type, type): + return _type_label(wanted_type) + + if ( + isinstance(wanted_type, tuple) + and all(isinstance(t, type) for t in wanted_type) + ): + items = [_type_label(t) for t in wanted_type] + if len(items) == 1: + return '(%s,)' % items[0] + return '(%s)' % ', '.join(items) + + return _safe_repr(wanted_type) + + +def _type_label(type_): + module = _safe_getattr(type_, '__module__') + qualname = _safe_getattr(type_, '__qualname__') or _safe_getattr(type_, '__name__') + if qualname is None: + return _safe_repr(type_) + + if module is None or module == 'builtins': + return qualname + + return '%s.%s' % (module, qualname) class ValueMatcher(Matcher): @@ -296,6 +324,16 @@ def _safe_getattr(value, name, default=None): return default +def _safe_repr(value): + try: + return repr(value) + except Exception: + try: + return object.__repr__(value) + except Exception: + return '' + + def _label_with_line(label, line_number): if line_number is None: return label diff --git a/tests/matcher_repr_test.py b/tests/matcher_repr_test.py index 4fa3a7c..488e620 100644 --- a/tests/matcher_repr_test.py +++ b/tests/matcher_repr_test.py @@ -16,10 +16,40 @@ def test_composed_matchers_include_quoted_nested_values(): assert repr(or_(eq("foo"), gt(1))) == ", ]>" +def test_any_repr_uses_pretty_names_for_types(): + assert repr(any_(int)) == "" + assert repr(any_((int, str))) == "" + + def test_any_repr_quotes_non_type_values(): assert repr(any_("foo")) == "" +def test_any_repr_handles_types_with_broken_introspection(): + class EvilMeta(type): + def __getattribute__(cls, name): + if name in {'__module__', '__qualname__', '__name__'}: + raise RuntimeError('boom') + return super().__getattribute__(name) + + class Evil(metaclass=EvilMeta): + pass + + matcher_repr = repr(any_(Evil)) + assert matcher_repr.startswith("" From 23a47402dc1c32d8dc1cb9c242222062a50ca2b5 Mon Sep 17 00:00:00 2001 From: herr kaste Date: Wed, 11 Mar 2026 18:58:30 +0100 Subject: [PATCH 4/4] Harden matcher repr rendering Use safe repr handling in ValueMatcher and Contains so diagnostic rendering cannot fail when user objects implement a broken __repr__. Also preserve explicit flags in Matches.__repr__ when a compiled pattern is passed, while still omitting default regex engine flags. --- mockito/matchers.py | 26 +++++++++++++++++++++++--- tests/matcher_repr_test.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/mockito/matchers.py b/mockito/matchers.py index 2980c13..6cfe184 100644 --- a/mockito/matchers.py +++ b/mockito/matchers.py @@ -174,7 +174,10 @@ def __init__(self, value): self.value = value def __repr__(self): - return "<%s: %r>" % (self.__class__.__name__, self.value) + return "<%s: %s>" % ( + self.__class__.__name__, + _safe_repr(self.value), + ) class Eq(ValueMatcher): @@ -351,13 +354,13 @@ def matches(self, arg): return self.sub and len(self.sub) > 0 and arg.find(self.sub) > -1 def __repr__(self): - return "" % self.sub + return "" % _safe_repr(self.sub) class Matches(Matcher): def __init__(self, regex, flags=0): self.regex = re.compile(regex, flags) - self.flags = flags + self.flags = _explicit_regex_flags(regex, flags) def matches(self, arg): if not isinstance(arg, str): @@ -371,6 +374,23 @@ def __repr__(self): return "" % self.regex.pattern +def _explicit_regex_flags(regex, flags): + if flags: + return flags + + compiled_flags = _safe_getattr(regex, 'flags') + pattern = _safe_getattr(regex, 'pattern') + if compiled_flags is None or pattern is None: + return 0 + + try: + baseline_flags = re.compile(pattern).flags + except Exception: + return compiled_flags + + return compiled_flags & ~baseline_flags + + class ArgumentCaptor(Matcher, Capturing): def __init__(self, matcher=None): self.matcher = matcher or Any() diff --git a/tests/matcher_repr_test.py b/tests/matcher_repr_test.py index 488e620..e14c207 100644 --- a/tests/matcher_repr_test.py +++ b/tests/matcher_repr_test.py @@ -50,6 +50,26 @@ def __repr__(self): assert 'BrokenRepr object' in matcher_repr +def test_value_matcher_repr_handles_values_with_broken_repr(): + class BrokenRepr: + def __repr__(self): + raise RuntimeError('boom') + + matcher_repr = repr(eq(BrokenRepr())) + assert matcher_repr.startswith('" @@ -61,6 +81,14 @@ def test_matches_repr_shows_only_explicit_flags(): ) +def test_matches_repr_shows_flags_for_compiled_patterns(): + compiled = re.compile('f..', re.IGNORECASE) + + assert repr(matches(compiled)) == ( + f"" + ) + + def test_arg_that_repr_includes_named_function_name(): # Predicate display name: "def is_positive" def is_positive(value):