diff --git a/.gitignore b/.gitignore index b90e338..d5c0e71 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,14 @@ # SPDX-FileCopyrightText: David Fritzsche # SPDX-License-Identifier: CC0-1.0 + +!.bumpversion.cfg +!.bumpversion.cfg.license +!.editorconfig +!.flake8 +!.gitattributes +!.gitignore +!.isort.cfg +!/.github *.bak *.egg-info *.pyc diff --git a/README.md b/README.md index 0a0e5d8..c680c7b 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,24 @@ the file: reveal_type(456) # R: Literal[456]? ``` +## mypy Error Codes + +The algorithm matching messages parses mypy error code both in the +output generated by mypy and in the Python comments. If both the mypy +output and the Python comment contain an error code, then the codes +must match. So the following test case expects that mypy writes out an +``assignment`` error code: + +``` python +@pytest.mark.mypy_testing +def mypy_test_invalid_assignment() -> None: + foo = "abc" + foo = 123 # E: Incompatible types in assignment (expression has type "int", variable has type "str") [assignment] +``` + +If the Python comment does not contain an error code, then the error +code written out by mypy (if any) is simply ignored. + ## Skipping and Expected Failures @@ -96,6 +114,10 @@ decorators are extracted from the ast. # Changelog +## Upcoming + +* Implement support for flexible matching of mypy error codes + ## v0.0.12 * Allow Windows drives in filename (#17, #34) diff --git a/mypy_tests/test_mypy_tests_in_test_file.py b/mypy_tests/test_mypy_tests_in_test_file.py index f8028fb..4108338 100644 --- a/mypy_tests/test_mypy_tests_in_test_file.py +++ b/mypy_tests/test_mypy_tests_in_test_file.py @@ -1,12 +1,15 @@ # SPDX-FileCopyrightText: David Fritzsche # SPDX-License-Identifier: CC0-1.0 +# flake8: noqa +# ruff: noqa + import pytest @pytest.mark.mypy_testing def err(): - import foo # E: Cannot find implementation or library stub for module named 'foo' # noqa + import foo # E: Cannot find implementation or library stub for module named 'foo' @pytest.mark.mypy_testing diff --git a/src/pytest_mypy_testing/message.py b/src/pytest_mypy_testing/message.py index c47fc49..62f1587 100644 --- a/src/pytest_mypy_testing/message.py +++ b/src/pytest_mypy_testing/message.py @@ -61,8 +61,11 @@ class Message: severity: Severity message: str revealed_type: Optional[str] = None + error_code: Optional[str] = None - TupleType = Tuple[str, int, Optional[int], Severity, str, Optional[str]] + TupleType = Tuple[ + str, int, Optional[int], Severity, str, Optional[str], Optional[str] + ] _prefix: str = dataclasses.field(init=False, repr=False, default="") @@ -70,7 +73,9 @@ class Message: r"^(?:# *type: *ignore *)?(?:# *)?" r"(?P[RENW]):" r"((?P\d+):)? *" - r"(?P[^#]*)(?:#.*?)?$" + r"(?P[^#]*?)" + r"(?: +\[(?P[^\]]*)\])?" + r"(?:#.*?)?$" ) OUTPUT_RE = re.compile( @@ -78,7 +83,9 @@ class Message: r"(?P[0-9]+):" r"((?P[0-9]+):)?" r" *(?P(error|note|warning)):" - r"(?P.*)$" + r"(?P.*?)" + r"(?: +\[(?P[^\]]*)\])?" + r"$" ) _OUTPUT_REVEALED_RE = re.compile( @@ -130,12 +137,15 @@ def astuple(self, *, normalized: bool = False) -> "Message.TupleType": self.severity, self.normalized_message if normalized else self.message, self.revealed_type, + self.error_code, ) def is_comment(self) -> bool: return (self.severity, self.message) in _COMMENT_MESSAGES - def _as_short_tuple(self, *, normalized: bool = False) -> "Message.TupleType": + def _as_short_tuple( + self, *, normalized: bool = False, default_error_code: Optional[str] = None + ) -> "Message.TupleType": if normalized: message = self.normalized_message else: @@ -147,14 +157,20 @@ def _as_short_tuple(self, *, normalized: bool = False) -> "Message.TupleType": self.severity, message, self.revealed_type, + self.error_code or default_error_code, ) def __eq__(self, other): if isinstance(other, Message): + default_error_code = self.error_code or other.error_code if self.colno is None or other.colno is None: - return self._as_short_tuple(normalized=True) == other._as_short_tuple( - normalized=True + a = self._as_short_tuple( + normalized=True, default_error_code=default_error_code + ) + b = other._as_short_tuple( + normalized=True, default_error_code=default_error_code ) + return a == b else: return self.astuple(normalized=True) == other.astuple(normalized=True) else: @@ -192,6 +208,7 @@ def from_comment( severity=Severity.from_string(m.group("severity")), message=message, revealed_type=revealed_type, + error_code=m.group("error_code") or None, ) @classmethod diff --git a/tests/test_message.py b/tests/test_message.py index ef3ae23..f95dccd 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -1,6 +1,8 @@ # SPDX-FileCopyrightText: David Fritzsche # SPDX-License-Identifier: CC0-1.0 +from typing import Optional + import pytest from pytest_mypy_testing.message import Message, Severity @@ -14,26 +16,35 @@ def test_init_severity(string: str, expected: Severity): @pytest.mark.parametrize( - "filename,comment,severity,message", + "filename,comment,severity,message,error_code", [ - ("z.py", "# E: bar", Severity.ERROR, "bar"), - ("z.py", "#type:ignore# W: bar", Severity.WARNING, "bar"), - ("z.py", "# type: ignore # W: bar", Severity.WARNING, "bar"), - ("z.py", "# R: bar", Severity.NOTE, "Revealed type is 'bar'"), + ("z.py", "# E: bar", Severity.ERROR, "bar", None), + ("z.py", "# E: bar", Severity.ERROR, "bar", "foo"), + ("z.py", "# E: bar [foo]", Severity.ERROR, "bar", "foo"), + ("z.py", "# E: bar [foo]", Severity.ERROR, "bar", ""), + ("z.py", "#type:ignore# W: bar", Severity.WARNING, "bar", None), + ("z.py", "# type: ignore # W: bar", Severity.WARNING, "bar", None), + ("z.py", "# R: bar", Severity.NOTE, "Revealed type is 'bar'", None), ], ) def test_message_from_comment( - filename: str, comment: str, severity: Severity, message: str + filename: str, + comment: str, + severity: Severity, + message: str, + error_code: Optional[str], ): lineno = 123 + actual = Message.from_comment(filename, lineno, comment) expected = Message( filename=filename, lineno=lineno, colno=None, severity=severity, message=message, + error_code=error_code, ) - assert Message.from_comment(filename, lineno, comment) == expected + assert actual == expected def test_message_from_invalid_comment():