summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIan Stapleton Cordasco <graffatcolmingov@gmail.com>2018-10-25 09:10:06 -0500
committerIan Stapleton Cordasco <graffatcolmingov@gmail.com>2018-10-25 09:45:03 -0500
commitf2776107db605eb67362db00bc5e9ef2afa9f39e (patch)
tree8901fb8e57e30d01b41d0837000131a3198072df
parent52d88d8ca7208d1edc554b41a3b185720672823c (diff)
downloadflake8-f2776107db605eb67362db00bc5e9ef2afa9f39e.tar.gz
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
-rw-r--r--src/flake8/__init__.py2
-rw-r--r--src/flake8/main/application.py6
-rw-r--r--src/flake8/main/options.py14
-rw-r--r--src/flake8/options/manager.py4
-rw-r--r--src/flake8/style_guide.py151
-rw-r--r--src/flake8/utils.py1
6 files changed, 173 insertions, 5 deletions
diff --git a/src/flake8/__init__.py b/src/flake8/__init__.py
index 5f316c9..c15bf87 100644
--- a/src/flake8/__init__.py
+++ b/src/flake8/__init__.py
@@ -15,7 +15,7 @@ import sys
LOG = logging.getLogger(__name__)
LOG.addHandler(logging.NullHandler())
-__version__ = "3.6.0"
+__version__ = "3.7.0dev0"
__version_info__ = tuple(
int(i) for i in __version__.split(".") if i.isdigit()
)
diff --git a/src/flake8/main/application.py b/src/flake8/main/application.py
index bd409d7..b86b3cc 100644
--- a/src/flake8/main/application.py
+++ b/src/flake8/main/application.py
@@ -65,8 +65,8 @@ class Application(object):
self.formatter = None
#: The :class:`flake8.plugins.notifier.Notifier` for listening plugins
self.listener_trie = None
- #: The :class:`flake8.style_guide.StyleGuide` built from the user's
- #: options
+ #: The :class:`flake8.style_guide.StyleGuideManager` built from the
+ #: user's options
self.guide = None
#: The :class:`flake8.checker.Manager` that will handle running all of
#: the checks selected by the user.
@@ -283,7 +283,7 @@ class Application(object):
# type: () -> NoneType
"""Initialize our StyleGuide."""
if self.guide is None:
- self.guide = style_guide.StyleGuide(
+ self.guide = style_guide.StyleGuideManager(
self.options, self.listener_trie, self.formatter
)
diff --git a/src/flake8/main/options.py b/src/flake8/main/options.py
index 73b3932..2aabab8 100644
--- a/src/flake8/main/options.py
+++ b/src/flake8/main/options.py
@@ -1,5 +1,6 @@
"""Contains the logic for all of the default options for Flake8."""
from flake8 import defaults
+from flake8 import utils
from flake8.main import debug
from flake8.main import vcs
@@ -19,6 +20,7 @@ def register_default_options(option_manager):
- ``--hang-closing``
- ``--ignore``
- ``--extend-ignore``
+ - ``--per-file-ignores``
- ``--max-line-length``
- ``--select``
- ``--disable-noqa``
@@ -143,6 +145,18 @@ def register_default_options(option_manager):
)
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(
"--max-line-length",
type="int",
metavar="n",
diff --git a/src/flake8/options/manager.py b/src/flake8/options/manager.py
index 3f4e883..7be4315 100644
--- a/src/flake8/options/manager.py
+++ b/src/flake8/options/manager.py
@@ -32,6 +32,7 @@ class Option(object):
parse_from_config=False,
comma_separated_list=False,
normalize_paths=False,
+ separator=None,
):
"""Initialize an Option instance wrapping optparse.Option.
@@ -79,6 +80,8 @@ class Option(object):
:param bool normalize_paths:
Whether the option is expecting a path or list of paths and should
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.long_option_name = long_option_name
@@ -107,6 +110,7 @@ class Option(object):
self.parse_from_config = parse_from_config
self.comma_separated_list = comma_separated_list
self.normalize_paths = normalize_paths
+ self.separator = separator or utils.COMMA_SEPARATED_LIST_RE
self.config_name = None
if parse_from_config:
diff --git a/src/flake8/style_guide.py b/src/flake8/style_guide.py
index 7565b00..42576e9 100644
--- a/src/flake8/style_guide.py
+++ b/src/flake8/style_guide.py
@@ -1,6 +1,7 @@
"""Implementation of the StyleGuide used by Flake8."""
import collections
import contextlib
+import copy
import enum
import functools
import itertools
@@ -321,10 +322,122 @@ class DecisionEngine(object):
return decision
+class StyleGuideManager(object):
+ """Manage multiple style guides for a single run."""
+
+ def __init__(self, options, listener_trie, formatter, 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.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, decider=None):
+ def __init__(
+ self, options, listener_trie, formatter, filename=None, decider=None
+ ):
"""Initialize our StyleGuide.
.. todo:: Add parameter documentation.
@@ -334,8 +447,24 @@ class StyleGuide(object):
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 = {}
+ 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
def processing_file(self, filename):
"""Record the fact that we're processing the file's results."""
@@ -343,6 +472,26 @@ class StyleGuide(object):
yield self
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):
# type: (str) -> Decision
"""Determine if the error code should be reported or ignored.
diff --git a/src/flake8/utils.py b/src/flake8/utils.py
index a837577..e77998d 100644
--- a/src/flake8/utils.py
+++ b/src/flake8/utils.py
@@ -11,6 +11,7 @@ import tokenize
DIFF_HUNK_REGEXP = re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@.*$")
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]")