Skip to content

Commit ab9f6c2

Browse files
author
David Fritzsche
committed
Implement support for flexible matching of mypy error codes
1 parent d3c9347 commit ab9f6c2

File tree

5 files changed

+76
-14
lines changed

5 files changed

+76
-14
lines changed

.gitignore

+9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# SPDX-FileCopyrightText: David Fritzsche
22
# SPDX-License-Identifier: CC0-1.0
3+
4+
!.bumpversion.cfg
5+
!.bumpversion.cfg.license
6+
!.editorconfig
7+
!.flake8
8+
!.gitattributes
9+
!.gitignore
10+
!.isort.cfg
11+
!/.github
312
*.bak
413
*.egg-info
514
*.pyc

README.md

+22
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,24 @@ the file:
7373
reveal_type(456) # R: Literal[456]?
7474
```
7575

76+
## mypy Error Codes
77+
78+
The algorithm matching messages parses mypy error code both in the
79+
output generated by mypy and in the Python comments. If both the mypy
80+
output and the Python comment contain an error code, then the codes
81+
must match. So the following test case expects that mypy writes out an
82+
``assignment`` error code:
83+
84+
``` python
85+
@pytest.mark.mypy_testing
86+
def mypy_test_invalid_assignment() -> None:
87+
foo = "abc"
88+
foo = 123 # E: Incompatible types in assignment (expression has type "int", variable has type "str") [assignment]
89+
```
90+
91+
If the Python comment does not contain an error code, then the error
92+
code written out by mypy (if any) is simply ignored.
93+
7694

7795
## Skipping and Expected Failures
7896

@@ -96,6 +114,10 @@ decorators are extracted from the ast.
96114

97115
# Changelog
98116

117+
## Upcoming
118+
119+
* Implement support for flexible matching of mypy error codes
120+
99121
## v0.0.12
100122

101123
* Allow Windows drives in filename (#17, #34)

mypy_tests/test_mypy_tests_in_test_file.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
# SPDX-FileCopyrightText: David Fritzsche
22
# SPDX-License-Identifier: CC0-1.0
33

4+
# flake8: noqa
5+
# ruff: noqa
6+
47
import pytest
58

69

710
@pytest.mark.mypy_testing
811
def err():
9-
import foo # E: Cannot find implementation or library stub for module named 'foo' # noqa
12+
import foo # E: Cannot find implementation or library stub for module named 'foo'
1013

1114

1215
@pytest.mark.mypy_testing

src/pytest_mypy_testing/message.py

+23-6
Original file line numberDiff line numberDiff line change
@@ -61,24 +61,31 @@ class Message:
6161
severity: Severity
6262
message: str
6363
revealed_type: Optional[str] = None
64+
error_code: Optional[str] = None
6465

65-
TupleType = Tuple[str, int, Optional[int], Severity, str, Optional[str]]
66+
TupleType = Tuple[
67+
str, int, Optional[int], Severity, str, Optional[str], Optional[str]
68+
]
6669

6770
_prefix: str = dataclasses.field(init=False, repr=False, default="")
6871

6972
COMMENT_RE = re.compile(
7073
r"^(?:# *type: *ignore *)?(?:# *)?"
7174
r"(?P<severity>[RENW]):"
7275
r"((?P<colno>\d+):)? *"
73-
r"(?P<message>[^#]*)(?:#.*?)?$"
76+
r"(?P<message>[^#]*?)"
77+
r"(?: +\[(?P<error_code>[^\]]*)\])?"
78+
r"(?:#.*?)?$"
7479
)
7580

7681
OUTPUT_RE = re.compile(
7782
r"^(?P<fname>([a-zA-Z]:)?[^:]+):"
7883
r"(?P<lineno>[0-9]+):"
7984
r"((?P<colno>[0-9]+):)?"
8085
r" *(?P<severity>(error|note|warning)):"
81-
r"(?P<message>.*)$"
86+
r"(?P<message>.*?)"
87+
r"(?: +\[(?P<error_code>[^\]]*)\])?"
88+
r"$"
8289
)
8390

8491
_OUTPUT_REVEALED_RE = re.compile(
@@ -130,12 +137,15 @@ def astuple(self, *, normalized: bool = False) -> "Message.TupleType":
130137
self.severity,
131138
self.normalized_message if normalized else self.message,
132139
self.revealed_type,
140+
self.error_code,
133141
)
134142

135143
def is_comment(self) -> bool:
136144
return (self.severity, self.message) in _COMMENT_MESSAGES
137145

138-
def _as_short_tuple(self, *, normalized: bool = False) -> "Message.TupleType":
146+
def _as_short_tuple(
147+
self, *, normalized: bool = False, default_error_code: Optional[str] = None
148+
) -> "Message.TupleType":
139149
if normalized:
140150
message = self.normalized_message
141151
else:
@@ -147,14 +157,20 @@ def _as_short_tuple(self, *, normalized: bool = False) -> "Message.TupleType":
147157
self.severity,
148158
message,
149159
self.revealed_type,
160+
self.error_code or default_error_code,
150161
)
151162

152163
def __eq__(self, other):
153164
if isinstance(other, Message):
165+
default_error_code = self.error_code or other.error_code
154166
if self.colno is None or other.colno is None:
155-
return self._as_short_tuple(normalized=True) == other._as_short_tuple(
156-
normalized=True
167+
a = self._as_short_tuple(
168+
normalized=True, default_error_code=default_error_code
169+
)
170+
b = other._as_short_tuple(
171+
normalized=True, default_error_code=default_error_code
157172
)
173+
return a == b
158174
else:
159175
return self.astuple(normalized=True) == other.astuple(normalized=True)
160176
else:
@@ -192,6 +208,7 @@ def from_comment(
192208
severity=Severity.from_string(m.group("severity")),
193209
message=message,
194210
revealed_type=revealed_type,
211+
error_code=m.group("error_code") or None,
195212
)
196213

197214
@classmethod

tests/test_message.py

+18-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# SPDX-FileCopyrightText: David Fritzsche
22
# SPDX-License-Identifier: CC0-1.0
33

4+
from typing import Optional
5+
46
import pytest
57

68
from pytest_mypy_testing.message import Message, Severity
@@ -14,26 +16,35 @@ def test_init_severity(string: str, expected: Severity):
1416

1517

1618
@pytest.mark.parametrize(
17-
"filename,comment,severity,message",
19+
"filename,comment,severity,message,error_code",
1820
[
19-
("z.py", "# E: bar", Severity.ERROR, "bar"),
20-
("z.py", "#type:ignore# W: bar", Severity.WARNING, "bar"),
21-
("z.py", "# type: ignore # W: bar", Severity.WARNING, "bar"),
22-
("z.py", "# R: bar", Severity.NOTE, "Revealed type is 'bar'"),
21+
("z.py", "# E: bar", Severity.ERROR, "bar", None),
22+
("z.py", "# E: bar", Severity.ERROR, "bar", "foo"),
23+
("z.py", "# E: bar [foo]", Severity.ERROR, "bar", "foo"),
24+
("z.py", "# E: bar [foo]", Severity.ERROR, "bar", ""),
25+
("z.py", "#type:ignore# W: bar", Severity.WARNING, "bar", None),
26+
("z.py", "# type: ignore # W: bar", Severity.WARNING, "bar", None),
27+
("z.py", "# R: bar", Severity.NOTE, "Revealed type is 'bar'", None),
2328
],
2429
)
2530
def test_message_from_comment(
26-
filename: str, comment: str, severity: Severity, message: str
31+
filename: str,
32+
comment: str,
33+
severity: Severity,
34+
message: str,
35+
error_code: Optional[str],
2736
):
2837
lineno = 123
38+
actual = Message.from_comment(filename, lineno, comment)
2939
expected = Message(
3040
filename=filename,
3141
lineno=lineno,
3242
colno=None,
3343
severity=severity,
3444
message=message,
45+
error_code=error_code,
3546
)
36-
assert Message.from_comment(filename, lineno, comment) == expected
47+
assert actual == expected
3748

3849

3950
def test_message_from_invalid_comment():

0 commit comments

Comments
 (0)