From 03cd23c5106f7ac9d6af8c45b97cc1fd4ccd2b8a Mon Sep 17 00:00:00 2001 From: Shehab Sherif Date: Fri, 20 Mar 2026 12:37:35 +0200 Subject: [PATCH 01/11] Add assertDoesNotAddMessages to CheckerTestCase Add a new context manager method to CheckerTestCase that asserts specific messages are NOT emitted, while allowing other messages to be present. This complements assertNoMessages which asserts that no messages at all are emitted. Also adds a _messages_match static helper for comparing two MessageTest instances field-by-field. Includes tests covering all six scenarios outlined in the issue plus additional edge cases for ignore_position and multiple unwanted messages. Closes #9598 --- doc/whatsnew/fragments/9598.feature | 3 + pylint/testutils/checker_test_case.py | 50 ++++++++++++ tests/testutils/test_checker_test_case.py | 96 +++++++++++++++++++++++ 3 files changed, 149 insertions(+) create mode 100644 doc/whatsnew/fragments/9598.feature create mode 100644 tests/testutils/test_checker_test_case.py diff --git a/doc/whatsnew/fragments/9598.feature b/doc/whatsnew/fragments/9598.feature new file mode 100644 index 0000000000..dfd31b6bd3 --- /dev/null +++ b/doc/whatsnew/fragments/9598.feature @@ -0,0 +1,3 @@ +Add ``assertDoesNotAddMessages`` to ``CheckerTestCase`` to assert that specific messages are not emitted, while allowing other messages to be present. This complements ``assertNoMessages`` which asserts that no messages at all are emitted. + +Closes #9598 diff --git a/pylint/testutils/checker_test_case.py b/pylint/testutils/checker_test_case.py index 951f38c0b9..fd6216d40e 100644 --- a/pylint/testutils/checker_test_case.py +++ b/pylint/testutils/checker_test_case.py @@ -37,6 +37,56 @@ def assertNoMessages(self) -> Iterator[None]: with self.assertAddsMessages(): yield + @contextlib.contextmanager + def assertDoesNotAddMessages( + self, *messages: MessageTest, ignore_position: bool = False + ) -> Generator[None]: + """Assert that the given messages are not added by the given method. + + This is different from ``assertNoMessages`` which asserts that no + messages at all are added. ``assertDoesNotAddMessages`` checks that + none of the *specific* messages passed as arguments are emitted, while + other messages may still be present. + """ + yield + got = self.linter.release_messages() + for unwanted in messages: + for gotten_msg in got: + if not self._messages_match( + unwanted, gotten_msg, ignore_position + ): + continue + got_str = "\n".join(repr(m) for m in got) + msg = ( + "Expected the following message to not be raised:\n" + f"\n {unwanted!r}\n\n" + f"but it was found among the actual messages:\n\n{got_str}\n" + ) + raise AssertionError(msg) + + @staticmethod + def _messages_match( + expected: MessageTest, actual: MessageTest, ignore_position: bool + ) -> bool: + if expected.msg_id != actual.msg_id: + return False + if expected.node != actual.node: + return False + if expected.args != actual.args: + return False + if expected.confidence != actual.confidence: + return False + if not ignore_position: + if expected.line != actual.line: + return False + if expected.col_offset != actual.col_offset: + return False + if expected.end_line != actual.end_line: + return False + if expected.end_col_offset != actual.end_col_offset: + return False + return True + @contextlib.contextmanager def assertAddsMessages( self, *messages: MessageTest, ignore_position: bool = False diff --git a/tests/testutils/test_checker_test_case.py b/tests/testutils/test_checker_test_case.py new file mode 100644 index 0000000000..ae43a392c0 --- /dev/null +++ b/tests/testutils/test_checker_test_case.py @@ -0,0 +1,96 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt + +"""Tests for CheckerTestCase assertion methods.""" + +from __future__ import annotations + +import pytest + +from pylint.checkers.base_checker import BaseChecker +from pylint.interfaces import UNDEFINED +from pylint.testutils import CheckerTestCase, MessageTest + + +class _DummyChecker(BaseChecker): + """A minimal checker used only for testing the test infrastructure.""" + + name = "dummy-for-testcase" + msgs = { + "W9901": ("Dummy message A", "dummy-msg-a", "Dummy message A."), + "W9902": ("Dummy message B", "dummy-msg-b", "Dummy message B."), + } + + +_MSG_A = MessageTest("W9901", line=1, node=None, args=None, confidence=UNDEFINED) +_MSG_B = MessageTest("W9902", line=2, node=None, args=None, confidence=UNDEFINED) + + +class TestCheckerTestCase(CheckerTestCase): + CHECKER_CLASS = _DummyChecker + + # -- assertAddsMessages scenarios (1-3) ----------------------------------- + + def test_assert_adds_messages_success(self) -> None: + """Scenario 1: expected raised / actual raised.""" + with self.assertAddsMessages(_MSG_A): + self.linter.add_message("W9901", line=1) + + def test_assert_adds_messages_failure_not_raised(self) -> None: + """Scenario 2: expected raised / actual not raised.""" + with pytest.raises(AssertionError): + with self.assertAddsMessages(_MSG_A): + pass # nothing emitted + + def test_assert_adds_messages_failure_wrong_message(self) -> None: + """Scenario 3: expected raised / actual not raised but another one raised.""" + with pytest.raises(AssertionError): + with self.assertAddsMessages(_MSG_A): + self.linter.add_message("W9902", line=2) + + # -- assertDoesNotAddMessages scenarios (4-6) ----------------------------- + + def test_assert_does_not_add_messages_failure(self) -> None: + """Scenario 4: expected not raised / actual raised.""" + with pytest.raises(AssertionError): + with self.assertDoesNotAddMessages(_MSG_A): + self.linter.add_message("W9901", line=1) + + def test_assert_does_not_add_messages_success(self) -> None: + """Scenario 5: expected not raised / actual not raised.""" + with self.assertDoesNotAddMessages(_MSG_A): + pass # nothing emitted + + def test_assert_does_not_add_messages_success_other_raised(self) -> None: + """Scenario 6: expected not raised / actual not raised but another one raised.""" + with self.assertDoesNotAddMessages(_MSG_A): + self.linter.add_message("W9902", line=2) + + # -- additional edge cases ------------------------------------------------ + + def test_assert_does_not_add_messages_ignore_position(self) -> None: + """Position mismatch means no match when ignore_position=False.""" + # Same msg_id but different line: should pass (not a match) + msg_different_line = MessageTest( + "W9901", line=99, node=None, args=None, confidence=UNDEFINED + ) + with self.assertDoesNotAddMessages(msg_different_line): + self.linter.add_message("W9901", line=1) + + def test_assert_does_not_add_messages_ignore_position_true(self) -> None: + """With ignore_position=True, position differences are ignored.""" + msg_different_line = MessageTest( + "W9901", line=99, node=None, args=None, confidence=UNDEFINED + ) + with pytest.raises(AssertionError): + with self.assertDoesNotAddMessages( + msg_different_line, ignore_position=True + ): + self.linter.add_message("W9901", line=1) + + def test_assert_does_not_add_messages_multiple_unwanted(self) -> None: + """Fails when any of several unwanted messages is found.""" + with pytest.raises(AssertionError): + with self.assertDoesNotAddMessages(_MSG_A, _MSG_B): + self.linter.add_message("W9902", line=2) From 3299943818646c9a5eaeb7ee92b9d9bf63c61093 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 10:39:32 +0000 Subject: [PATCH 02/11] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- pylint/testutils/checker_test_case.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pylint/testutils/checker_test_case.py b/pylint/testutils/checker_test_case.py index fd6216d40e..3d76a61259 100644 --- a/pylint/testutils/checker_test_case.py +++ b/pylint/testutils/checker_test_case.py @@ -52,9 +52,7 @@ def assertDoesNotAddMessages( got = self.linter.release_messages() for unwanted in messages: for gotten_msg in got: - if not self._messages_match( - unwanted, gotten_msg, ignore_position - ): + if not self._messages_match(unwanted, gotten_msg, ignore_position): continue got_str = "\n".join(repr(m) for m in got) msg = ( From 8a426ba730b05f4932d0d900f2ca3b60e1aa2bce Mon Sep 17 00:00:00 2001 From: Shehab Sherif Date: Fri, 20 Mar 2026 12:48:54 +0200 Subject: [PATCH 03/11] Address Copilot review comments - Use try/except/else instead of bare yield so messages are always drained and never leak into subsequent tests when the with-block raises an exception - Raise TypeError when called with no arguments (vacuous call) - Fix double space in docstring - Reformat news fragment: wrap long line, use Refs instead of Closes --- doc/whatsnew/fragments/9598.feature | 7 ++-- pylint/testutils/checker_test_case.py | 39 +++++++++++++++-------- tests/testutils/test_checker_test_case.py | 18 +++++++++++ 3 files changed, 48 insertions(+), 16 deletions(-) diff --git a/doc/whatsnew/fragments/9598.feature b/doc/whatsnew/fragments/9598.feature index dfd31b6bd3..83b4190672 100644 --- a/doc/whatsnew/fragments/9598.feature +++ b/doc/whatsnew/fragments/9598.feature @@ -1,3 +1,6 @@ -Add ``assertDoesNotAddMessages`` to ``CheckerTestCase`` to assert that specific messages are not emitted, while allowing other messages to be present. This complements ``assertNoMessages`` which asserts that no messages at all are emitted. +Add ``assertDoesNotAddMessages`` to ``CheckerTestCase`` to assert that +specific messages are not emitted, while allowing other messages to be +present. This complements ``assertNoMessages`` which asserts that no +messages at all are emitted. -Closes #9598 +Refs #9598 diff --git a/pylint/testutils/checker_test_case.py b/pylint/testutils/checker_test_case.py index 3d76a61259..5d97f97d3b 100644 --- a/pylint/testutils/checker_test_case.py +++ b/pylint/testutils/checker_test_case.py @@ -44,23 +44,34 @@ def assertDoesNotAddMessages( """Assert that the given messages are not added by the given method. This is different from ``assertNoMessages`` which asserts that no - messages at all are added. ``assertDoesNotAddMessages`` checks that + messages at all are added. ``assertDoesNotAddMessages`` checks that none of the *specific* messages passed as arguments are emitted, while other messages may still be present. """ - yield - got = self.linter.release_messages() - for unwanted in messages: - for gotten_msg in got: - if not self._messages_match(unwanted, gotten_msg, ignore_position): - continue - got_str = "\n".join(repr(m) for m in got) - msg = ( - "Expected the following message to not be raised:\n" - f"\n {unwanted!r}\n\n" - f"but it was found among the actual messages:\n\n{got_str}\n" - ) - raise AssertionError(msg) + if not messages: + raise TypeError( + "assertDoesNotAddMessages requires at least one MessageTest argument" + ) + try: + yield + except Exception: + self.linter.release_messages() + raise + else: + got = self.linter.release_messages() + for unwanted in messages: + for gotten_msg in got: + if not self._messages_match( + unwanted, gotten_msg, ignore_position + ): + continue + got_str = "\n".join(repr(m) for m in got) + msg = ( + "Expected the following message to not be raised:\n" + f"\n {unwanted!r}\n\n" + f"but it was found among the actual messages:\n\n{got_str}\n" + ) + raise AssertionError(msg) @staticmethod def _messages_match( diff --git a/tests/testutils/test_checker_test_case.py b/tests/testutils/test_checker_test_case.py index ae43a392c0..cf1d119918 100644 --- a/tests/testutils/test_checker_test_case.py +++ b/tests/testutils/test_checker_test_case.py @@ -94,3 +94,21 @@ def test_assert_does_not_add_messages_multiple_unwanted(self) -> None: with pytest.raises(AssertionError): with self.assertDoesNotAddMessages(_MSG_A, _MSG_B): self.linter.add_message("W9902", line=2) + + def test_assert_does_not_add_messages_no_args_raises(self) -> None: + """Calling with no arguments must raise TypeError.""" + with pytest.raises(TypeError, match="requires at least one"): + with self.assertDoesNotAddMessages(): + pass + + def test_assert_does_not_add_messages_exception_in_body_drains_messages( + self, + ) -> None: + """An exception in the with-block must not leak messages to later tests.""" + with pytest.raises(RuntimeError): + with self.assertDoesNotAddMessages(_MSG_A): + self.linter.add_message("W9901", line=1) + raise RuntimeError("something went wrong") + # Messages must have been drained; a subsequent assertNoMessages should pass. + with self.assertNoMessages(): + pass From 4c986799426c110e94b3f8b04f1b0a0b98fa68d3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 10:51:00 +0000 Subject: [PATCH 04/11] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- pylint/testutils/checker_test_case.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pylint/testutils/checker_test_case.py b/pylint/testutils/checker_test_case.py index 5d97f97d3b..ee11d4d605 100644 --- a/pylint/testutils/checker_test_case.py +++ b/pylint/testutils/checker_test_case.py @@ -61,9 +61,7 @@ def assertDoesNotAddMessages( got = self.linter.release_messages() for unwanted in messages: for gotten_msg in got: - if not self._messages_match( - unwanted, gotten_msg, ignore_position - ): + if not self._messages_match(unwanted, gotten_msg, ignore_position): continue got_str = "\n".join(repr(m) for m in got) msg = ( From faedc024d274d97a6c6e99676eada1d9e6c0f569 Mon Sep 17 00:00:00 2001 From: Shehab Sherif Date: Fri, 20 Mar 2026 21:31:28 +0200 Subject: [PATCH 05/11] test: cover all _messages_match mismatch branches for 100% patch coverage Add targeted unit tests for each early-return False branch in _messages_match: node, args, confidence, col_offset, end_line, and end_col_offset mismatches. This brings patch coverage from 83.33% to 100%. --- tests/testutils/test_checker_test_case.py | 42 +++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/testutils/test_checker_test_case.py b/tests/testutils/test_checker_test_case.py index cf1d119918..9f38afbf96 100644 --- a/tests/testutils/test_checker_test_case.py +++ b/tests/testutils/test_checker_test_case.py @@ -112,3 +112,45 @@ def test_assert_does_not_add_messages_exception_in_body_drains_messages( # Messages must have been drained; a subsequent assertNoMessages should pass. with self.assertNoMessages(): pass + + # -- _messages_match branch coverage -------------------------------------- + + def test_messages_match_node_mismatch(self) -> None: + expected = MessageTest("W9901", line=1, node="sentinel", args=None) + actual = MessageTest("W9901", line=1, node=None, args=None) + assert not self._messages_match(expected, actual, ignore_position=False) + + def test_messages_match_args_mismatch(self) -> None: + expected = MessageTest("W9901", line=1, node=None, args=("x",)) + actual = MessageTest("W9901", line=1, node=None, args=None) + assert not self._messages_match(expected, actual, ignore_position=False) + + def test_messages_match_confidence_mismatch(self) -> None: + from pylint.interfaces import HIGH + + expected = MessageTest( + "W9901", line=1, node=None, args=None, confidence=HIGH + ) + actual = MessageTest( + "W9901", line=1, node=None, args=None, confidence=UNDEFINED + ) + assert not self._messages_match(expected, actual, ignore_position=False) + + def test_messages_match_col_offset_mismatch(self) -> None: + expected = MessageTest("W9901", line=1, node=None, args=None, col_offset=5) + actual = MessageTest("W9901", line=1, node=None, args=None, col_offset=10) + assert not self._messages_match(expected, actual, ignore_position=False) + + def test_messages_match_end_line_mismatch(self) -> None: + expected = MessageTest("W9901", line=1, node=None, args=None, end_line=5) + actual = MessageTest("W9901", line=1, node=None, args=None, end_line=10) + assert not self._messages_match(expected, actual, ignore_position=False) + + def test_messages_match_end_col_offset_mismatch(self) -> None: + expected = MessageTest( + "W9901", line=1, node=None, args=None, end_col_offset=5 + ) + actual = MessageTest( + "W9901", line=1, node=None, args=None, end_col_offset=10 + ) + assert not self._messages_match(expected, actual, ignore_position=False) From 0f15cc25a6964f184f1af560dbf9b825a4d23b77 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 19:33:47 +0000 Subject: [PATCH 06/11] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/testutils/test_checker_test_case.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/tests/testutils/test_checker_test_case.py b/tests/testutils/test_checker_test_case.py index 9f38afbf96..7a0af38db3 100644 --- a/tests/testutils/test_checker_test_case.py +++ b/tests/testutils/test_checker_test_case.py @@ -128,9 +128,7 @@ def test_messages_match_args_mismatch(self) -> None: def test_messages_match_confidence_mismatch(self) -> None: from pylint.interfaces import HIGH - expected = MessageTest( - "W9901", line=1, node=None, args=None, confidence=HIGH - ) + expected = MessageTest("W9901", line=1, node=None, args=None, confidence=HIGH) actual = MessageTest( "W9901", line=1, node=None, args=None, confidence=UNDEFINED ) @@ -147,10 +145,6 @@ def test_messages_match_end_line_mismatch(self) -> None: assert not self._messages_match(expected, actual, ignore_position=False) def test_messages_match_end_col_offset_mismatch(self) -> None: - expected = MessageTest( - "W9901", line=1, node=None, args=None, end_col_offset=5 - ) - actual = MessageTest( - "W9901", line=1, node=None, args=None, end_col_offset=10 - ) + expected = MessageTest("W9901", line=1, node=None, args=None, end_col_offset=5) + actual = MessageTest("W9901", line=1, node=None, args=None, end_col_offset=10) assert not self._messages_match(expected, actual, ignore_position=False) From 5bbba3f7514ddf4a3686e067f823a15f97a18390 Mon Sep 17 00:00:00 2001 From: Shehab Sherif Date: Thu, 26 Mar 2026 01:44:01 +0200 Subject: [PATCH 07/11] Address maintainer feedback from Pierre-Sassoulas - assertDoesNotAddMessages now accepts message ID strings instead of full MessageTest objects, matching the intended use case described in the issue: checking that a specific message was never triggered regardless of position, args, or node - Remove _messages_match helper and ignore_position parameter as they are no longer needed with the simpler msg_id-only matching - Rename news fragment from 9598.feature to 9598.internal since this change touches the plugin developer API, not an end-user feature - Simplify tests to use plain string IDs --- .../fragments/{9598.feature => 9598.internal} | 0 pylint/testutils/checker_test_case.py | 54 +++---------- tests/testutils/test_checker_test_case.py | 76 +++---------------- 3 files changed, 22 insertions(+), 108 deletions(-) rename doc/whatsnew/fragments/{9598.feature => 9598.internal} (100%) diff --git a/doc/whatsnew/fragments/9598.feature b/doc/whatsnew/fragments/9598.internal similarity index 100% rename from doc/whatsnew/fragments/9598.feature rename to doc/whatsnew/fragments/9598.internal diff --git a/pylint/testutils/checker_test_case.py b/pylint/testutils/checker_test_case.py index ee11d4d605..8fb5717f16 100644 --- a/pylint/testutils/checker_test_case.py +++ b/pylint/testutils/checker_test_case.py @@ -38,19 +38,15 @@ def assertNoMessages(self) -> Iterator[None]: yield @contextlib.contextmanager - def assertDoesNotAddMessages( - self, *messages: MessageTest, ignore_position: bool = False - ) -> Generator[None]: - """Assert that the given messages are not added by the given method. + def assertDoesNotAddMessages(self, *message_ids: str) -> Generator[None]: + """Assert that none of the given message IDs are emitted by the checker. - This is different from ``assertNoMessages`` which asserts that no - messages at all are added. ``assertDoesNotAddMessages`` checks that - none of the *specific* messages passed as arguments are emitted, while - other messages may still be present. + Unlike ``assertNoMessages``, other messages may still be emitted. + Only the specified message IDs are checked to be absent. """ - if not messages: + if not message_ids: raise TypeError( - "assertDoesNotAddMessages requires at least one MessageTest argument" + "assertDoesNotAddMessages requires at least one message ID argument" ) try: yield @@ -59,40 +55,14 @@ def assertDoesNotAddMessages( raise else: got = self.linter.release_messages() - for unwanted in messages: - for gotten_msg in got: - if not self._messages_match(unwanted, gotten_msg, ignore_position): - continue + emitted_ids = {m.msg_id for m in got} + for unwanted_id in message_ids: + if unwanted_id in emitted_ids: got_str = "\n".join(repr(m) for m in got) - msg = ( - "Expected the following message to not be raised:\n" - f"\n {unwanted!r}\n\n" - f"but it was found among the actual messages:\n\n{got_str}\n" + raise AssertionError( + f"Message '{unwanted_id}' was not expected to be emitted" + f" but it was found among the actual messages:\n\n{got_str}\n" ) - raise AssertionError(msg) - - @staticmethod - def _messages_match( - expected: MessageTest, actual: MessageTest, ignore_position: bool - ) -> bool: - if expected.msg_id != actual.msg_id: - return False - if expected.node != actual.node: - return False - if expected.args != actual.args: - return False - if expected.confidence != actual.confidence: - return False - if not ignore_position: - if expected.line != actual.line: - return False - if expected.col_offset != actual.col_offset: - return False - if expected.end_line != actual.end_line: - return False - if expected.end_col_offset != actual.end_col_offset: - return False - return True @contextlib.contextmanager def assertAddsMessages( diff --git a/tests/testutils/test_checker_test_case.py b/tests/testutils/test_checker_test_case.py index 7a0af38db3..487022c7ae 100644 --- a/tests/testutils/test_checker_test_case.py +++ b/tests/testutils/test_checker_test_case.py @@ -24,7 +24,6 @@ class _DummyChecker(BaseChecker): _MSG_A = MessageTest("W9901", line=1, node=None, args=None, confidence=UNDEFINED) -_MSG_B = MessageTest("W9902", line=2, node=None, args=None, confidence=UNDEFINED) class TestCheckerTestCase(CheckerTestCase): @@ -54,97 +53,42 @@ def test_assert_adds_messages_failure_wrong_message(self) -> None: def test_assert_does_not_add_messages_failure(self) -> None: """Scenario 4: expected not raised / actual raised.""" with pytest.raises(AssertionError): - with self.assertDoesNotAddMessages(_MSG_A): + with self.assertDoesNotAddMessages("W9901"): self.linter.add_message("W9901", line=1) def test_assert_does_not_add_messages_success(self) -> None: """Scenario 5: expected not raised / actual not raised.""" - with self.assertDoesNotAddMessages(_MSG_A): + with self.assertDoesNotAddMessages("W9901"): pass # nothing emitted def test_assert_does_not_add_messages_success_other_raised(self) -> None: """Scenario 6: expected not raised / actual not raised but another one raised.""" - with self.assertDoesNotAddMessages(_MSG_A): + with self.assertDoesNotAddMessages("W9901"): self.linter.add_message("W9902", line=2) # -- additional edge cases ------------------------------------------------ - def test_assert_does_not_add_messages_ignore_position(self) -> None: - """Position mismatch means no match when ignore_position=False.""" - # Same msg_id but different line: should pass (not a match) - msg_different_line = MessageTest( - "W9901", line=99, node=None, args=None, confidence=UNDEFINED - ) - with self.assertDoesNotAddMessages(msg_different_line): - self.linter.add_message("W9901", line=1) - - def test_assert_does_not_add_messages_ignore_position_true(self) -> None: - """With ignore_position=True, position differences are ignored.""" - msg_different_line = MessageTest( - "W9901", line=99, node=None, args=None, confidence=UNDEFINED - ) - with pytest.raises(AssertionError): - with self.assertDoesNotAddMessages( - msg_different_line, ignore_position=True - ): - self.linter.add_message("W9901", line=1) - - def test_assert_does_not_add_messages_multiple_unwanted(self) -> None: - """Fails when any of several unwanted messages is found.""" - with pytest.raises(AssertionError): - with self.assertDoesNotAddMessages(_MSG_A, _MSG_B): - self.linter.add_message("W9902", line=2) - def test_assert_does_not_add_messages_no_args_raises(self) -> None: """Calling with no arguments must raise TypeError.""" with pytest.raises(TypeError, match="requires at least one"): with self.assertDoesNotAddMessages(): pass + def test_assert_does_not_add_messages_multiple_unwanted(self) -> None: + """Fails when any of the several unwanted message IDs is found.""" + with pytest.raises(AssertionError): + with self.assertDoesNotAddMessages("W9901", "W9902"): + self.linter.add_message("W9902", line=2) + def test_assert_does_not_add_messages_exception_in_body_drains_messages( self, ) -> None: """An exception in the with-block must not leak messages to later tests.""" with pytest.raises(RuntimeError): - with self.assertDoesNotAddMessages(_MSG_A): + with self.assertDoesNotAddMessages("W9901"): self.linter.add_message("W9901", line=1) raise RuntimeError("something went wrong") # Messages must have been drained; a subsequent assertNoMessages should pass. with self.assertNoMessages(): pass - # -- _messages_match branch coverage -------------------------------------- - - def test_messages_match_node_mismatch(self) -> None: - expected = MessageTest("W9901", line=1, node="sentinel", args=None) - actual = MessageTest("W9901", line=1, node=None, args=None) - assert not self._messages_match(expected, actual, ignore_position=False) - - def test_messages_match_args_mismatch(self) -> None: - expected = MessageTest("W9901", line=1, node=None, args=("x",)) - actual = MessageTest("W9901", line=1, node=None, args=None) - assert not self._messages_match(expected, actual, ignore_position=False) - - def test_messages_match_confidence_mismatch(self) -> None: - from pylint.interfaces import HIGH - - expected = MessageTest("W9901", line=1, node=None, args=None, confidence=HIGH) - actual = MessageTest( - "W9901", line=1, node=None, args=None, confidence=UNDEFINED - ) - assert not self._messages_match(expected, actual, ignore_position=False) - - def test_messages_match_col_offset_mismatch(self) -> None: - expected = MessageTest("W9901", line=1, node=None, args=None, col_offset=5) - actual = MessageTest("W9901", line=1, node=None, args=None, col_offset=10) - assert not self._messages_match(expected, actual, ignore_position=False) - - def test_messages_match_end_line_mismatch(self) -> None: - expected = MessageTest("W9901", line=1, node=None, args=None, end_line=5) - actual = MessageTest("W9901", line=1, node=None, args=None, end_line=10) - assert not self._messages_match(expected, actual, ignore_position=False) - - def test_messages_match_end_col_offset_mismatch(self) -> None: - expected = MessageTest("W9901", line=1, node=None, args=None, end_col_offset=5) - actual = MessageTest("W9901", line=1, node=None, args=None, end_col_offset=10) - assert not self._messages_match(expected, actual, ignore_position=False) From 8a3aaec61e1fe31ecc1e8bfa4c3990ec64b0ccb4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 23:46:03 +0000 Subject: [PATCH 08/11] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/testutils/test_checker_test_case.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/testutils/test_checker_test_case.py b/tests/testutils/test_checker_test_case.py index 487022c7ae..be1f8f863d 100644 --- a/tests/testutils/test_checker_test_case.py +++ b/tests/testutils/test_checker_test_case.py @@ -91,4 +91,3 @@ def test_assert_does_not_add_messages_exception_in_body_drains_messages( # Messages must have been drained; a subsequent assertNoMessages should pass. with self.assertNoMessages(): pass - From 9f088f4447d7b9d0c45328c23b102f69f5588e54 Mon Sep 17 00:00:00 2001 From: Shehab Sherif Date: Thu, 26 Mar 2026 01:59:30 +0200 Subject: [PATCH 09/11] Fix no-else-raise lint warning in assertDoesNotAddMessages Replace try/except/else with try/except/finally to avoid R1720. The exception_raised flag tracks whether the with-block raised, so the finally block skips message checks when propagating an exception from the test body. --- pylint/testutils/checker_test_case.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/pylint/testutils/checker_test_case.py b/pylint/testutils/checker_test_case.py index 8fb5717f16..b068ec8719 100644 --- a/pylint/testutils/checker_test_case.py +++ b/pylint/testutils/checker_test_case.py @@ -48,21 +48,23 @@ def assertDoesNotAddMessages(self, *message_ids: str) -> Generator[None]: raise TypeError( "assertDoesNotAddMessages requires at least one message ID argument" ) + exception_raised = False try: yield except Exception: - self.linter.release_messages() + exception_raised = True raise - else: + finally: got = self.linter.release_messages() - emitted_ids = {m.msg_id for m in got} - for unwanted_id in message_ids: - if unwanted_id in emitted_ids: - got_str = "\n".join(repr(m) for m in got) - raise AssertionError( - f"Message '{unwanted_id}' was not expected to be emitted" - f" but it was found among the actual messages:\n\n{got_str}\n" - ) + if not exception_raised: + emitted_ids = {m.msg_id for m in got} + for unwanted_id in message_ids: + if unwanted_id in emitted_ids: + got_str = "\n".join(repr(m) for m in got) + raise AssertionError( + f"Message '{unwanted_id}' was not expected to be emitted" + f" but it was found among the actual messages:\n\n{got_str}\n" + ) @contextlib.contextmanager def assertAddsMessages( From 12d10c41cefc4bdd9121e83f28734fe48290a619 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Thu, 26 Mar 2026 07:09:52 +0100 Subject: [PATCH 10/11] Less LLM comments, skeleton of raise match assertion Co-authored-by: Pierre Sassoulas --- tests/testutils/test_checker_test_case.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/testutils/test_checker_test_case.py b/tests/testutils/test_checker_test_case.py index be1f8f863d..2da76669ab 100644 --- a/tests/testutils/test_checker_test_case.py +++ b/tests/testutils/test_checker_test_case.py @@ -29,7 +29,6 @@ class _DummyChecker(BaseChecker): class TestCheckerTestCase(CheckerTestCase): CHECKER_CLASS = _DummyChecker - # -- assertAddsMessages scenarios (1-3) ----------------------------------- def test_assert_adds_messages_success(self) -> None: """Scenario 1: expected raised / actual raised.""" @@ -38,7 +37,7 @@ def test_assert_adds_messages_success(self) -> None: def test_assert_adds_messages_failure_not_raised(self) -> None: """Scenario 2: expected raised / actual not raised.""" - with pytest.raises(AssertionError): + with pytest.raises(AssertionError, match="..."): with self.assertAddsMessages(_MSG_A): pass # nothing emitted @@ -48,11 +47,10 @@ def test_assert_adds_messages_failure_wrong_message(self) -> None: with self.assertAddsMessages(_MSG_A): self.linter.add_message("W9902", line=2) - # -- assertDoesNotAddMessages scenarios (4-6) ----------------------------- def test_assert_does_not_add_messages_failure(self) -> None: """Scenario 4: expected not raised / actual raised.""" - with pytest.raises(AssertionError): + with pytest.raises(AssertionError, match="..."): with self.assertDoesNotAddMessages("W9901"): self.linter.add_message("W9901", line=1) @@ -66,7 +64,6 @@ def test_assert_does_not_add_messages_success_other_raised(self) -> None: with self.assertDoesNotAddMessages("W9901"): self.linter.add_message("W9902", line=2) - # -- additional edge cases ------------------------------------------------ def test_assert_does_not_add_messages_no_args_raises(self) -> None: """Calling with no arguments must raise TypeError.""" @@ -76,7 +73,7 @@ def test_assert_does_not_add_messages_no_args_raises(self) -> None: def test_assert_does_not_add_messages_multiple_unwanted(self) -> None: """Fails when any of the several unwanted message IDs is found.""" - with pytest.raises(AssertionError): + with pytest.raises(AssertionError, match="..."): with self.assertDoesNotAddMessages("W9901", "W9902"): self.linter.add_message("W9902", line=2) From 7d083549a6c7475cacf7b2102ff7b975f1faabce Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 06:10:44 +0000 Subject: [PATCH 11/11] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/testutils/test_checker_test_case.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/testutils/test_checker_test_case.py b/tests/testutils/test_checker_test_case.py index 2da76669ab..490e4d35c4 100644 --- a/tests/testutils/test_checker_test_case.py +++ b/tests/testutils/test_checker_test_case.py @@ -29,7 +29,6 @@ class _DummyChecker(BaseChecker): class TestCheckerTestCase(CheckerTestCase): CHECKER_CLASS = _DummyChecker - def test_assert_adds_messages_success(self) -> None: """Scenario 1: expected raised / actual raised.""" with self.assertAddsMessages(_MSG_A): @@ -47,7 +46,6 @@ def test_assert_adds_messages_failure_wrong_message(self) -> None: with self.assertAddsMessages(_MSG_A): self.linter.add_message("W9902", line=2) - def test_assert_does_not_add_messages_failure(self) -> None: """Scenario 4: expected not raised / actual raised.""" with pytest.raises(AssertionError, match="..."): @@ -64,7 +62,6 @@ def test_assert_does_not_add_messages_success_other_raised(self) -> None: with self.assertDoesNotAddMessages("W9901"): self.linter.add_message("W9902", line=2) - def test_assert_does_not_add_messages_no_args_raises(self) -> None: """Calling with no arguments must raise TypeError.""" with pytest.raises(TypeError, match="requires at least one"):