Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/14392.bugfix.rst
Original file line number Diff line number Diff line change
@@ -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.
17 changes: 14 additions & 3 deletions src/_pytest/raises.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
16 changes: 16 additions & 0 deletions testing/python/raises.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"\\\\|")