diff --git a/src/flake8/formatting/base.py b/src/flake8/formatting/base.py index a17cb44..f12018a 100644 --- a/src/flake8/formatting/base.py +++ b/src/flake8/formatting/base.py @@ -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 diff --git a/src/flake8/formatting/default.py b/src/flake8/formatting/default.py index 7c8073e..f43dc42 100644 --- a/src/flake8/formatting/default.py +++ b/src/flake8/formatting/default.py @@ -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", diff --git a/src/flake8/statistics.py b/src/flake8/statistics.py index ad93c8f..571d50b 100644 --- a/src/flake8/statistics.py +++ b/src/flake8/statistics.py @@ -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, diff --git a/src/flake8/style_guide.py b/src/flake8/style_guide.py index 9d45c79..2f521aa 100644 --- a/src/flake8/style_guide.py +++ b/src/flake8/style_guide.py @@ -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. diff --git a/src/flake8/violation.py b/src/flake8/violation.py new file mode 100644 index 0000000..06983b1 --- /dev/null +++ b/src/flake8/violation.py @@ -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 diff --git a/tests/unit/test_base_formatter.py b/tests/unit/test_base_formatter.py index d096457..7830eb4 100644 --- a/tests/unit/test_base_formatter.py +++ b/tests/unit/test_base_formatter.py @@ -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, diff --git a/tests/unit/test_filenameonly_formatter.py b/tests/unit/test_filenameonly_formatter.py index 165ef69..e92d4bb 100644 --- a/tests/unit/test_filenameonly_formatter.py +++ b/tests/unit/test_filenameonly_formatter.py @@ -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 diff --git a/tests/unit/test_nothing_formatter.py b/tests/unit/test_nothing_formatter.py index c019bdf..eb4b862 100644 --- a/tests/unit/test_nothing_formatter.py +++ b/tests/unit/test_nothing_formatter.py @@ -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 diff --git a/tests/unit/test_statistics.py b/tests/unit/test_statistics.py index 9937916..03f3189 100644 --- a/tests/unit/test_statistics.py +++ b/tests/unit/test_statistics.py @@ -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(): diff --git a/tests/unit/test_violation.py b/tests/unit/test_violation.py index b9cf1a3..6b47691 100644 --- a/tests/unit/test_violation.py +++ b/tests/unit/test_violation.py @@ -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