Skip to content

Commit d005566

Browse files
committed
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.
1 parent d8ed049 commit d005566

File tree

2 files changed

+51
-3
lines changed

2 files changed

+51
-3
lines changed

mockito/matchers.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,10 @@ def __init__(self, value):
174174
self.value = value
175175

176176
def __repr__(self):
177-
return "<%s: %r>" % (self.__class__.__name__, self.value)
177+
return "<%s: %s>" % (
178+
self.__class__.__name__,
179+
_safe_repr(self.value),
180+
)
178181

179182

180183
class Eq(ValueMatcher):
@@ -355,13 +358,13 @@ def matches(self, arg):
355358
return self.sub and len(self.sub) > 0 and arg.find(self.sub) > -1
356359

357360
def __repr__(self):
358-
return "<Contains: %r>" % self.sub
361+
return "<Contains: %s>" % _safe_repr(self.sub)
359362

360363

361364
class Matches(Matcher):
362365
def __init__(self, regex, flags=0):
363366
self.regex = re.compile(regex, flags)
364-
self.flags = flags
367+
self.flags = _explicit_regex_flags(regex, flags)
365368

366369
def matches(self, arg):
367370
if not isinstance(arg, str):
@@ -375,6 +378,23 @@ def __repr__(self):
375378
return "<Matches: %r>" % self.regex.pattern
376379

377380

381+
def _explicit_regex_flags(regex, flags):
382+
if flags:
383+
return flags
384+
385+
compiled_flags = _safe_getattr(regex, 'flags')
386+
pattern = _safe_getattr(regex, 'pattern')
387+
if compiled_flags is None or pattern is None:
388+
return 0
389+
390+
try:
391+
baseline_flags = re.compile(pattern).flags
392+
except Exception:
393+
return compiled_flags
394+
395+
return compiled_flags & ~baseline_flags
396+
397+
378398
class ArgumentCaptor(Matcher, Capturing):
379399
def __init__(self, matcher=None):
380400
self.matcher = matcher or Any()

tests/matcher_repr_test.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,26 @@ def __repr__(self):
5050
assert 'BrokenRepr object' in matcher_repr
5151

5252

53+
def test_value_matcher_repr_handles_values_with_broken_repr():
54+
class BrokenRepr:
55+
def __repr__(self):
56+
raise RuntimeError('boom')
57+
58+
matcher_repr = repr(eq(BrokenRepr()))
59+
assert matcher_repr.startswith('<Eq: <')
60+
assert 'BrokenRepr object' in matcher_repr
61+
62+
63+
def test_contains_repr_handles_values_with_broken_repr():
64+
class BrokenRepr:
65+
def __repr__(self):
66+
raise RuntimeError('boom')
67+
68+
matcher_repr = repr(contains(BrokenRepr()))
69+
assert matcher_repr.startswith('<Contains: <')
70+
assert 'BrokenRepr object' in matcher_repr
71+
72+
5373
def test_contains_repr_uses_safe_quoted_substring():
5474
assert repr(contains("a'b")) == "<Contains: \"a'b\">"
5575

@@ -61,6 +81,14 @@ def test_matches_repr_shows_only_explicit_flags():
6181
)
6282

6383

84+
def test_matches_repr_shows_flags_for_compiled_patterns():
85+
compiled = re.compile('f..', re.IGNORECASE)
86+
87+
assert repr(matches(compiled)) == (
88+
f"<Matches: 'f..' flags={int(re.IGNORECASE)}>"
89+
)
90+
91+
6492
def test_arg_that_repr_includes_named_function_name():
6593
# Predicate display name: "def is_positive"
6694
def is_positive(value):

0 commit comments

Comments
 (0)