Merge branch 'pr/78'

Closes #78
This commit is contained in:
Ian Cordasco 2016-07-20 19:29:32 -05:00
commit 1dad1e3a95
No known key found for this signature in database
GPG key ID: 656D3395E4A9791A
7 changed files with 118 additions and 64 deletions

View file

@ -12,10 +12,11 @@ versions.
If your plugin does not register options, it *should* Just Work. If your plugin does not register options, it *should* Just Work.
The **only** breaking change in |Flake8| 3.0 is the fact that we no longer The **only two** breaking changes in |Flake8| 3.0 is the fact that we no
check the option parser for a list of strings to parse from a config file. On longer check the option parser for a list of strings to parse from a config
|Flake8| 2.x, to have an option parsed from the configuration files that file and we no longer patch pep8 or pycodestyle's ``stdin_get_value``
|Flake8| finds and parses you would have to do something like: functions. On |Flake8| 2.x, to have an option parsed from the configuration
files that |Flake8| finds and parses you would have to do something like:
.. code-block:: python .. code-block:: python
@ -101,56 +102,86 @@ as a single path.
Option Handling on Flake8 2 and 3 Option Handling on Flake8 2 and 3
================================= =================================
So, in conclusion, we can now write our plugin that relies on registering To ease the transition, the |Flake8| maintainers have released
options with |Flake8| and have it work on |Flake8| 2.x and 3.x. `flake8-polyfill`_. |polyfill| provides a convenience function to help users
transition between Flake8 2 and 3 without issue. For example, if your plugin
has to work on Flake8 2.x and 3.x but you want to take advantage of some of
the new options to ``add_option``, you can do
.. code-block:: python .. code-block:: python
import optparse from flake8_polyfill import options
option_args = ('-X', '--example-flag')
option_kwargs = {
'type': 'string',
'parse_from_config': True,
'help': '...',
}
try:
# Flake8 3.x registration
parser.add_option(*option_args, **option_kwargs)
except (optparse.OptionError, TypeError):
# Flake8 2.x registration
parse_from_config = option_kwargs.pop('parse_from_config', False)
option = parser.add_option(*option_args, **option_kwargs)
if parse_from_config:
parser.config_options.append(option.get_opt_string().lstrip('-'))
Or, you can write a tiny helper function: class MyPlugin(object):
@classmethod
.. code-block:: python def add_options(cls, parser):
options.register(
import optparse parser,
'--application-names', default='', type='string',
def register_opt(parser, *args, **kwargs): help='Names of the applications to be checked.',
try: parse_from_config=True,
# Flake8 3.x registration comma_separated_list=True,
parser.add_option(*args, **kwargs) )
except (optparse.OptionError, TypeError): options.register(
# Flake8 2.x registration parser,
parse_from_config = kwargs.pop('parse_from_config', False) '--style-name', default='', type='string',
kwargs.pop('comma_separated_list', False) help='The name of the style convention you want to use',
kwargs.pop('normalize_paths', False) parse_from_config=True,
option = parser.add_option(*args, **kwargs) )
if parse_from_config: options.register(
parser.config_options.append(option.get_opt_string().lstrip('-')) parser,
'--application-paths', default='', type='string',
.. code-block:: python help='Locations of the application code',
parse_from_config=True,
comma_separated_list=True,
normalize_paths=True,
)
@classmethod @classmethod
def register_options(cls, parser): def parse_options(cls, parsed_options):
register_opt(parser, '-X', '--example-flag', type='string', cls.application_names = parsed_options.application_names
parse_from_config=True, help='...') cls.style_name = parsed_options.style_name
cls.application_paths = parsed_options.application_paths
The transition period is admittedly not fantastic, but we believe that this |polyfill| will handle these extra options using *callbacks* to the option
is a worthwhile change for plugin developers going forward. We also hope to parser. The project has direct replications of the functions that |Flake8|
help with the transition phase for as many plugins as we can manage. uses to provide the same functionality. This means that the values you receive
should be identically parsed whether you're using Flake8 2.x or 3.x.
.. autofunction:: flake8_polyfill.options.register
Standard In Handling on Flake8 2.5, 2.6, and 3
==============================================
After releasing |Flake8| 2.6, handling standard-in became a bit trickier for
some plugins. |Flake8| 2.5 and earlier had started monkey-patching pep8's
``stdin_get_value`` function. 2.6 switched to pycodestyle and only
monkey-patched that. 3.0 has its own internal implementation and uses that but
does not directly provide anything for plugins using pep8 and pycodestyle's
``stdin_get_value`` function. |polyfill| provides this functionality for
plugin developers via it's :mod:`flake8_polyfill.stdin` module.
If a plugin needs to read the content from stdin, it can do the following:
.. code-block:: python
from flake8_polyfill import stdin
stdin.monkey_patch('pep8') # To monkey-patch only pep8
stdin.monkey_patch('pycodestyle') # To monkey-patch only pycodestyle
stdin.monkey_patch('all') # To monkey-patch both pep8 and pycodestyle
Further, when using ``all``, |polyfill| does not require both packages to be
installed but will attempt to monkey-patch both and will silently ignore the
fact that pep8 or pycodestyle is not installed.
.. autofunction:: flake8_polyfill.stdin.monkey_patch
.. links
.. _flake8-polyfill: https://pypi.io/project/flake8-polyfill/
.. |polyfill| replace:: ``flake8-polyfill``

