summaryrefslogtreecommitdiff
path: root/src/flake8/main
diff options
context:
space:
mode:
Diffstat (limited to 'src/flake8/main')
-rw-r--r--src/flake8/main/__init__.py1
-rw-r--r--src/flake8/main/application.py296
-rw-r--r--src/flake8/main/cli.py17
-rw-r--r--src/flake8/main/git.py207
-rw-r--r--src/flake8/main/mercurial.py128
-rw-r--r--src/flake8/main/options.py201
-rw-r--r--src/flake8/main/setuptools_command.py77
-rw-r--r--src/flake8/main/vcs.py39
8 files changed, 966 insertions, 0 deletions
diff --git a/src/flake8/main/__init__.py b/src/flake8/main/__init__.py
new file mode 100644
index 0000000..d3aa1de
--- /dev/null
+++ b/src/flake8/main/__init__.py
@@ -0,0 +1 @@
+"""Module containing the logic for the Flake8 entry-points."""
diff --git a/src/flake8/main/application.py b/src/flake8/main/application.py
new file mode 100644
index 0000000..225c701
--- /dev/null
+++ b/src/flake8/main/application.py
@@ -0,0 +1,296 @@
+"""Module containing the application logic for Flake8."""
+from __future__ import print_function
+
+import logging
+import sys
+import time
+
+import flake8
+from flake8 import checker
+from flake8 import defaults
+from flake8 import style_guide
+from flake8 import utils
+from flake8.main import options
+from flake8.options import aggregator
+from flake8.options import manager
+from flake8.plugins import manager as plugin_manager
+
+LOG = logging.getLogger(__name__)
+
+
+class Application(object):
+ """Abstract our application into a class."""
+
+ def __init__(self, program='flake8', version=flake8.__version__):
+ # type: (str, str) -> NoneType
+ """Initialize our application.
+
+ :param str program:
+ The name of the program/application that we're executing.
+ :param str version:
+ The version of the program/application we're executing.
+ """
+ #: The timestamp when the Application instance was instantiated.
+ self.start_time = time.time()
+ #: The timestamp when the Application finished reported errors.
+ self.end_time = None
+ #: The name of the program being run
+ self.program = program
+ #: The version of the program being run
+ self.version = version
+ #: The instance of :class:`flake8.options.manager.OptionManager` used
+ #: to parse and handle the options and arguments passed by the user
+ self.option_manager = manager.OptionManager(
+ prog='flake8', version=flake8.__version__
+ )
+ options.register_default_options(self.option_manager)
+
+ # We haven't found or registered our plugins yet, so let's defer
+ # printing the version until we aggregate options from config files
+ # and the command-line. First, let's clone our arguments on the CLI,
+ # then we'll attempt to remove ``--version`` so that we can avoid
+ # triggering the "version" action in optparse. If it's not there, we
+ # do not need to worry and we can continue. If it is, we successfully
+ # defer printing the version until just a little bit later.
+ # Similarly we have to defer printing the help text until later.
+ args = sys.argv[:]
+ try:
+ args.remove('--version')
+ except ValueError:
+ pass
+ try:
+ args.remove('--help')
+ except ValueError:
+ pass
+ try:
+ args.remove('-h')
+ except ValueError:
+ pass
+
+ preliminary_opts, _ = self.option_manager.parse_args(args)
+ # Set the verbosity of the program
+ flake8.configure_logging(preliminary_opts.verbose,
+ preliminary_opts.output_file)
+
+ #: The instance of :class:`flake8.plugins.manager.Checkers`
+ self.check_plugins = None
+ #: The instance of :class:`flake8.plugins.manager.Listeners`
+ self.listening_plugins = None
+ #: The instance of :class:`flake8.plugins.manager.ReportFormatters`
+ self.formatting_plugins = None
+ #: The user-selected formatter from :attr:`formatting_plugins`
+ 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
+ self.guide = None
+ #: The :class:`flake8.checker.Manager` that will handle running all of
+ #: the checks selected by the user.
+ self.file_checker_manager = None
+
+ #: The user-supplied options parsed into an instance of
+ #: :class:`optparse.Values`
+ self.options = None
+ #: The left over arguments that were not parsed by
+ #: :attr:`option_manager`
+ self.args = None
+ #: The number of errors, warnings, and other messages after running
+ #: flake8 and taking into account ignored errors and lines.
+ self.result_count = 0
+ #: The total number of errors before accounting for ignored errors and
+ #: lines.
+ self.total_result_count = 0
+
+ #: Whether the program is processing a diff or not
+ self.running_against_diff = False
+ #: The parsed diff information
+ self.parsed_diff = {}
+
+ def exit(self):
+ # type: () -> NoneType
+ """Handle finalization and exiting the program.
+
+ This should be the last thing called on the application instance. It
+ will check certain options and exit appropriately.
+ """
+ if self.options.count:
+ print(self.result_count)
+
+ if not self.options.exit_zero:
+ raise SystemExit(self.result_count > 0)
+
+ def find_plugins(self):
+ # type: () -> NoneType
+ """Find and load the plugins for this application.
+
+ If :attr:`check_plugins`, :attr:`listening_plugins`, or
+ :attr:`formatting_plugins` are ``None`` then this method will update
+ them with the appropriate plugin manager instance. Given the expense
+ of finding plugins (via :mod:`pkg_resources`) we want this to be
+ idempotent and so only update those attributes if they are ``None``.
+ """
+ if self.check_plugins is None:
+ self.check_plugins = plugin_manager.Checkers()
+
+ if self.listening_plugins is None:
+ self.listening_plugins = plugin_manager.Listeners()
+
+ if self.formatting_plugins is None:
+ self.formatting_plugins = plugin_manager.ReportFormatters()
+
+ self.check_plugins.load_plugins()
+ self.listening_plugins.load_plugins()
+ self.formatting_plugins.load_plugins()
+
+ def register_plugin_options(self):
+ # type: () -> NoneType
+ """Register options provided by plugins to our option manager."""
+ self.check_plugins.register_options(self.option_manager)
+ self.check_plugins.register_plugin_versions(self.option_manager)
+ self.listening_plugins.register_options(self.option_manager)
+ self.formatting_plugins.register_options(self.option_manager)
+
+ def parse_configuration_and_cli(self, argv=None):
+ # type: (Union[NoneType, List[str]]) -> NoneType
+ """Parse configuration files and the CLI options.
+
+ :param list argv:
+ Command-line arguments passed in directly.
+ """
+ if self.options is None and self.args is None:
+ self.options, self.args = aggregator.aggregate_options(
+ self.option_manager, argv
+ )
+
+ self.running_against_diff = self.options.diff
+ if self.running_against_diff:
+ self.parsed_diff = utils.parse_unified_diff()
+
+ self.check_plugins.provide_options(self.option_manager, self.options,
+ self.args)
+ self.listening_plugins.provide_options(self.option_manager,
+ self.options,
+ self.args)
+ self.formatting_plugins.provide_options(self.option_manager,
+ self.options,
+ self.args)
+
+ def make_formatter(self):
+ # type: () -> NoneType
+ """Initialize a formatter based on the parsed options."""
+ if self.formatter is None:
+ self.formatter = self.formatting_plugins.get(
+ self.options.format, self.formatting_plugins['default']
+ ).execute(self.options)
+
+ def make_notifier(self):
+ # type: () -> NoneType
+ """Initialize our listener Notifier."""
+ if self.listener_trie is None:
+ self.listener_trie = self.listening_plugins.build_notifier()
+
+ def make_guide(self):
+ # type: () -> NoneType
+ """Initialize our StyleGuide."""
+ if self.guide is None:
+ self.guide = style_guide.StyleGuide(
+ self.options, self.listener_trie, self.formatter
+ )
+
+ if self.running_against_diff:
+ self.guide.add_diff_ranges(self.parsed_diff)
+
+ def make_file_checker_manager(self):
+ # type: () -> NoneType
+ """Initialize our FileChecker Manager."""
+ if self.file_checker_manager is None:
+ self.file_checker_manager = checker.Manager(
+ style_guide=self.guide,
+ arguments=self.args,
+ checker_plugins=self.check_plugins,
+ )
+
+ def run_checks(self):
+ # type: () -> NoneType
+ """Run the actual checks with the FileChecker Manager.
+
+ This method encapsulates the logic to make a
+ :class:`~flake8.checker.Manger` instance run the checks it is
+ managing.
+ """
+ files = None
+ if self.running_against_diff:
+ files = list(sorted(self.parsed_diff.keys()))
+ self.file_checker_manager.start(files)
+ self.file_checker_manager.run()
+ LOG.info('Finished running')
+ self.file_checker_manager.stop()
+ self.end_time = time.time()
+
+ def report_benchmarks(self):
+ """Aggregate, calculate, and report benchmarks for this run."""
+ if not self.options.benchmark:
+ return
+
+ time_elapsed = self.end_time - self.start_time
+ statistics = [('seconds elapsed', time_elapsed)]
+ add_statistic = statistics.append
+ for statistic in (defaults.STATISTIC_NAMES + ('files',)):
+ value = self.file_checker_manager.statistics[statistic]
+ total_description = 'total ' + statistic + ' processed'
+ add_statistic((total_description, value))
+ per_second_description = statistic + ' processed per second'
+ add_statistic((per_second_description, int(value / time_elapsed)))
+
+ self.formatter.show_benchmarks(statistics)
+
+ def report_errors(self):
+ # type: () -> NoneType
+ """Report all the errors found by flake8 3.0.
+
+ This also updates the :attr:`result_count` attribute with the total
+ number of errors, warnings, and other messages found.
+ """
+ LOG.info('Reporting errors')
+ results = self.file_checker_manager.report()
+ self.total_result_count, self.result_count = results
+ LOG.info('Found a total of %d results and reported %d',
+ self.total_result_count, self.result_count)
+
+ def initialize(self, argv):
+ # type: () -> NoneType
+ """Initialize the application to be run.
+
+ This finds the plugins, registers their options, and parses the
+ command-line arguments.
+ """
+ self.find_plugins()
+ self.register_plugin_options()
+ self.parse_configuration_and_cli(argv)
+ self.make_formatter()
+ self.make_notifier()
+ self.make_guide()
+ self.make_file_checker_manager()
+
+ def _run(self, argv):
+ # type: (Union[NoneType, List[str]]) -> NoneType
+ self.initialize(argv)
+ self.run_checks()
+ self.report_errors()
+ self.report_benchmarks()
+
+ def run(self, argv=None):
+ # type: (Union[NoneType, List[str]]) -> NoneType
+ """Run our application.
+
+ This method will also handle KeyboardInterrupt exceptions for the
+ entirety of the flake8 application. If it sees a KeyboardInterrupt it
+ will forcibly clean up the :class:`~flake8.checker.Manager`.
+ """
+ try:
+ self._run(argv)
+ except KeyboardInterrupt as exc:
+ LOG.critical('Caught keyboard interrupt from user')
+ LOG.exception(exc)
+ self.file_checker_manager._force_cleanup()
diff --git a/src/flake8/main/cli.py b/src/flake8/main/cli.py
new file mode 100644
index 0000000..29bd159
--- /dev/null
+++ b/src/flake8/main/cli.py
@@ -0,0 +1,17 @@
+"""Command-line implementation of flake8."""
+from flake8.main import application
+
+
+def main(argv=None):
+ # type: (Union[NoneType, List[str]]) -> NoneType
+ """Main entry-point for the flake8 command-line tool.
+
+ This handles the creation of an instance of :class:`Application`, runs it,
+ and then exits the application.
+
+ :param list argv:
+ The arguments to be passed to the application for parsing.
+ """
+ app = application.Application()
+ app.run(argv)
+ app.exit()
diff --git a/src/flake8/main/git.py b/src/flake8/main/git.py
new file mode 100644
index 0000000..bae0233
--- /dev/null
+++ b/src/flake8/main/git.py
@@ -0,0 +1,207 @@
+"""Module containing the main git hook interface and helpers.
+
+.. autofunction:: hook
+.. autofunction:: install
+
+"""
+import contextlib
+import os
+import shutil
+import stat
+import subprocess
+import sys
+import tempfile
+
+from flake8 import defaults
+from flake8 import exceptions
+
+__all__ = ('hook', 'install')
+
+
+def hook(lazy=False, strict=False):
+ """Execute Flake8 on the files in git's index.
+
+ Determine which files are about to be committed and run Flake8 over them
+ to check for violations.
+
+ :param bool lazy:
+ Find files not added to the index prior to committing. This is useful
+ if you frequently use ``git commit -a`` for example. This defaults to
+ False since it will otherwise include files not in the index.
+ :param bool strict:
+ If True, return the total number of errors/violations found by Flake8.
+ This will cause the hook to fail.
+ :returns:
+ Total number of errors found during the run.
+ :rtype:
+ int
+ """
+ # NOTE(sigmavirus24): Delay import of application until we need it.
+ from flake8.main import application
+ app = application.Application()
+ with make_temporary_directory() as tempdir:
+ filepaths = list(copy_indexed_files_to(tempdir, lazy))
+ app.initialize(filepaths)
+ app.run_checks()
+
+ app.report_errors()
+ if strict:
+ return app.result_count
+ return 0
+
+
+def install():
+ """Install the git hook script.
+
+ This searches for the ``.git`` directory and will install an executable
+ pre-commit python script in the hooks sub-directory if one does not
+ already exist.
+
+ :returns:
+ True if successful, False if the git directory doesn't exist.
+ :rtype:
+ bool
+ :raises:
+ flake8.exceptions.GitHookAlreadyExists
+ """
+ git_directory = find_git_directory()
+ if git_directory is None or not os.path.exists(git_directory):
+ return False
+
+ hooks_directory = os.path.join(git_directory, 'hooks')
+ if not os.path.exists(hooks_directory):
+ os.mkdir(hooks_directory)
+
+ pre_commit_file = os.path.abspath(
+ os.path.join(hooks_directory, 'pre-commit')
+ )
+ if os.path.exists(pre_commit_file):
+ raise exceptions.GitHookAlreadyExists(
+ 'File already exists',
+ path=pre_commit_file,
+ )
+
+ executable = get_executable()
+
+ with open(pre_commit_file, 'w') as fd:
+ fd.write(_HOOK_TEMPLATE.format(executable=executable))
+
+ # NOTE(sigmavirus24): The following sets:
+ # - read, write, and execute permissions for the owner
+ # - read permissions for people in the group
+ # - read permissions for other people
+ # The owner needs the file to be readable, writable, and executable
+ # so that git can actually execute it as a hook.
+ pre_commit_permissions = stat.S_IRWXU | stat.S_IRGRP | stat.S_IROTH
+ os.chmod(pre_commit_file, pre_commit_permissions)
+ return True
+
+
+def get_executable():
+ if sys.executable is not None:
+ return sys.executable
+ return '/usr/bin/env python'
+
+
+def find_git_directory():
+ rev_parse = piped_process(['git', 'rev-parse', '--git-dir'])
+
+ (stdout, _) = rev_parse.communicate()
+ stdout = to_text(stdout)
+
+ if rev_parse.returncode == 0:
+ return stdout.strip()
+ return None
+
+
+def copy_indexed_files_to(temporary_directory, lazy):
+ modified_files = find_modified_files(lazy)
+ for filename in modified_files:
+ contents = get_staged_contents_from(filename)
+ yield copy_file_to(temporary_directory, filename, contents)
+
+
+def copy_file_to(destination_directory, filepath, contents):
+ directory, filename = os.path.split(os.path.abspath(filepath))
+ temporary_directory = make_temporary_directory_from(destination_directory,
+ directory)
+ if not os.path.exists(temporary_directory):
+ os.makedirs(temporary_directory)
+ temporary_filepath = os.path.join(temporary_directory, filename)
+ with open(temporary_filepath, 'wb') as fd:
+ fd.write(contents)
+ return temporary_filepath
+
+
+def make_temporary_directory_from(destination, directory):
+ prefix = os.path.commonprefix([directory, destination])
+ common_directory_path = os.path.relpath(directory, start=prefix)
+ return os.path.join(destination, common_directory_path)
+
+
+def find_modified_files(lazy):
+ diff_index = piped_process(
+ ['git', 'diff-index', '--cached', '--name-only',
+ '--diff-filter=ACMRTUXB', 'HEAD'],
+ )
+
+ (stdout, _) = diff_index.communicate()
+ stdout = to_text(stdout)
+ return stdout.splitlines()
+
+
+def get_staged_contents_from(filename):
+ git_show = piped_process(['git', 'show', ':{0}'.format(filename)])
+ (stdout, _) = git_show.communicate()
+ return stdout
+
+
+@contextlib.contextmanager
+def make_temporary_directory():
+ temporary_directory = tempfile.mkdtemp()
+ yield temporary_directory
+ shutil.rmtree(temporary_directory, ignore_errors=True)
+
+
+def to_text(string):
+ """Ensure that the string is text."""
+ if callable(getattr(string, 'decode', None)):
+ return string.decode('utf-8')
+ return string
+
+
+def piped_process(command):
+ return subprocess.Popen(
+ command,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ )
+
+
+def git_config_for(parameter):
+ config = piped_process(['git', 'config', '--get', '--bool', parameter])
+ (stdout, _) = config.communicate()
+ return to_text(stdout)
+
+
+def config_for(parameter):
+ environment_variable = 'flake8_{0}'.format(parameter).upper()
+ git_variable = 'flake8.{0}'.format(parameter)
+ value = os.environ.get(environment_variable, git_config_for(git_variable))
+ return value.lower() in defaults.TRUTHY_VALUES
+
+
+_HOOK_TEMPLATE = """#!{executable}
+import os
+import sys
+
+from flake8.main import git
+
+if __name__ == '__main__':
+ sys.exit(
+ git.hook(
+ strict=git.config_for('strict'),
+ lazy=git.config_for('lazy'),
+ )
+ )
+"""
diff --git a/src/flake8/main/mercurial.py b/src/flake8/main/mercurial.py
new file mode 100644
index 0000000..d067612
--- /dev/null
+++ b/src/flake8/main/mercurial.py
@@ -0,0 +1,128 @@
+"""Module containing the main mecurial hook interface and helpers.
+
+.. autofunction:: hook
+.. autofunction:: install
+
+"""
+import configparser
+import os
+import subprocess
+
+from flake8 import exceptions as exc
+
+__all__ = ('hook', 'install')
+
+
+def hook(ui, repo, **kwargs):
+ """Execute Flake8 on the repository provided by Mercurial.
+
+ To understand the parameters read more of the Mercurial documentation
+ around Hooks: https://www.mercurial-scm.org/wiki/Hook.
+
+ We avoid using the ``ui`` attribute because it can cause issues with
+ the GPL license tha Mercurial is under. We don't import it, but we
+ avoid using it all the same.
+ """
+ from flake8.main import application
+ hgrc = find_hgrc(create_if_missing=False)
+ if hgrc is None:
+ print('Cannot locate your root mercurial repository.')
+ raise SystemExit(True)
+
+ hgconfig = configparser_for(hgrc)
+ strict = hgconfig.get('flake8', 'strict', fallback=True)
+
+ filenames = list(get_filenames_from(repo, kwargs))
+
+ app = application.Application()
+ app.run(filenames)
+
+ if strict:
+ return app.result_count
+ return 0
+
+
+def install():
+ """Ensure that the mercurial hooks are installed."""
+ hgrc = find_hgrc(create_if_missing=True)
+ if hgrc is None:
+ return False
+
+ hgconfig = configparser_for(hgrc)
+
+ if not hgconfig.has_section('hooks'):
+ hgconfig.add_section('hooks')
+
+ if hgconfig.has_option('hooks', 'commit'):
+ raise exc.MercurialCommitHookAlreadyExists(
+ path=hgrc,
+ value=hgconfig.get('hooks', 'commit'),
+ )
+
+ if hgconfig.has_option('hooks', 'qrefresh'):
+ raise exc.MercurialQRefreshHookAlreadyExists(
+ path=hgrc,
+ value=hgconfig.get('hooks', 'qrefresh'),
+ )
+
+ hgconfig.set('hooks', 'commit', 'python:flake8.main.mercurial.hook')
+ hgconfig.set('hooks', 'qrefresh', 'python:flake8.main.mercurial.hook')
+
+ if not hgconfig.has_section('flake8'):
+ hgconfig.add_section('flake8')
+
+ if not hgconfig.has_option('flake8', 'strict'):
+ hgconfig.set('flake8', 'strict', False)
+
+ with open(hgrc, 'w') as fd:
+ hgconfig.write(fd)
+
+ return True
+
+
+def get_filenames_from(repository, kwargs):
+ seen_filenames = set()
+ node = kwargs['node']
+ for revision in range(repository[node], len(repository)):
+ for filename in repository[revision].files():
+ full_filename = os.path.join(repository.root, filename)
+ have_seen_filename = full_filename in seen_filenames
+ filename_does_not_exist = not os.path.exists(full_filename)
+ if have_seen_filename or filename_does_not_exist:
+ continue
+
+ seen_filenames.add(full_filename)
+ if full_filename.endswith('.py'):
+ yield full_filename
+
+
+def find_hgrc(create_if_missing=False):
+ root = subprocess.Popen(
+ ['hg', 'root'],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ )
+
+ (hg_directory, _) = root.communicate()
+ if callable(getattr(hg_directory, 'decode', None)):
+ hg_directory = hg_directory.decode('utf-8')
+
+ if not os.path.isdir(hg_directory):
+ return None
+
+ hgrc = os.path.abspath(
+ os.path.join(hg_directory, '.hg', 'hgrc')
+ )
+ if not os.path.exists(hgrc):
+ if create_if_missing:
+ open(hgrc, 'w').close()
+ else:
+ return None
+
+ return hgrc
+
+
+def configparser_for(path):
+ parser = configparser.ConfigParser(interpolation=None)
+ parser.read(path)
+ return parser
diff --git a/src/flake8/main/options.py b/src/flake8/main/options.py
new file mode 100644
index 0000000..c725c38
--- /dev/null
+++ b/src/flake8/main/options.py
@@ -0,0 +1,201 @@
+"""Contains the logic for all of the default options for Flake8."""
+from flake8 import defaults
+from flake8.main import vcs
+
+
+def register_default_options(option_manager):
+ """Register the default options on our OptionManager.
+
+ The default options include:
+
+ - ``-v``/``--verbose``
+ - ``-q``/``--quiet``
+ - ``--count``
+ - ``--diff``
+ - ``--exclude``
+ - ``--filename``
+ - ``--format``
+ - ``--hang-closing``
+ - ``--ignore``
+ - ``--max-line-length``
+ - ``--select``
+ - ``--disable-noqa``
+ - ``--show-source``
+ - ``--statistics``
+ - ``--enable-extensions``
+ - ``--exit-zero``
+ - ``-j``/``--jobs``
+ - ``--output-file``
+ - ``--append-config``
+ - ``--config``
+ - ``--isolated``
+ """
+ add_option = option_manager.add_option
+
+ # pep8 options
+ add_option(
+ '-v', '--verbose', default=0, action='count',
+ parse_from_config=True,
+ help='Print more information about what is happening in flake8.'
+ ' This option is repeatable and will increase verbosity each '
+ 'time it is repeated.',
+ )
+ add_option(
+ '-q', '--quiet', default=0, action='count',
+ parse_from_config=True,
+ help='Report only file names, or nothing. This option is repeatable.',
+ )
+
+ add_option(
+ '--count', action='store_true', parse_from_config=True,
+ help='Print total number of errors and warnings to standard error and'
+ ' set the exit code to 1 if total is not empty.',
+ )
+
+ add_option(
+ '--diff', action='store_true',
+ help='Report changes only within line number ranges in the unified '
+ 'diff provided on standard in by the user.',
+ )
+
+ add_option(
+ '--exclude', metavar='patterns', default=defaults.EXCLUDE,
+ comma_separated_list=True, parse_from_config=True,
+ normalize_paths=True,
+ help='Comma-separated list of files or directories to exclude.'
+ ' (Default: %default)',
+ )
+
+ add_option(
+ '--filename', metavar='patterns', default='*.py',
+ parse_from_config=True, comma_separated_list=True,
+ help='Only check for filenames matching the patterns in this comma-'
+ 'separated list. (Default: %default)',
+ )
+
+ add_option(
+ '--stdin-display-name', default='stdin',
+ help='The name used when reporting errors from code passed via stdin.'
+ ' This is useful for editors piping the file contents to flake8.'
+ ' (Default: %default)',
+ )
+
+ # TODO(sigmavirus24): Figure out --first/--repeat
+
+ # NOTE(sigmavirus24): We can't use choices for this option since users can
+ # freely provide a format string and that will break if we restrict their
+ # choices.
+ add_option(
+ '--format', metavar='format', default='default',
+ parse_from_config=True,
+ help='Format errors according to the chosen formatter.',
+ )
+
+ add_option(
+ '--hang-closing', action='store_true', parse_from_config=True,
+ help='Hang closing bracket instead of matching indentation of opening'
+ " bracket's line.",
+ )
+
+ add_option(
+ '--ignore', metavar='errors', default=defaults.IGNORE,
+ parse_from_config=True, comma_separated_list=True,
+ help='Comma-separated list of errors and warnings to ignore (or skip).'
+ ' For example, ``--ignore=E4,E51,W234``. (Default: %default)',
+ )
+
+ add_option(
+ '--max-line-length', type='int', metavar='n',
+ default=defaults.MAX_LINE_LENGTH, parse_from_config=True,
+ help='Maximum allowed line length for the entirety of this run. '
+ '(Default: %default)',
+ )
+
+ add_option(
+ '--select', metavar='errors', default=defaults.SELECT,
+ parse_from_config=True, comma_separated_list=True,
+ help='Comma-separated list of errors and warnings to enable.'
+ ' For example, ``--select=E4,E51,W234``. (Default: %default)',
+ )
+
+ add_option(
+ '--disable-noqa', default=False, parse_from_config=True,
+ action='store_true',
+ help='Disable the effect of "# noqa". This will report errors on '
+ 'lines with "# noqa" at the end.'
+ )
+
+ # TODO(sigmavirus24): Decide what to do about --show-pep8
+
+ add_option(
+ '--show-source', action='store_true', parse_from_config=True,
+ help='Show the source generate each error or warning.',
+ )
+
+ add_option(
+ '--statistics', action='store_true', parse_from_config=True,
+ help='Count errors and warnings.',
+ )
+
+ # Flake8 options
+ add_option(
+ '--enable-extensions', default='', parse_from_config=True,
+ comma_separated_list=True, type='string',
+ help='Enable plugins and extensions that are otherwise disabled '
+ 'by default',
+ )
+
+ add_option(
+ '--exit-zero', action='store_true',
+ help='Exit with status code "0" even if there are errors.',
+ )
+
+ add_option(
+ '--install-hook', action='callback', type='choice',
+ choices=vcs.choices(), callback=vcs.install,
+ help='Install a hook that is run prior to a commit for the supported '
+ 'version control systema.'
+ )
+
+ add_option(
+ '-j', '--jobs', type='string', default='auto', parse_from_config=True,
+ help='Number of subprocesses to use to run checks in parallel. '
+ 'This is ignored on Windows. The default, "auto", will '
+ 'auto-detect the number of processors available to use.'
+ ' (Default: %default)',
+ )
+
+ add_option(
+ '--output-file', default=None, type='string', parse_from_config=True,
+ # callback=callbacks.redirect_stdout,
+ help='Redirect report to a file.',
+ )
+
+ # Config file options
+
+ add_option(
+ '--append-config', action='append',
+ help='Provide extra config files to parse in addition to the files '
+ 'found by Flake8 by default. These files are the last ones read '
+ 'and so they take the highest precedence when multiple files '
+ 'provide the same option.',
+ )
+
+ add_option(
+ '--config', default=None,
+ help='Path to the config file that will be the authoritative config '
+ 'source. This will cause Flake8 to ignore all other '
+ 'configuration files.'
+ )
+
+ add_option(
+ '--isolated', default=False, action='store_true',
+ help='Ignore all found configuration files.',
+ )
+
+ # Benchmarking
+
+ add_option(
+ '--benchmark', default=False, action='store_true',
+ help='Print benchmark information about this run of Flake8',
+ )
diff --git a/src/flake8/main/setuptools_command.py b/src/flake8/main/setuptools_command.py
new file mode 100644
index 0000000..1c27bf6
--- /dev/null
+++ b/src/flake8/main/setuptools_command.py
@@ -0,0 +1,77 @@
+"""The logic for Flake8's integration with setuptools."""
+import os
+
+import setuptools
+
+from flake8.main import application as app
+
+
+class Flake8(setuptools.Command):
+ """Run Flake8 via setuptools/distutils for registered modules."""
+
+ description = 'Run Flake8 on modules registered in setup.py'
+ # NOTE(sigmavirus24): If we populated this with a list of tuples, users
+ # could do something like ``python setup.py flake8 --ignore=E123,E234``
+ # but we would have to redefine it and we can't define it dynamically.
+ # Since I refuse to copy-and-paste the options here or maintain two lists
+ # of options, and since this will break when users use plugins that
+ # provide command-line options, we are leaving this empty. If users want
+ # to configure this command, they can do so through config files.
+ user_options = []
+
+ def initialize_options(self):
+ """Override this method to initialize our application."""
+ pass
+
+ def finalize_options(self):
+ """Override this to parse the parameters."""
+ pass
+
+ def package_files(self):
+ """Collect the files/dirs included in the registered modules."""
+ seen_package_directories = ()
+ directories = self.distribution.package_dir or {}
+ empty_directory_exists = '' in directories
+ packages = self.distribution.packages or []
+ for package in packages:
+ package_directory = package
+ if package in directories:
+ package_directory = directories[package]
+ elif empty_directory_exists:
+ package_directory = os.path.join(directories[''],
+ package_directory)
+
+ # NOTE(sigmavirus24): Do not collect submodules, e.g.,
+ # if we have:
+ # - flake8/
+ # - flake8/plugins/
+ # Flake8 only needs ``flake8/`` to be provided. It will
+ # recurse on its own.
+ if package_directory.startswith(seen_package_directories):
+ continue
+
+ seen_package_directories += (package_directory,)
+ yield package_directory
+
+ def module_files(self):
+ """Collect the files listed as py_modules."""
+ modules = self.distribution.py_modules or []
+ filename_from = '{0}.py'.format
+ for module in modules:
+ yield filename_from(module)
+
+ def distribution_files(self):
+ """Collect package and module files."""
+ for package in self.package_files():
+ yield package
+
+ for module in self.module_files():
+ yield module
+
+ yield 'setup.py'
+
+ def run(self):
+ """Run the Flake8 application."""
+ flake8 = app.Application()
+ flake8.run(list(self.distribution_files()))
+ flake8.exit()
diff --git a/src/flake8/main/vcs.py b/src/flake8/main/vcs.py
new file mode 100644
index 0000000..6f7499e
--- /dev/null
+++ b/src/flake8/main/vcs.py
@@ -0,0 +1,39 @@
+"""Module containing some of the logic for our VCS installation logic."""
+from flake8 import exceptions as exc
+from flake8.main import git
+from flake8.main import mercurial
+
+
+# NOTE(sigmavirus24): In the future, we may allow for VCS hooks to be defined
+# as plugins, e.g., adding a flake8.vcs entry-point. In that case, this
+# dictionary should disappear, and this module might contain more code for
+# managing those bits (in conjuntion with flake8.plugins.manager).
+_INSTALLERS = {
+ 'git': git.install,
+ 'mercurial': mercurial.install,
+}
+
+
+def install(option, option_string, value, parser):
+ """Determine which version control hook to install.
+
+ For more information about the callback signature, see:
+ https://docs.python.org/2/library/optparse.html#optparse-option-callbacks
+ """
+ installer = _INSTALLERS.get(value)
+ errored = False
+ successful = False
+ try:
+ successful = installer()
+ except exc.HookInstallationError as hook_error:
+ print(str(hook_error))
+ errored = True
+
+ if not successful:
+ print('Could not find the {0} directory'.format(value))
+ raise SystemExit(not successful and errored)
+
+
+def choices():
+ """Return the list of VCS choices."""
+ return list(_INSTALLERS.keys())