Add support for per-file ignores in config

This adds support for rules that ignore violation codes on a per-file
basis. This takes a similar functional approach to
https://github.com/snoack/flake8-per-file-ignores which allows for glob
patterns like the `--exclude` option.

Closes #156
This commit is contained in:
Ian Stapleton Cordasco 2018-10-25 09:10:06 -05:00
parent 52d88d8ca7
commit f2776107db
No known key found for this signature in database
GPG key ID: C9D7A2604B4FCB2A
6 changed files with 174 additions and 6 deletions

View file

@ -15,7 +15,7 @@ import sys
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
LOG.addHandler(logging.NullHandler()) LOG.addHandler(logging.NullHandler())
__version__ = "3.6.0" __version__ = "3.7.0dev0"
__version_info__ = tuple( __version_info__ = tuple(
int(i) for i in __version__.split(".") if i.isdigit() int(i) for i in __version__.split(".") if i.isdigit()
) )

View file

@ -65,8 +65,8 @@ class Application(object):
self.formatter = None self.formatter = None
#: The :class:`flake8.plugins.notifier.Notifier` for listening plugins #: The :class:`flake8.plugins.notifier.Notifier` for listening plugins
self.listener_trie = None self.listener_trie = None
#: The :class:`flake8.style_guide.StyleGuide` built from the user's #: The :class:`flake8.style_guide.StyleGuideManager` built from the
#: options #: user's options
self.guide = None self.guide = None
#: The :class:`flake8.checker.Manager` that will handle running all of #: The :class:`flake8.checker.Manager` that will handle running all of
#: the checks selected by the user. #: the checks selected by the user.
@ -283,7 +283,7 @@ class Application(object):
# type: () -> NoneType # type: () -> NoneType
"""Initialize our StyleGuide.""" """Initialize our StyleGuide."""
if self.guide is None: if self.guide is None:
self.guide = style_guide.StyleGuide( self.guide = style_guide.StyleGuideManager(
self.options, self.listener_trie, self.formatter self.options, self.listener_trie, self.formatter
) )

View file

@ -1,5 +1,6 @@
"""Contains the logic for all of the default options for Flake8.""" """Contains the logic for all of the default options for Flake8."""
from flake8 import defaults from flake8 import defaults
from flake8 import utils
from flake8.main import debug from flake8.main import debug
from flake8.main import vcs from flake8.main import vcs
@ -19,6 +20,7 @@ def register_default_options(option_manager):
- ``--hang-closing`` - ``--hang-closing``
- ``--ignore`` - ``--ignore``
- ``--extend-ignore`` - ``--extend-ignore``
- ``--per-file-ignores``
- ``--max-line-length`` - ``--max-line-length``
- ``--select`` - ``--select``
- ``--disable-noqa`` - ``--disable-noqa``
@ -142,6 +144,18 @@ def register_default_options(option_manager):
" of ignored ones. For example, ``--extend-ignore=E4,E51,W234``.", " of ignored ones. For example, ``--extend-ignore=E4,E51,W234``.",
) )
add_option(
"--per-file-ignores",
parse_from_config=True,
comma_separated_list=True,
separator=utils.NEWLINE_SEPARATED_LIST_RE,
help="A pairing of filenames and violation codes that defines which "
"violations to ignore in a particular file. The filenames can be "
"specified in a manner similar to the ``--exclude`` option and the "
"violations work similarly to the ``--ignore`` and ``--select`` "
"options.",
)
add_option( add_option(
"--max-line-length", "--max-line-length",
type="int", type="int",

View file

@ -32,6 +32,7 @@ class Option(object):
parse_from_config=False, parse_from_config=False,
comma_separated_list=False, comma_separated_list=False,
normalize_paths=False, normalize_paths=False,
separator=None,
): ):
"""Initialize an Option instance wrapping optparse.Option. """Initialize an Option instance wrapping optparse.Option.
@ -79,6 +80,8 @@ class Option(object):
:param bool normalize_paths: :param bool normalize_paths:
Whether the option is expecting a path or list of paths and should Whether the option is expecting a path or list of paths and should
attempt to normalize the paths to absolute paths. attempt to normalize the paths to absolute paths.
:param separator:
The item that separates the "comma"-separated list.
""" """
self.short_option_name = short_option_name self.short_option_name = short_option_name
self.long_option_name = long_option_name self.long_option_name = long_option_name
@ -107,6 +110,7 @@ class Option(object):
self.parse_from_config = parse_from_config self.parse_from_config = parse_from_config
self.comma_separated_list = comma_separated_list self.comma_separated_list = comma_separated_list
self.normalize_paths = normalize_paths self.normalize_paths = normalize_paths
self.separator = separator or utils.COMMA_SEPARATED_LIST_RE
self.config_name = None self.config_name = None
if parse_from_config: if parse_from_config:

View file