View file

@ -2,3 +2,4 @@ sphinx>=1.3.0
sphinx_rtd_theme sphinx_rtd_theme
sphinx-prompt sphinx-prompt
configparser configparser
flake8-polyfill

View file

@ -234,6 +234,11 @@ class Manager(object):
:rtype: :rtype:
bool bool
""" """
if path == '-':
if self.options.stdin_display_name == 'stdin':
return False
path = self.options.stdin_display_name
exclude = self.options.exclude exclude = self.options.exclude
if not exclude: if not exclude:
return False return False
@ -271,7 +276,7 @@ class Manager(object):
return (file_exists and matches_filename_patterns) or is_stdin return (file_exists and matches_filename_patterns) or is_stdin
self.checkers = [ self.checkers = [
FileChecker(filename, self.checks, self.style_guide) FileChecker(filename, self.checks, self.options)
for argument in paths for argument in paths
for filename in utils.filenames_from(argument, for filename in utils.filenames_from(argument,
self.is_path_excluded) self.is_path_excluded)
@ -294,7 +299,7 @@ class Manager(object):
results_reported = results_found = 0 results_reported = results_found = 0
for checker in self.checkers: for checker in self.checkers:
results = sorted(checker.results, key=lambda tup: (tup[2], tup[3])) results = sorted(checker.results, key=lambda tup: (tup[2], tup[3]))
results_reported += self._handle_results(checker.filename, results_reported += self._handle_results(checker.display_name,
results) results)
results_found += len(results) results_found += len(results)
return (results_found, results_reported) return (results_found, results_reported)
@ -315,9 +320,9 @@ class Manager(object):
final_results[filename] = results final_results[filename] = results
for checker in self.checkers: for checker in self.checkers:
filename = checker.filename filename = checker.display_name
checker.results = sorted(final_results.get(filename, []), checker.results = sorted(final_results.get(filename, []),
key=lambda tup: (tup[1], tup[2])) key=lambda tup: (tup[2], tup[2]))
def run_serial(self): def run_serial(self):
"""Run the checkers in serial.""" """Run the checkers in serial."""
@ -379,7 +384,7 @@ class Manager(object):
class FileChecker(object): class FileChecker(object):
"""Manage running checks for a file and aggregate the results.""" """Manage running checks for a file and aggregate the results."""
def __init__(self, filename, checks, style_guide): def __init__(self, filename, checks, options):
"""Initialize our file checker. """Initialize our file checker.
:param str filename: :param str filename:
@ -388,16 +393,17 @@ class FileChecker(object):
The plugins registered to check the file. The plugins registered to check the file.
:type checks: :type checks:
flake8.plugins.manager.Checkers flake8.plugins.manager.Checkers
:param style_guide: :param options:
The initialized StyleGuide for this particular run. Parsed option values from config and command-line.
:type style_guide: :type options:
flake8.style_guide.StyleGuide optparse.Values
""" """
self.options = options
self.filename = filename self.filename = filename
self.checks = checks self.checks = checks
self.style_guide = style_guide
self.results = [] self.results = []
self.processor = self._make_processor() self.processor = self._make_processor()
self.display_name = self.processor.filename
self.statistics = { self.statistics = {
'tokens': 0, 'tokens': 0,
'logical lines': 0, 'logical lines': 0,
@ -406,8 +412,7 @@ class FileChecker(object):
def _make_processor(self): def _make_processor(self):
try: try:
return processor.FileProcessor(self.filename, return processor.FileProcessor(self.filename, self.options)
self.style_guide.options)
except IOError: except IOError:
# If we can not read the file due to an IOError (e.g., the file # If we can not read the file due to an IOError (e.g., the file
# does not exist or we do not have the permissions to open it) # does not exist or we do not have the permissions to open it)

View file

@ -55,12 +55,12 @@ class FileProcessor(object):
:param str filename: :param str filename:
Name of the file to process Name of the file to process
""" """
self.options = options
self.filename = filename self.filename = filename
self.lines = lines self.lines = lines
if lines is None: if lines is None:
self.lines = self.read_lines() self.lines = self.read_lines()
self.strip_utf_bom() self.strip_utf_bom()
self.options = options
# Defaults for public attributes # Defaults for public attributes
#: Number of preceding blank lines #: Number of preceding blank lines
@ -272,9 +272,11 @@ class FileProcessor(object):
# type: () -> List[str] # type: () -> List[str]
"""Read the lines for this file checker.""" """Read the lines for this file checker."""
if self.filename is None or self.filename == '-': if self.filename is None or self.filename == '-':
self.filename = 'stdin' self.filename = self.options.stdin_display_name
return self.read_lines_from_stdin() lines = self.read_lines_from_stdin()
return self.read_lines_from_filename() else:
lines = self.read_lines_from_filename()
return lines
def _readlines_py2(self): def _readlines_py2(self):
# type: () -> List[str] # type: () -> List[str]

View file

@ -260,8 +260,6 @@ class StyleGuide(object):
""" """
error = Error(code, filename, line_number, column_number, text, error = Error(code, filename, line_number, column_number, text,
physical_line) physical_line)
if error.filename is None or error.filename == '-':
error = error._replace(filename=self.options.stdin_display_name)
error_is_selected = (self.should_report_error(error.code) is error_is_selected = (self.should_report_error(error.code) is
Decision.Selected) Decision.Selected)
is_not_inline_ignored = self.is_inline_ignored(error) is False is_not_inline_ignored = self.is_inline_ignored(error) is False

View file

@ -208,6 +208,10 @@ def filenames_from(arg, predicate=None):
""" """
if predicate is None: if predicate is None:
predicate = _default_predicate predicate = _default_predicate
if predicate(arg):
return
if os.path.isdir(arg): if os.path.isdir(arg):
for root, sub_directories, files in os.walk(arg): for root, sub_directories, files in os.walk(arg):
if predicate(root): if predicate(root):

View file

@ -14,6 +14,7 @@ def options_from(**kwargs):
kwargs.setdefault('hang_closing', True) kwargs.setdefault('hang_closing', True)
kwargs.setdefault('max_line_length', 79) kwargs.setdefault('max_line_length', 79)
kwargs.setdefault('verbose', False) kwargs.setdefault('verbose', False)
kwargs.setdefault('stdin_display_name', 'stdin')
return optparse.Values(kwargs) return optparse.Values(kwargs)
@ -63,7 +64,7 @@ def test_read_lines_from_stdin(stdin_get_value):
@mock.patch('flake8.utils.stdin_get_value') @mock.patch('flake8.utils.stdin_get_value')
def test_read_lines_sets_filename_attribute(stdin_get_value): def test_stdin_filename_attribute(stdin_get_value):
"""Verify that we update the filename attribute.""" """Verify that we update the filename attribute."""
stdin_value = mock.Mock() stdin_value = mock.Mock()
stdin_value.splitlines.return_value = [] stdin_value.splitlines.return_value = []
@ -72,6 +73,18 @@ def test_read_lines_sets_filename_attribute(stdin_get_value):
assert file_processor.filename == 'stdin' assert file_processor.filename == 'stdin'
@mock.patch('flake8.utils.stdin_get_value')
def test_read_lines_uses_display_name(stdin_get_value):
"""Verify that when processing stdin we use a display name if present."""
stdin_value = mock.Mock()
stdin_value.splitlines.return_value = []
stdin_get_value.return_value = stdin_value
file_processor = processor.FileProcessor('-', options_from(
stdin_display_name='display_name.py'
))
assert file_processor.filename == 'display_name.py'
def test_line_for(): def test_line_for():
"""Verify we grab the correct line from the cached lines.""" """Verify we grab the correct line from the cached lines."""
file_processor = processor.FileProcessor('-', options_from(), lines=[ file_processor = processor.FileProcessor('-', options_from(), lines=[