diff --git a/changelog/14392.bugfix.rst b/changelog/14392.bugfix.rst new file mode 100644 index 00000000000..ef768560467 --- /dev/null +++ b/changelog/14392.bugfix.rst @@ -0,0 +1 @@ +Fixed ``is_fully_escaped`` not handling consecutive backslashes correctly: an escaped backslash before a metacharacter (e.g. ``\\\\.``) was incorrectly treated as escaping the metacharacter itself, causing ``pytest.raises(match=...)`` to skip the regex diff display when it should have shown one. diff --git a/src/_pytest/raises.py b/src/_pytest/raises.py index 82fe2c41c96..4dc55d2c813 100644 --- a/src/_pytest/raises.py +++ b/src/_pytest/raises.py @@ -345,9 +345,20 @@ def _check_raw_type( def is_fully_escaped(s: str) -> bool: # we know we won't compile with re.VERBOSE, so whitespace doesn't need to be escaped metacharacters = "{}()+.*?^$[]|" - return not any( - c in metacharacters and (i == 0 or s[i - 1] != "\\") for (i, c) in enumerate(s) - ) + for i, c in enumerate(s): + if c in metacharacters: + # Count consecutive backslashes preceding this metacharacter. + # An odd number of backslashes means the metacharacter is escaped + # (the last backslash does the escaping); an even number means + # it is not escaped (backslashes escape each other in pairs). + n_backslashes = 0 + j = i - 1 + while j >= 0 and s[j] == "\\": + n_backslashes += 1 + j -= 1 + if n_backslashes % 2 == 0: + return False + return True def unescape(s: str) -> str: diff --git a/testing/python/raises.py b/testing/python/raises.py index f74d747c0df..52336b122a1 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -444,3 +444,19 @@ def test_pipe_is_treated_as_regex_metacharacter(self) -> None: assert not is_fully_escaped("foo|bar") assert is_fully_escaped(r"foo\|bar") assert unescape(r"foo\|bar") == "foo|bar" + + def test_consecutive_backslashes_in_escape_check(self) -> None: + """Consecutive backslashes escape each other, leaving the metachar unescaped.""" + from _pytest.raises import is_fully_escaped + + # r"\." -> one backslash escapes the dot -> fully escaped + assert is_fully_escaped(r"\.") + # r"\\." -> two backslashes: the first escapes the second, dot is unescaped + assert not is_fully_escaped(r"\\.") + # r"\\\." -> three backslashes: pair escapes pair, last escapes dot -> fully escaped + assert is_fully_escaped(r"\\\.") + # Same idea with pipe metachar + # "\\\\|" is the string \\| (2 backslashes + pipe): even count, pipe is unescaped + assert not is_fully_escaped("\\\\|") + # r"\\\\|" is the string \\\\| (4 backslashes + pipe): even count, pipe is unescaped + assert not is_fully_escaped(r"\\\\|")