From 8d55ab05b65376aeefe5392dcc253b0a38567d7d Mon Sep 17 00:00:00 2001 From: dariomesic Date: Wed, 29 Oct 2025 15:26:51 +0100 Subject: [PATCH 1/5] Clarify regex match error in pytest.raises --- changelog/13859.improvement.rst | 1 + src/_pytest/raises.py | 6 +-- testing/python/raises.py | 10 ++-- testing/python/raises_group.py | 86 ++++++++++++++++----------------- 4 files changed, 51 insertions(+), 52 deletions(-) create mode 100644 changelog/13859.improvement.rst diff --git a/changelog/13859.improvement.rst b/changelog/13859.improvement.rst new file mode 100644 index 00000000000..56b4615a3e2 --- /dev/null +++ b/changelog/13859.improvement.rst @@ -0,0 +1 @@ +Clarify the error message for `pytest.raises()` when a regex `match` fails. \ No newline at end of file diff --git a/src/_pytest/raises.py b/src/_pytest/raises.py index 2a53a5a148a..ad56a8c14d2 100644 --- a/src/_pytest/raises.py +++ b/src/_pytest/raises.py @@ -519,12 +519,10 @@ def _check_match(self, e: BaseException) -> bool: self._fail_reason = ("\n" if diff[0][0] == "-" else "") + "\n".join(diff) return False - # I don't love "Regex"+"Input" vs something like "expected regex"+"exception message" - # when they're similar it's not always obvious which is which self._fail_reason = ( f"Regex pattern did not match{maybe_specify_type}.\n" - f" Regex: {_match_pattern(self.match)!r}\n" - f" Input: {stringified_exception!r}" + f" Expected regex: {_match_pattern(self.match)!r}\n" + f" Actual message: {stringified_exception!r}" ) if _match_pattern(self.match) == stringified_exception: self._fail_reason += "\n Did you mean to `re.escape()` the regex?" diff --git a/testing/python/raises.py b/testing/python/raises.py index 9e3fe304528..411a45cfa92 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -253,8 +253,8 @@ def test_raises_match(self) -> None: msg = "with base 16" expr = ( "Regex pattern did not match.\n" - f" Regex: {msg!r}\n" - " Input: \"invalid literal for int() with base 10: 'asdf'\"" + f" Expected regex: {msg!r}\n" + f" Actual message: \"invalid literal for int() with base 10: 'asdf'\"" ) with pytest.raises(AssertionError, match="^" + re.escape(expr) + "$"): with pytest.raises(ValueError, match=msg): @@ -289,7 +289,7 @@ def test_match_failure_string_quoting(self): with pytest.raises(AssertionError, match="'foo"): raise AssertionError("'bar") (msg,) = excinfo.value.args - assert msg == '''Regex pattern did not match.\n Regex: "'foo"\n Input: "'bar"''' + assert msg == '''Regex pattern did not match.\n Expected regex: "'foo"\n Actual message: "'bar"''' def test_match_failure_exact_string_message(self): message = "Oh here is a message with (42) numbers in parameters" @@ -299,8 +299,8 @@ def test_match_failure_exact_string_message(self): (msg,) = excinfo.value.args assert msg == ( "Regex pattern did not match.\n" - " Regex: 'Oh here is a message with (42) numbers in parameters'\n" - " Input: 'Oh here is a message with (42) numbers in parameters'\n" + " Expected regex: 'Oh here is a message with (42) numbers in parameters'\n" + " Actual message: 'Oh here is a message with (42) numbers in parameters'\n" " Did you mean to `re.escape()` the regex?" ) diff --git a/testing/python/raises_group.py b/testing/python/raises_group.py index e911ba52cbc..b164a3c2e2b 100644 --- a/testing/python/raises_group.py +++ b/testing/python/raises_group.py @@ -382,8 +382,8 @@ def test_match() -> None: with ( fails_raises_group( "Regex pattern did not match the `ExceptionGroup()`.\n" - " Regex: 'foo'\n" - " Input: 'bar'" + " Expected regex: 'foo'\n" + " Actual message: 'bar'" ), RaisesGroup(ValueError, match="foo"), ): @@ -396,8 +396,8 @@ def test_match() -> None: with ( fails_raises_group( "Regex pattern did not match the `ExceptionGroup()`.\n" - " Regex: 'foo'\n" - " Input: 'bar'\n" + " Expected regex: 'foo'\n" + " Actual message: 'bar'\n" " but matched the expected `ValueError`.\n" " You might want `RaisesGroup(RaisesExc(ValueError, match='foo'))`" ), @@ -570,8 +570,8 @@ def test_assert_message() -> None: " ExceptionGroup('', [RuntimeError()]):\n" " RaisesGroup(ValueError): `RuntimeError()` is not an instance of `ValueError`\n" " RaisesGroup(ValueError, match='a'): Regex pattern did not match the `ExceptionGroup()`.\n" - " Regex: 'a'\n" - " Input: ''\n" + " Expected regex: 'a'\n" + " Actual message: ''\n" " RuntimeError():\n" " RaisesGroup(ValueError): `RuntimeError()` is not an exception group\n" " RaisesGroup(ValueError, match='a'): `RuntimeError()` is not an exception group", @@ -634,8 +634,8 @@ def test_assert_message() -> None: fails_raises_group( # TODO: did not match Exceptiongroup('h(ell)o', ...) ? "Raised exception group did not match: Regex pattern did not match the `ExceptionGroup()`.\n" - " Regex: 'h(ell)o'\n" - " Input: 'h(ell)o'\n" + " Expected regex: 'h(ell)o'\n" + " Actual message: 'h(ell)o'\n" " Did you mean to `re.escape()` the regex?", add_prefix=False, # to see the full structure ), @@ -645,8 +645,8 @@ def test_assert_message() -> None: with ( fails_raises_group( "RaisesExc(match='h(ell)o'): Regex pattern did not match.\n" - " Regex: 'h(ell)o'\n" - " Input: 'h(ell)o'\n" + " Expected regex: 'h(ell)o'\n" + " Actual message: 'h(ell)o'\n" " Did you mean to `re.escape()` the regex?", ), RaisesGroup(RaisesExc(match="h(ell)o")), @@ -799,8 +799,8 @@ def test_suggestion_on_nested_and_brief_error() -> None: "The following raised exceptions did not find a match\n" " ExceptionGroup('^hello', [Exception()]):\n" " RaisesGroup(Exception, match='^hello'): Regex pattern did not match the `ExceptionGroup()`.\n" - " Regex: '^hello'\n" - " Input: '^hello'\n" + " Expected regex: '^hello'\n" + " Actual message: '^hello'\n" " Did you mean to `re.escape()` the regex?\n" " Unexpected nested `ExceptionGroup()`, expected `ValueError`" ), @@ -830,8 +830,8 @@ def test_assert_message_nested() -> None: " RaisesGroup(ValueError): `TypeError()` is not an instance of `ValueError`\n" " RaisesGroup(RaisesGroup(ValueError)): RaisesGroup(ValueError): `TypeError()` is not an exception group\n" " RaisesGroup(RaisesExc(TypeError, match='foo')): RaisesExc(TypeError, match='foo'): Regex pattern did not match.\n" - " Regex: 'foo'\n" - " Input: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'\n" + " Expected regex: 'foo'\n" + " Actual message: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'\n" " RaisesGroup(TypeError, ValueError): 1 matched exception. Too few exceptions raised, found no match for: [ValueError]\n" " ExceptionGroup('Exceptions from Trio nursery', [TypeError('cccccccccccccccccccccccccccccc'), TypeError('dddddddddddddddddddddddddddddd')]):\n" " RaisesGroup(ValueError): \n" @@ -856,12 +856,12 @@ def test_assert_message_nested() -> None: " The following raised exceptions did not find a match\n" " TypeError('cccccccccccccccccccccccccccccc'):\n" " RaisesExc(TypeError, match='foo'): Regex pattern did not match.\n" - " Regex: 'foo'\n" - " Input: 'cccccccccccccccccccccccccccccc'\n" + " Expected regex: 'foo'\n" + " Actual message: 'cccccccccccccccccccccccccccccc'\n" " TypeError('dddddddddddddddddddddddddddddd'):\n" " RaisesExc(TypeError, match='foo'): Regex pattern did not match.\n" - " Regex: 'foo'\n" - " Input: 'dddddddddddddddddddddddddddddd'\n" + " Expected regex: 'foo'\n" + " Actual message: 'dddddddddddddddddddddddddddddd'\n" " RaisesGroup(TypeError, ValueError): \n" " 1 matched exception. \n" " The following expected exceptions did not find a match:\n" @@ -945,8 +945,8 @@ def test_misordering_example() -> None: " It matches `ValueError` which was paired with `ValueError('foo')`\n" " It matches `ValueError` which was paired with `ValueError('foo')`\n" " RaisesExc(ValueError, match='foo'): Regex pattern did not match.\n" - " Regex: 'foo'\n" - " Input: 'bar'\n" + " Expected regex: 'foo'\n" + " Actual message: 'bar'\n" "There exist a possible match when attempting an exhaustive check, but RaisesGroup uses a greedy algorithm. Please make your expected exceptions more stringent with `RaisesExc` etc so the greedy algorithm can function." ), RaisesGroup( @@ -1036,34 +1036,34 @@ def test_identity_oopsies() -> None: "The following raised exceptions did not find a match\n" " ValueError('foo'):\n" " RaisesExc(match='bar'): Regex pattern did not match.\n" - " Regex: 'bar'\n" - " Input: 'foo'\n" + " Expected regex: 'bar'\n" + " Actual message: 'foo'\n" " RaisesExc(match='bar'): Regex pattern did not match.\n" - " Regex: 'bar'\n" - " Input: 'foo'\n" + " Expected regex: 'bar'\n" + " Actual message: 'foo'\n" " RaisesExc(match='bar'): Regex pattern did not match.\n" - " Regex: 'bar'\n" - " Input: 'foo'\n" + " Expected regex: 'bar'\n" + " Actual message: 'foo'\n" " ValueError('foo'):\n" " RaisesExc(match='bar'): Regex pattern did not match.\n" - " Regex: 'bar'\n" - " Input: 'foo'\n" + " Expected regex: 'bar'\n" + " Actual message: 'foo'\n" " RaisesExc(match='bar'): Regex pattern did not match.\n" - " Regex: 'bar'\n" - " Input: 'foo'\n" + " Expected regex: 'bar'\n" + " Actual message: 'foo'\n" " RaisesExc(match='bar'): Regex pattern did not match.\n" - " Regex: 'bar'\n" - " Input: 'foo'\n" + " Expected regex: 'bar'\n" + " Actual message: 'foo'\n" " ValueError('foo'):\n" " RaisesExc(match='bar'): Regex pattern did not match.\n" - " Regex: 'bar'\n" - " Input: 'foo'\n" + " Expected regex: 'bar'\n" + " Actual message: 'foo'\n" " RaisesExc(match='bar'): Regex pattern did not match.\n" - " Regex: 'bar'\n" - " Input: 'foo'\n" + " Expected regex: 'bar'\n" + " Actual message: 'foo'\n" " RaisesExc(match='bar'): Regex pattern did not match.\n" - " Regex: 'bar'\n" - " Input: 'foo'" + " Expected regex: 'bar'\n" + " Actual message: 'foo'" ), RaisesGroup(m, m, m), ): @@ -1120,7 +1120,7 @@ def test_raisesexc() -> None: # currently RaisesGroup says "Raised exception did not match" but RaisesExc doesn't... with pytest.raises( AssertionError, - match=wrap_escape("Regex pattern did not match.\n Regex: 'foo'\n Input: 'bar'"), + match=wrap_escape("Regex pattern did not match.\n Expected regex: 'foo'\n Actual message: 'bar'"), ): with RaisesExc(TypeError, match="foo"): raise TypeError("bar") @@ -1132,8 +1132,8 @@ def test_raisesexc_match() -> None: with ( fails_raises_group( "RaisesExc(ValueError, match='foo'): Regex pattern did not match.\n" - " Regex: 'foo'\n" - " Input: 'bar'" + " Expected regex: 'foo'\n" + " Actual message: 'bar'" ), RaisesGroup(RaisesExc(ValueError, match="foo")), ): @@ -1145,8 +1145,8 @@ def test_raisesexc_match() -> None: with ( fails_raises_group( "RaisesExc(match='foo'): Regex pattern did not match.\n" - " Regex: 'foo'\n" - " Input: 'bar'" + " Expected regex: 'foo'\n" + " Actual message: 'bar'" ), RaisesGroup(RaisesExc(match="foo")), ): From 3355ada96dacdef9cda6989b12b0e0cfe071c806 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 14:32:13 +0000 Subject: [PATCH 2/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- changelog/13859.improvement.rst | 2 +- testing/python/raises.py | 5 ++++- testing/python/raises_group.py | 4 +++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/changelog/13859.improvement.rst b/changelog/13859.improvement.rst index 56b4615a3e2..b785381fd68 100644 --- a/changelog/13859.improvement.rst +++ b/changelog/13859.improvement.rst @@ -1 +1 @@ -Clarify the error message for `pytest.raises()` when a regex `match` fails. \ No newline at end of file +Clarify the error message for `pytest.raises()` when a regex `match` fails. diff --git a/testing/python/raises.py b/testing/python/raises.py index 411a45cfa92..0bf02a8063f 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -289,7 +289,10 @@ def test_match_failure_string_quoting(self): with pytest.raises(AssertionError, match="'foo"): raise AssertionError("'bar") (msg,) = excinfo.value.args - assert msg == '''Regex pattern did not match.\n Expected regex: "'foo"\n Actual message: "'bar"''' + assert ( + msg + == '''Regex pattern did not match.\n Expected regex: "'foo"\n Actual message: "'bar"''' + ) def test_match_failure_exact_string_message(self): message = "Oh here is a message with (42) numbers in parameters" diff --git a/testing/python/raises_group.py b/testing/python/raises_group.py index b164a3c2e2b..0247b118c58 100644 --- a/testing/python/raises_group.py +++ b/testing/python/raises_group.py @@ -1120,7 +1120,9 @@ def test_raisesexc() -> None: # currently RaisesGroup says "Raised exception did not match" but RaisesExc doesn't... with pytest.raises( AssertionError, - match=wrap_escape("Regex pattern did not match.\n Expected regex: 'foo'\n Actual message: 'bar'"), + match=wrap_escape( + "Regex pattern did not match.\n Expected regex: 'foo'\n Actual message: 'bar'" + ), ): with RaisesExc(TypeError, match="foo"): raise TypeError("bar") From 00c9cc998d97d9a86ddfb675ae7575ac66bbaf57 Mon Sep 17 00:00:00 2001 From: dariomesic Date: Wed, 29 Oct 2025 15:45:38 +0100 Subject: [PATCH 3/5] CI: Clarify pytest.raises() regex mismatch error message --- changelog/13859.improvement.rst | 2 +- src/_pytest/_code/code.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/changelog/13859.improvement.rst b/changelog/13859.improvement.rst index 56b4615a3e2..53296df6e72 100644 --- a/changelog/13859.improvement.rst +++ b/changelog/13859.improvement.rst @@ -1 +1 @@ -Clarify the error message for `pytest.raises()` when a regex `match` fails. \ No newline at end of file +Clarify the error message for `pytest.raises()` when a regex `match` fails. diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 06036d21956..add2a493ca7 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -772,7 +772,11 @@ def match(self, regexp: str | re.Pattern[str]) -> Literal[True]: """ __tracebackhide__ = True value = stringify_exception(self.value) - msg = f"Regex pattern did not match.\n Regex: {regexp!r}\n Input: {value!r}" + msg = ( + f"Regex pattern did not match.\n" + f" Expected regex: {regexp!r}\n" + f" Actual message: {value!r}" + ) if regexp == value: msg += "\n Did you mean to `re.escape()` the regex?" assert re.search(regexp, value), msg From f96d20952704f2c69fb81275411c58a94b0a4d47 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 14:48:01 +0000 Subject: [PATCH 4/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- changelog/13859.improvement.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/changelog/13859.improvement.rst b/changelog/13859.improvement.rst index 42fd3cc3047..b785381fd68 100644 --- a/changelog/13859.improvement.rst +++ b/changelog/13859.improvement.rst @@ -1,2 +1 @@ Clarify the error message for `pytest.raises()` when a regex `match` fails. - From 3375cb63fff77ea0008c13d81dfc34066de0139d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 14:48:01 +0000 Subject: [PATCH 5/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- changelog/13859.improvement.rst | 1 - testing/code/test_excinfo.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/changelog/13859.improvement.rst b/changelog/13859.improvement.rst index 42fd3cc3047..b785381fd68 100644 --- a/changelog/13859.improvement.rst +++ b/changelog/13859.improvement.rst @@ -1,2 +1 @@ Clarify the error message for `pytest.raises()` when a regex `match` fails. - diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 636469ee6f1..78aba40cd17 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -443,8 +443,8 @@ def test_division_zero(): match = [ r"E .* AssertionError: Regex pattern did not match.", - r"E .* Regex: '\[123\]\+'", - r"E .* Input: 'division by zero'", + r"E Expected regex: '\[123\]\+'", + r"E Actual message: 'division by zero'", ] result.stdout.re_match_lines(match) result.stdout.no_fnmatch_line("*__tracebackhide__ = True*")