Skip to content

Commit 23a4740

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 5e03139 commit 23a4740

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):
@@ -351,13 +354,13 @@ def matches(self, arg):
351354
return self.sub and len(self.sub) > 0 and arg.find(self.sub) > -1
352355

353356
def __repr__(self):
354-
return "<Contains: %r>" % self.sub
357+
return "<Contains: %s>" % _safe_repr(self.sub)
355358

356359

357360
class Matches(Matcher):
358361
def __init__(self, regex, flags=0):
359362
self.regex = re.compile(regex, flags)
360-
self.flags = flags
363+
self.flags = _explicit_regex_flags(regex, flags)
361364

362365
def matches(self, arg):
363366
if not isinstance(arg, str):
@@ -371,6 +374,23 @@ def __repr__(self):
371374
return "<Matches: %r>" % self.regex.pattern
372375

373376

377+
def _explicit_regex_flags(regex, flags):
378+
if flags:
379+
return flags
380+
381+
compiled_flags = _safe_getattr(regex, 'flags')
382+
pattern = _safe_getattr(regex, 'pattern')
383+
if compiled_flags is None or pattern is None:
384+
return 0
385+
386+
try:
387+
baseline_flags = re.compile(pattern).flags
388+
except Exception:
389+
return compiled_flags
390+
391+
return compiled_flags & ~baseline_flags
392+
393+
374394
class ArgumentCaptor(Matcher, Capturing):
375395
def __init__(self, matcher=None):
376396
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)