@ -1,6 +1,7 @@
"""Implementation of the StyleGuide used by Flake8.""" """Implementation of the StyleGuide used by Flake8."""
import collections import collections
import contextlib import contextlib
import copy
import enum import enum
import functools import functools
import itertools import itertools
@ -321,8 +322,8 @@ class DecisionEngine(object):
return decision return decision
class StyleGuide(object): class StyleGuideManager(object):
"""Manage a Flake8 user's style guide.""" """Manage multiple style guides for a single run."""
def __init__(self, options, listener_trie, formatter, decider=None): def __init__(self, options, listener_trie, formatter, decider=None):
"""Initialize our StyleGuide. """Initialize our StyleGuide.
@ -334,8 +335,136 @@ class StyleGuide(object):
self.formatter = formatter self.formatter = formatter
self.stats = statistics.Statistics() self.stats = statistics.Statistics()
self.decider = decider or DecisionEngine(options) self.decider = decider or DecisionEngine(options)
self.style_guides = []
self.default_style_guide = StyleGuide(
options, listener_trie, formatter, decider=decider
)
self.style_guides = list(
itertools.chain(
[self.default_style_guide],
self.populate_style_guides_with(options),
)
)
def populate_style_guides_with(self, options):
"""Generate style guides from the per-file-ignores option.
:param options:
The original options parsed from the CLI and config file.
:type options:
:class:`~optparse.Values`
:returns:
A copy of the default style guide with overridden values.
:rtype:
:class:`~flake8.style_guide.StyleGuide`
"""
for value in options.per_file_ignores:
filename, violations_str = value.split(":")
violations = utils.parse_comma_separated_list(violations_str)
yield self.default_style_guide.copy(
filename=filename, extend_ignore_with=violations
)
def style_guide_for(self, filename):
"""Find the StyleGuide for the filename in particular."""
guides = sorted(
(g for g in self.style_guides if g.applies_to(filename)),
key=lambda g: len(g.filename or ""),
)
if len(guides) > 1:
return guides[-1]
return guides[0]
@contextlib.contextmanager
def processing_file(self, filename):
"""Record the fact that we're processing the file's results."""
guide = self.style_guide_for(filename)
with guide.processing_file(filename):
yield guide
def handle_error(
self,
code,
filename,
line_number,
column_number,
text,
physical_line=None,
):
# type: (str, str, int, int, str) -> int
"""Handle an error reported by a check.
:param str code:
The error code found, e.g., E123.
:param str filename:
The file in which the error was found.
:param int line_number:
The line number (where counting starts at 1) at which the error
occurs.
:param int column_number:
The column number (where counting starts at 1) at which the error
occurs.
:param str text:
The text of the error message.
:param str physical_line:
The actual physical line causing the error.
:returns:
1 if the error was reported. 0 if it was ignored. This is to allow
for counting of the number of errors found that were not ignored.
:rtype:
int
"""
guide = self.style_guide_for(filename)
return guide.handle_error(
code, filename, line_number, column_number, text, physical_line
)
def add_diff_ranges(self, diffinfo):
"""Update the StyleGuides to filter out information not in the diff.
This provides information to the underlying StyleGuides so that only
the errors in the line number ranges are reported.
:param dict diffinfo:
Dictionary mapping filenames to sets of line number ranges.
"""
for guide in self.style_guides.values():
guide.add_diff_ranges(diffinfo)
class StyleGuide(object):
"""Manage a Flake8 user's style guide."""
def __init__(
self, options, listener_trie, formatter, filename=None, decider=None
):
"""Initialize our StyleGuide.
.. todo:: Add parameter documentation.
"""
self.options = options
self.listener = listener_trie
self.formatter = formatter
self.stats = statistics.Statistics()
self.decider = decider or DecisionEngine(options)
self.filename = filename
if self.filename:
self.filename = utils.normalize_path(self.filename)
self._parsed_diff = {} self._parsed_diff = {}
def __repr__(self):
"""Make it easier to debug which StyleGuide we're using."""
return "<StyleGuide [{}]>".format(self.filename)
def copy(self, filename=None, extend_ignore_with=None, **kwargs):
"""Create a copy of this style guide with different values."""
filename = filename or self.filename
options = copy.copy(self.options)
options.ignore.extend(extend_ignore_with or [])
return StyleGuide(
options, self.listener, self.formatter, filename=filename
)
@contextlib.contextmanager @contextlib.contextmanager
def processing_file(self, filename): def processing_file(self, filename):
"""Record the fact that we're processing the file's results.""" """Record the fact that we're processing the file's results."""
@ -343,6 +472,26 @@ class StyleGuide(object):
yield self yield self
self.formatter.finished(filename) self.formatter.finished(filename)
def applies_to(self, filename):
"""Check if this StyleGuide applies to the file.
:param str filename:
The name of the file with violations that we're potentially
applying this StyleGuide to.
:returns:
True if this applies, False otherwise
:rtype:
bool
"""
if self.filename is None:
return True
normalized_filename = utils.normalize_path(filename)
return (
normalized_filename == self.filename
or utils.fnmatch(filename, self.filename)
or utils.fnmatch(normalized_filename, self.filename)
)
def should_report_error(self, code): def should_report_error(self, code):
# type: (str) -> Decision # type: (str) -> Decision
"""Determine if the error code should be reported or ignored. """Determine if the error code should be reported or ignored.

View file

@ -11,6 +11,7 @@ import tokenize
DIFF_HUNK_REGEXP = re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@.*$") DIFF_HUNK_REGEXP = re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@.*$")
COMMA_SEPARATED_LIST_RE = re.compile(r"[,\s]") COMMA_SEPARATED_LIST_RE = re.compile(r"[,\s]")
NEWLINE_SEPARATED_LIST_RE = re.compile(r"[\s]")
LOCAL_PLUGIN_LIST_RE = re.compile(r"[,\t\n\r\f\v]") LOCAL_PLUGIN_LIST_RE = re.compile(r"[,\t\n\r\f\v]")