From 0c058bde48dd84af09b3c0d3ff4ff4b646c5071c Mon Sep 17 00:00:00 2001 From: Olli Johnson Date: Fri, 1 Sep 2023 16:31:14 +0100 Subject: [PATCH] Proposal for supporting call-overload errors --- README.md | 5 +++++ src/pytest_mypy_testing/message.py | 19 ++++++++++++++----- src/pytest_mypy_testing/output_processing.py | 11 +++++++++++ tests/test_basics.mypy-testing | 9 +++++++++ 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 9e1e076..c21abbc 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ the file: * `# F: ` - we expect a mypy fatal error message * `# R: ` - we expect a mypy note message `Revealed type is ''`. This is useful to easily check `reveal_type` output: + ```python @pytest.mark.mypy_testing def mypy_use_reveal_type(): @@ -74,6 +75,10 @@ the file: reveal_type(456) # R: Literal[456]? ``` +* `# O: ` - we expect a mypy error message and additionally suppress any + notes on the same line. This is useful to test for errors such as + `call-overload` where mypy provides extra details in notes along with the error. + ## mypy Error Code Matching The algorithm matching messages parses mypy error code both in the diff --git a/src/pytest_mypy_testing/message.py b/src/pytest_mypy_testing/message.py index ff2ecd5..d0a7b8c 100644 --- a/src/pytest_mypy_testing/message.py +++ b/src/pytest_mypy_testing/message.py @@ -41,6 +41,7 @@ def __repr__(self) -> str: "W": Severity.WARNING, "E": Severity.ERROR, "F": Severity.FATAL, + "O": Severity.ERROR, } _COMMENT_MESSAGES = frozenset( @@ -64,6 +65,7 @@ class Message: message: str = "" revealed_type: Optional[str] = None error_code: Optional[str] = None + suppress_notes: bool = False TupleType = Tuple[ str, int, Optional[int], Severity, str, Optional[str], Optional[str] @@ -73,7 +75,7 @@ class Message: COMMENT_RE = re.compile( r"^(?:# *type: *ignore *)?(?:# *)?" - r"(?P[RENW]):" + r"(?P[RENWO]):" r"((?P\d+):)?" r" *" r"(?P[^#]*)" @@ -239,9 +241,11 @@ def from_comment( """Create message object from Python *comment*. >>> Message.from_comment("foo.py", 1, "R: foo") - Message(filename='foo.py', lineno=1, colno=None, severity=Severity.NOTE, message="Revealed type is 'foo'", revealed_type='foo', error_code=None) + Message(filename='foo.py', lineno=1, colno=None, severity=Severity.NOTE, message="Revealed type is 'foo'", revealed_type='foo', error_code=None, suppress_notes=False) >>> Message.from_comment("foo.py", 1, "E: [assignment]") - Message(filename='foo.py', lineno=1, colno=None, severity=Severity.ERROR, message='', revealed_type=None, error_code='assignment') + Message(filename='foo.py', lineno=1, colno=None, severity=Severity.ERROR, message='', revealed_type=None, error_code='assignment', suppress_notes=False) + >>> Message.from_comment("foo.py", 1, "O: [call-overload]") + Message(filename='foo.py', lineno=1, colno=None, severity=Severity.ERROR, message='', revealed_type=None, error_code='call-overload', suppress_notes=True) """ m = cls.COMMENT_RE.match(comment.strip()) if not m: @@ -250,11 +254,15 @@ def from_comment( message, error_code = cls.__split_message_and_error_code( m.group("message_and_error_code") ) + + suppress_notes = False + revealed_type = None if m.group("severity") == "R": revealed_type = message message = "Revealed type is {!r}".format(message) - else: - revealed_type = None + elif m.group("severity") == "O": + suppress_notes = True + return Message( str(filename), lineno=lineno, @@ -263,6 +271,7 @@ def from_comment( message=message, revealed_type=revealed_type, error_code=error_code, + suppress_notes=suppress_notes, ) @classmethod diff --git a/src/pytest_mypy_testing/output_processing.py b/src/pytest_mypy_testing/output_processing.py index ddf9235..656cf98 100644 --- a/src/pytest_mypy_testing/output_processing.py +++ b/src/pytest_mypy_testing/output_processing.py @@ -106,6 +106,17 @@ def _chunk_to_dict(chunk: Sequence[Message]) -> Dict[int, List[Message]]: errors: List[OutputMismatch] = [] + # If an expected message has specified to suppress notes on a line, then + # drop them before performing the comparison. + suppress_notes_lines = { + msg.lineno for msg in expected_messages if msg.suppress_notes + } + actual_messages = [ + msg + for msg in actual_messages + if msg.lineno not in suppress_notes_lines or msg.severity != Severity.NOTE + ] + for a_chunk, b_chunk in iter_msg_seq_diff_chunks( actual_messages, expected_messages ): diff --git a/tests/test_basics.mypy-testing b/tests/test_basics.mypy-testing index 2272972..6b99825 100644 --- a/tests/test_basics.mypy-testing +++ b/tests/test_basics.mypy-testing @@ -3,6 +3,7 @@ # SPDX-License-Identifier: CC0-1.0 import pytest +import dataclasses @pytest.mark.mypy_testing @@ -97,3 +98,11 @@ def mypy_test_xfail_missing_note(): @pytest.mark.xfail def mypy_test_xfail_unexpected_note(): reveal_type([]) # unexpected message + + +@pytest.mark.mypy_testing +def mypy_test_suppress_notes(): + # Check the use of the "O" severity + dataclasses.field(default=123, default_factory=lambda: 456) # O: [call-overload] + # Check that notes on other lines are not suppressed + reveal_type(123) # N: Revealed type is 'Literal[123]?'