Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement support for flexible matching of mypy error codes #41

Merged
merged 1 commit into from
Feb 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -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
Expand Down
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
Expand Down
5 changes: 4 additions & 1 deletion mypy_tests/test_mypy_tests_in_test_file.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
29 changes: 23 additions & 6 deletions src/pytest_mypy_testing/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,24 +61,31 @@ 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="")

COMMENT_RE = re.compile(
r"^(?:# *type: *ignore *)?(?:# *)?"
r"(?P<severity>[RENW]):"
r"((?P<colno>\d+):)? *"
r"(?P<message>[^#]*)(?:#.*?)?$"
r"(?P<message>[^#]*?)"
r"(?: +\[(?P<error_code>[^\]]*)\])?"
r"(?:#.*?)?$"
)

OUTPUT_RE = re.compile(
r"^(?P<fname>([a-zA-Z]:)?[^:]+):"
r"(?P<lineno>[0-9]+):"
r"((?P<colno>[0-9]+):)?"
r" *(?P<severity>(error|note|warning)):"
r"(?P<message>.*)$"
r"(?P<message>.*?)"
r"(?: +\[(?P<error_code>[^\]]*)\])?"
r"$"
)

_OUTPUT_REVEALED_RE = re.compile(
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down
25 changes: 18 additions & 7 deletions tests/test_message.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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():
Expand Down