Skip to content

Commit 5e03139

Browse files
committed
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. "<Any: int>", "<Any: (int, str)>") and still fall back safely for non-type values.
1 parent c4d239d commit 5e03139

File tree

2 files changed

+69
-1
lines changed

2 files changed

+69
-1
lines changed

mockito/matchers.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,35 @@ def matches(self, arg):
138138
return True
139139

140140
def __repr__(self):
141-
return "<Any: %r>" % self.wanted_type
141+
return "<Any: %s>" % _any_wanted_type_label(self.wanted_type)
142+
143+
144+
def _any_wanted_type_label(wanted_type):
145+
if isinstance(wanted_type, type):
146+
return _type_label(wanted_type)
147+
148+
if (
149+
isinstance(wanted_type, tuple)
150+
and all(isinstance(t, type) for t in wanted_type)
151+
):
152+
items = [_type_label(t) for t in wanted_type]
153+
if len(items) == 1:
154+
return '(%s,)' % items[0]
155+
return '(%s)' % ', '.join(items)
156+
157+
return _safe_repr(wanted_type)
158+
159+
160+
def _type_label(type_):
161+
module = _safe_getattr(type_, '__module__')
162+
qualname = _safe_getattr(type_, '__qualname__') or _safe_getattr(type_, '__name__')
163+
if qualname is None:
164+
return _safe_repr(type_)
165+
166+
if module is None or module == 'builtins':
167+
return qualname
168+
169+
return '%s.%s' % (module, qualname)
142170

143171

144172
class ValueMatcher(Matcher):
@@ -296,6 +324,16 @@ def _safe_getattr(value, name, default=None):
296324
return default
297325

298326

327+
def _safe_repr(value):
328+
try:
329+
return repr(value)
330+
except Exception:
331+
try:
332+
return object.__repr__(value)
333+
except Exception:
334+
return '<unrepresentable>'
335+
336+
299337
def _label_with_line(label, line_number):
300338
if line_number is None:
301339
return label

tests/matcher_repr_test.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,40 @@ def test_composed_matchers_include_quoted_nested_values():
1616
assert repr(or_(eq("foo"), gt(1))) == "<Or: [<Eq: 'foo'>, <Gt: 1>]>"
1717

1818

19+
def test_any_repr_uses_pretty_names_for_types():
20+
assert repr(any_(int)) == "<Any: int>"
21+
assert repr(any_((int, str))) == "<Any: (int, str)>"
22+
23+
1924
def test_any_repr_quotes_non_type_values():
2025
assert repr(any_("foo")) == "<Any: 'foo'>"
2126

2227

28+
def test_any_repr_handles_types_with_broken_introspection():
29+
class EvilMeta(type):
30+
def __getattribute__(cls, name):
31+
if name in {'__module__', '__qualname__', '__name__'}:
32+
raise RuntimeError('boom')
33+
return super().__getattribute__(name)
34+
35+
class Evil(metaclass=EvilMeta):
36+
pass
37+
38+
matcher_repr = repr(any_(Evil))
39+
assert matcher_repr.startswith("<Any: <class '")
40+
assert "Evil" in matcher_repr
41+
42+
43+
def test_any_repr_handles_values_with_broken_repr():
44+
class BrokenRepr:
45+
def __repr__(self):
46+
raise RuntimeError('boom')
47+
48+
matcher_repr = repr(any_(BrokenRepr()))
49+
assert matcher_repr.startswith('<Any: <')
50+
assert 'BrokenRepr object' in matcher_repr
51+
52+
2353
def test_contains_repr_uses_safe_quoted_substring():
2454
assert repr(contains("a'b")) == "<Contains: \"a'b\">"
2555

0 commit comments

Comments
 (0)