break type checking cycles

This commit is contained in:
Anthony Sottile 2022-01-05 13:36:22 -05:00
parent f6267dd4d7
commit fa4c31fb97
10 changed files with 142 additions and 150 deletions

View file

@ -6,13 +6,10 @@ from typing import IO
from typing import List
from typing import Optional
from typing import Tuple
from typing import TYPE_CHECKING
from flake8.formatting import _windows_color
if TYPE_CHECKING:
from flake8.statistics import Statistics
from flake8.style_guide import Violation
from flake8.statistics import Statistics
from flake8.violation import Violation
class BaseFormatter:
@ -98,9 +95,9 @@ class BaseFormatter:
:param error:
This will be an instance of
:class:`~flake8.style_guide.Violation`.
:class:`~flake8.violation.Violation`.
:type error:
flake8.style_guide.Violation
flake8.violation.Violation
"""
line = self.format(error)
source = self.show_source(error)
@ -113,9 +110,9 @@ class BaseFormatter:
:param error:
This will be an instance of
:class:`~flake8.style_guide.Violation`.
:class:`~flake8.violation.Violation`.
:type error:
flake8.style_guide.Violation
flake8.violation.Violation
:returns:
The formatted error string.
:rtype:
@ -163,9 +160,9 @@ class BaseFormatter:
:param error:
This will be an instance of
:class:`~flake8.style_guide.Violation`.
:class:`~flake8.violation.Violation`.
:type error:
flake8.style_guide.Violation
flake8.violation.Violation
:returns:
The formatted error string if the user wants to show the source.
If the user does not want to show the source, this will return

View file

@ -1,12 +1,9 @@
"""Default formatting class for Flake8."""
from typing import Optional
from typing import Set
from typing import TYPE_CHECKING
from flake8.formatting import base
if TYPE_CHECKING:
from flake8.style_guide import Violation
from flake8.violation import Violation
COLORS = {
"bold": "\033[1m",

View file

@ -4,10 +4,8 @@ from typing import Generator
from typing import List
from typing import NamedTuple
from typing import Optional
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from flake8.style_guide import Violation
from flake8.violation import Violation
class Statistics:
@ -34,7 +32,7 @@ class Statistics:
The Violation instance containing the information about the
violation.
:type error:
flake8.style_guide.Violation
flake8.violation.Violation
"""
key = Key.create_from(error)
if key not in self._store:
@ -86,7 +84,7 @@ class Key(NamedTuple):
@classmethod
def create_from(cls, error: "Violation") -> "Key":
"""Create a Key from :class:`flake8.style_guide.Violation`."""
"""Create a Key from :class:`flake8.violation.Violation`."""
return cls(filename=error.filename, code=error.code)
def matches(self, prefix: str, filename: Optional[str]) -> bool:
@ -127,7 +125,7 @@ class Statistic:
@classmethod
def create_from(cls, error: "Violation") -> "Statistic":
"""Create a Statistic from a :class:`flake8.style_guide.Violation`."""
"""Create a Statistic from a :class:`flake8.violation.Violation`."""
return cls(
error_code=error.code,
filename=error.filename,

View file

@ -5,13 +5,10 @@ import copy
import enum
import functools
import itertools
import linecache
import logging
from typing import Dict
from typing import Generator
from typing import List
from typing import Match
from typing import NamedTuple
from typing import Optional
from typing import Sequence
from typing import Set
@ -22,6 +19,7 @@ from flake8 import defaults
from flake8 import statistics
from flake8 import utils
from flake8.formatting import base as base_formatter
from flake8.violation import Violation
__all__ = ("StyleGuide",)
@ -49,98 +47,6 @@ class Decision(enum.Enum):
Selected = "selected error"
@functools.lru_cache(maxsize=512)
def find_noqa(physical_line: str) -> Optional[Match[str]]:
return defaults.NOQA_INLINE_REGEXP.search(physical_line)
class Violation(NamedTuple):
"""Class representing a violation reported by Flake8."""
code: str
filename: str
line_number: int
column_number: int
text: str
physical_line: Optional[str]
def is_inline_ignored(self, disable_noqa: bool) -> bool:
"""Determine if a comment has been added to ignore this line.
:param bool disable_noqa:
Whether or not users have provided ``--disable-noqa``.
:returns:
True if error is ignored in-line, False otherwise.
:rtype:
bool
"""
physical_line = self.physical_line
# TODO(sigmavirus24): Determine how to handle stdin with linecache
if disable_noqa:
return False
if physical_line is None:
physical_line = linecache.getline(self.filename, self.line_number)
noqa_match = find_noqa(physical_line)
if noqa_match is None:
LOG.debug("%r is not inline ignored", self)
return False
codes_str = noqa_match.groupdict()["codes"]
if codes_str is None:
LOG.debug("%r is ignored by a blanket ``# noqa``", self)
return True
codes = set(utils.parse_comma_separated_list(codes_str))
if self.code in codes or self.code.startswith(tuple(codes)):
LOG.debug(
"%r is ignored specifically inline with ``# noqa: %s``",
self,
codes_str,
)
return True
LOG.debug(
"%r is not ignored inline with ``# noqa: %s``", self, codes_str
)
return False
def is_in(self, diff: Dict[str, Set[int]]) -> bool:
"""Determine if the violation is included in a diff's line ranges.
This function relies on the parsed data added via
:meth:`~StyleGuide.add_diff_ranges`. If that has not been called and
we are not evaluating files in a diff, then this will always return
True. If there are diff ranges, then this will return True if the
line number in the error falls inside one of the ranges for the file
(and assuming the file is part of the diff data). If there are diff
ranges, this will return False if the file is not part of the diff
data or the line number of the error is not in any of the ranges of
the diff.
:returns:
True if there is no diff or if the error is in the diff's line
number ranges. False if the error's line number falls outside
the diff's line number ranges.
:rtype:
bool
"""
if not diff:
return True
# NOTE(sigmavirus24): The parsed diff will be a defaultdict with
# a set as the default value (if we have received it from
# flake8.utils.parse_unified_diff). In that case ranges below
# could be an empty set (which is False-y) or if someone else
# is using this API, it could be None. If we could guarantee one
# or the other, we would check for it more explicitly.
line_numbers = diff.get(self.filename)
if not line_numbers:
return False
return self.line_number in line_numbers
class DecisionEngine:
"""A class for managing the decision process around violations.

107
src/flake8/violation.py Normal file
View file

@ -0,0 +1,107 @@
"""Contains the Violation error class used internally."""
import functools
import linecache
import logging
from typing import Dict
from typing import Match
from typing import NamedTuple
from typing import Optional
from typing import Set
from flake8 import defaults
from flake8 import utils
LOG = logging.getLogger(__name__)
@functools.lru_cache(maxsize=512)
def _find_noqa(physical_line: str) -> Optional[Match[str]]:
return defaults.NOQA_INLINE_REGEXP.search(physical_line)
class Violation(NamedTuple):
"""Class representing a violation reported by Flake8."""
code: str
filename: str
line_number: int
column_number: int
text: str
physical_line: Optional[str]
def is_inline_ignored(self, disable_noqa: bool) -> bool:
"""Determine if a comment has been added to ignore this line.
:param bool disable_noqa:
Whether or not users have provided ``--disable-noqa``.
:returns:
True if error is ignored in-line, False otherwise.
:rtype:
bool
"""
physical_line = self.physical_line
# TODO(sigmavirus24): Determine how to handle stdin with linecache
if disable_noqa:
return False
if physical_line is None:
physical_line = linecache.getline(self.filename, self.line_number)
noqa_match = _find_noqa(physical_line)
if noqa_match is None:
LOG.debug("%r is not inline ignored", self)
return False
codes_str = noqa_match.groupdict()["codes"]
if codes_str is None:
LOG.debug("%r is ignored by a blanket ``# noqa``", self)
return True
codes = set(utils.parse_comma_separated_list(codes_str))
if self.code in codes or self.code.startswith(tuple(codes)):
LOG.debug(
"%r is ignored specifically inline with ``# noqa: %s``",
self,
codes_str,
)
return True
LOG.debug(
"%r is not ignored inline with ``# noqa: %s``", self, codes_str
)
return False
def is_in(self, diff: Dict[str, Set[int]]) -> bool:
"""Determine if the violation is included in a diff's line ranges.
This function relies on the parsed data added via
:meth:`~StyleGuide.add_diff_ranges`. If that has not been called and
we are not evaluating files in a diff, then this will always return
True. If there are diff ranges, then this will return True if the
line number in the error falls inside one of the ranges for the file
(and assuming the file is part of the diff data). If there are diff
ranges, this will return False if the file is not part of the diff
data or the line number of the error is not in any of the ranges of
the diff.
:returns:
True if there is no diff or if the error is in the diff's line
number ranges. False if the error's line number falls outside
the diff's line number ranges.
:rtype:
bool
"""
if not diff:
return True
# NOTE(sigmavirus24): The parsed diff will be a defaultdict with
# a set as the default value (if we have received it from
# flake8.utils.parse_unified_diff). In that case ranges below
# could be an empty set (which is False-y) or if someone else
# is using this API, it could be None. If we could guarantee one
# or the other, we would check for it more explicitly.
line_numbers = diff.get(self.filename)
if not line_numbers:
return False
return self.line_number in line_numbers

View file

@ -5,9 +5,9 @@ from unittest import mock
import pytest
from flake8 import style_guide
from flake8.formatting import _windows_color
from flake8.formatting import base
from flake8.violation import Violation
def options(**kwargs):
@ -48,7 +48,7 @@ def test_format_needs_to_be_implemented():
formatter = base.BaseFormatter(options())
with pytest.raises(NotImplementedError):
formatter.format(
style_guide.Violation("A000", "file.py", 1, 1, "error text", None)
Violation("A000", "file.py", 1, 1, "error text", None)
)
@ -57,9 +57,7 @@ def test_show_source_returns_nothing_when_not_showing_source():
formatter = base.BaseFormatter(options(show_source=False))
assert (
formatter.show_source(
style_guide.Violation(
"A000", "file.py", 1, 1, "error text", "line"
)
Violation("A000", "file.py", 1, 1, "error text", "line")
)
== ""
)
@ -70,7 +68,7 @@ def test_show_source_returns_nothing_when_there_is_source():
formatter = base.BaseFormatter(options(show_source=True))
assert (
formatter.show_source(
style_guide.Violation("A000", "file.py", 1, 1, "error text", None)
Violation("A000", "file.py", 1, 1, "error text", None)
)
== ""
)
@ -99,7 +97,7 @@ def test_show_source_returns_nothing_when_there_is_source():
def test_show_source_updates_physical_line_appropriately(line1, line2, column):
"""Ensure the error column is appropriately indicated."""
formatter = base.BaseFormatter(options(show_source=True))
error = style_guide.Violation("A000", "file.py", 1, column, "error", line1)
error = Violation("A000", "file.py", 1, column, "error", line1)
output = formatter.show_source(error)
assert output == line1 + line2
@ -208,7 +206,7 @@ def test_handle_formats_the_error():
"""Verify that a formatter will call format from handle."""
formatter = FormatFormatter(options(show_source=False))
filemock = formatter.output_fd = mock.Mock()
error = style_guide.Violation(
error = Violation(
code="A001",
filename="example.py",
line_number=1,

View file

@ -1,8 +1,8 @@
"""Tests for the FilenameOnly formatter object."""
import argparse
from flake8 import style_guide
from flake8.formatting import default
from flake8.violation import Violation
def options(**kwargs):
@ -18,16 +18,14 @@ def test_caches_filenames_already_printed():
formatter = default.FilenameOnly(options())
assert formatter.filenames_already_printed == set()
formatter.format(
style_guide.Violation("code", "file.py", 1, 1, "text", "l")
)
formatter.format(Violation("code", "file.py", 1, 1, "text", "l"))
assert formatter.filenames_already_printed == {"file.py"}
def test_only_returns_a_string_once_from_format():
"""Verify format ignores the second error with the same filename."""
formatter = default.FilenameOnly(options())
error = style_guide.Violation("code", "file.py", 1, 1, "text", "1")
error = Violation("code", "file.py", 1, 1, "text", "1")
assert formatter.format(error) == "file.py"
assert formatter.format(error) is None
@ -36,6 +34,6 @@ def test_only_returns_a_string_once_from_format():
def test_show_source_returns_nothing():
"""Verify show_source returns nothing."""
formatter = default.FilenameOnly(options())
error = style_guide.Violation("code", "file.py", 1, 1, "text", "1")
error = Violation("code", "file.py", 1, 1, "text", "1")
assert formatter.show_source(error) is None

View file

@ -1,8 +1,8 @@
"""Tests for the Nothing formatter obbject."""
import argparse
from flake8 import style_guide
from flake8.formatting import default
from flake8.violation import Violation
def options(**kwargs):
@ -16,7 +16,7 @@ def options(**kwargs):
def test_format_returns_nothing():
"""Verify Nothing.format returns None."""
formatter = default.Nothing(options())
error = style_guide.Violation("code", "file.py", 1, 1, "text", "1")
error = Violation("code", "file.py", 1, 1, "text", "1")
assert formatter.format(error) is None
@ -24,6 +24,6 @@ def test_format_returns_nothing():
def test_show_source_returns_nothing():
"""Verify Nothing.show_source returns None."""
formatter = default.Nothing(options())
error = style_guide.Violation("code", "file.py", 1, 1, "text", "1")
error = Violation("code", "file.py", 1, 1, "text", "1")
assert formatter.show_source(error) is None

View file

@ -2,7 +2,7 @@
import pytest
from flake8 import statistics as stats
from flake8 import style_guide
from flake8.violation import Violation
DEFAULT_ERROR_CODE = "E100"
DEFAULT_FILENAME = "file.py"
@ -16,7 +16,7 @@ def make_error(**kwargs):
kwargs.setdefault("line_number", 1)
kwargs.setdefault("column_number", 1)
kwargs.setdefault("text", DEFAULT_TEXT)
return style_guide.Violation(**kwargs, physical_line=None)
return Violation(**kwargs, physical_line=None)
def test_key_creation():

View file

@ -1,9 +1,9 @@
"""Tests for the flake8.style_guide.Violation class."""
"""Tests for the flake8.violation.Violation class."""
from unittest import mock
import pytest
from flake8 import style_guide
from flake8.violation import Violation
@pytest.mark.parametrize(
@ -33,9 +33,7 @@ from flake8 import style_guide
)
def test_is_inline_ignored(error_code, physical_line, expected_result):
"""Verify that we detect inline usage of ``# noqa``."""
error = style_guide.Violation(
error_code, "filename.py", 1, 1, "error text", None
)
error = Violation(error_code, "filename.py", 1, 1, "error text", None)
# We want `None` to be passed as the physical line so we actually use our
# monkey-patched linecache.getline value.
@ -45,9 +43,7 @@ def test_is_inline_ignored(error_code, physical_line, expected_result):
def test_disable_is_inline_ignored():
"""Verify that is_inline_ignored exits immediately if disabling NoQA."""
error = style_guide.Violation(
"E121", "filename.py", 1, 1, "error text", "line"
)
error = Violation("E121", "filename.py", 1, 1, "error text", "line")
with mock.patch("linecache.getline") as getline:
assert error.is_inline_ignored(True) is False
@ -67,13 +63,8 @@ def test_disable_is_inline_ignored():
)
def test_violation_is_in_diff(violation_file, violation_line, diff, expected):
"""Verify that we find violations within a diff."""
violation = style_guide.Violation(
"E001",
violation_file,
violation_line,
1,
"warning",
"line",
violation = Violation(
"E001", violation_file, violation_line, 1, "warning", "line"
)
assert violation.is_in(diff) is expected