summaryrefslogtreecommitdiff
path: root/src/flake8/options
diff options
context:
space:
mode:
Diffstat (limited to 'src/flake8/options')
-rw-r--r--src/flake8/options/__init__.py12
-rw-r--r--src/flake8/options/aggregator.py74
-rw-r--r--src/flake8/options/config.py279
-rw-r--r--src/flake8/options/manager.py256
4 files changed, 621 insertions, 0 deletions
diff --git a/src/flake8/options/__init__.py b/src/flake8/options/__init__.py
new file mode 100644
index 0000000..cc20daa
--- /dev/null
+++ b/src/flake8/options/__init__.py
@@ -0,0 +1,12 @@
+"""Package containing the option manager and config management logic.
+
+- :mod:`flake8.options.config` contains the logic for finding, parsing, and
+ merging configuration files.
+
+- :mod:`flake8.options.manager` contains the logic for managing customized
+ Flake8 command-line and configuration options.
+
+- :mod:`flake8.options.aggregator` uses objects from both of the above modules
+ to aggregate configuration into one object used by plugins and Flake8.
+
+"""
diff --git a/src/flake8/options/aggregator.py b/src/flake8/options/aggregator.py
new file mode 100644
index 0000000..99d0cfe
--- /dev/null
+++ b/src/flake8/options/aggregator.py
@@ -0,0 +1,74 @@
+"""Aggregation function for CLI specified options and config file options.
+
+This holds the logic that uses the collected and merged config files and
+applies the user-specified command-line configuration on top of it.
+"""
+import logging
+
+from flake8 import utils
+from flake8.options import config
+
+LOG = logging.getLogger(__name__)
+
+
+def aggregate_options(manager, arglist=None, values=None):
+ """Aggregate and merge CLI and config file options.
+
+ :param flake8.option.manager.OptionManager manager:
+ The instance of the OptionManager that we're presently using.
+ :param list arglist:
+ The list of arguments to pass to ``manager.parse_args``. In most cases
+ this will be None so ``parse_args`` uses ``sys.argv``. This is mostly
+ available to make testing easier.
+ :param optparse.Values values:
+ Previously parsed set of parsed options.
+ :returns:
+ Tuple of the parsed options and extra arguments returned by
+ ``manager.parse_args``.
+ :rtype:
+ tuple(optparse.Values, list)
+ """
+ # Get defaults from the option parser
+ default_values, _ = manager.parse_args([], values=values)
+ # Get original CLI values so we can find additional config file paths and
+ # see if --config was specified.
+ original_values, original_args = manager.parse_args(arglist)
+ extra_config_files = utils.normalize_paths(original_values.append_config)
+
+ # Make our new configuration file mergerator
+ config_parser = config.MergedConfigParser(
+ option_manager=manager,
+ extra_config_files=extra_config_files,
+ args=original_args,
+ )
+
+ # Get the parsed config
+ parsed_config = config_parser.parse(original_values.config,
+ original_values.isolated)
+
+ # Extend the default ignore value with the extended default ignore list,
+ # registered by plugins.
+ extended_default_ignore = manager.extended_default_ignore.copy()
+ LOG.debug('Extended default ignore list: %s',
+ list(extended_default_ignore))
+ extended_default_ignore.update(default_values.ignore)
+ default_values.ignore = list(extended_default_ignore)
+ LOG.debug('Merged default ignore list: %s', default_values.ignore)
+
+ # Merge values parsed from config onto the default values returned
+ for config_name, value in parsed_config.items():
+ dest_name = config_name
+ # If the config name is somehow different from the destination name,
+ # fetch the destination name from our Option
+ if not hasattr(default_values, config_name):
+ dest_name = config_parser.config_options[config_name].dest
+
+ LOG.debug('Overriding default value of (%s) for "%s" with (%s)',
+ getattr(default_values, dest_name, None),
+ dest_name,
+ value)
+ # Override the default values with the config values
+ setattr(default_values, dest_name, value)
+
+ # Finally parse the command-line options
+ return manager.parse_args(arglist, default_values)
diff --git a/src/flake8/options/config.py b/src/flake8/options/config.py
new file mode 100644
index 0000000..48719a8
--- /dev/null
+++ b/src/flake8/options/config.py
@@ -0,0 +1,279 @@
+"""Config handling logic for Flake8."""
+import configparser
+import logging
+import os.path
+import sys
+
+LOG = logging.getLogger(__name__)
+
+__all__ = ('ConfigFileFinder', 'MergedConfigParser')
+
+
+class ConfigFileFinder(object):
+ """Encapsulate the logic for finding and reading config files."""
+
+ PROJECT_FILENAMES = ('setup.cfg', 'tox.ini')
+
+ def __init__(self, program_name, args, extra_config_files):
+ """Initialize object to find config files.
+
+ :param str program_name:
+ Name of the current program (e.g., flake8).
+ :param list args:
+ The extra arguments passed on the command-line.
+ :param list extra_config_files:
+ Extra configuration files specified by the user to read.
+ """
+ # The values of --append-config from the CLI
+ extra_config_files = extra_config_files or []
+ self.extra_config_files = [
+ # Ensure the paths are absolute paths for local_config_files
+ os.path.abspath(f) for f in extra_config_files
+ ]
+
+ # Platform specific settings
+ self.is_windows = sys.platform == 'win32'
+ self.xdg_home = os.environ.get('XDG_CONFIG_HOME',
+ os.path.expanduser('~/.config'))
+
+ # Look for '.<program_name>' files
+ self.program_config = '.' + program_name
+ self.program_name = program_name
+
+ # List of filenames to find in the local/project directory
+ self.project_filenames = ('setup.cfg', 'tox.ini', self.program_config)
+
+ self.local_directory = os.path.abspath(os.curdir)
+
+ if not args:
+ args = ['.']
+ self.parent = self.tail = os.path.abspath(os.path.commonprefix(args))
+
+ @staticmethod
+ def _read_config(files):
+ config = configparser.RawConfigParser()
+ try:
+ found_files = config.read(files)
+ except configparser.ParsingError:
+ LOG.exception("There was an error trying to parse a config "
+ "file. The files we were attempting to parse "
+ "were: %r", files)
+ found_files = []
+ return (config, found_files)
+
+ def cli_config(self, files):
+ """Read and parse the config file specified on the command-line."""
+ config, found_files = self._read_config(files)
+ if found_files:
+ LOG.debug('Found cli configuration files: %s', found_files)
+ return config
+
+ def generate_possible_local_files(self):
+ """Find and generate all local config files."""
+ tail = self.tail
+ parent = self.parent
+ local_dir = self.local_directory
+ while tail:
+ for project_filename in self.project_filenames:
+ filename = os.path.abspath(os.path.join(parent,
+ project_filename))
+ yield filename
+ if parent == local_dir:
+ break
+ (parent, tail) = os.path.split(parent)
+
+ def local_config_files(self):
+ """Find all local config files which actually exist.
+
+ Filter results from
+ :meth:`~ConfigFileFinder.generate_possible_local_files` based
+ on whether the filename exists or not.
+
+ :returns:
+ List of files that exist that are local project config files with
+ extra config files appended to that list (which also exist).
+ :rtype:
+ [str]
+ """
+ exists = os.path.exists
+ return [
+ filename
+ for filename in self.generate_possible_local_files()
+ if os.path.exists(filename)
+ ] + [f for f in self.extra_config_files if exists(f)]
+
+ def local_configs(self):
+ """Parse all local config files into one config object."""
+ config, found_files = self._read_config(self.local_config_files())
+ if found_files:
+ LOG.debug('Found local configuration files: %s', found_files)
+ return config
+
+ def user_config_file(self):
+ """Find the user-level config file."""
+ if self.is_windows:
+ return os.path.expanduser('~\\' + self.program_config)
+ return os.path.join(self.xdg_home, self.program_name)
+
+ def user_config(self):
+ """Parse the user config file into a config object."""
+ config, found_files = self._read_config(self.user_config_file())
+ if found_files:
+ LOG.debug('Found user configuration files: %s', found_files)
+ return config
+
+
+class MergedConfigParser(object):
+ """Encapsulate merging different types of configuration files.
+
+ This parses out the options registered that were specified in the
+ configuration files, handles extra configuration files, and returns
+ dictionaries with the parsed values.
+ """
+
+ #: Set of types that should use the
+ #: :meth:`~configparser.RawConfigParser.getint` method.
+ GETINT_TYPES = set(['int', 'count'])
+ #: Set of actions that should use the
+ #: :meth:`~configparser.RawConfigParser.getbool` method.
+ GETBOOL_ACTIONS = set(['store_true', 'store_false'])
+
+ def __init__(self, option_manager, extra_config_files=None, args=None):
+ """Initialize the MergedConfigParser instance.
+
+ :param flake8.option.manager.OptionManager option_manager:
+ Initialized OptionManager.
+ :param list extra_config_files:
+ List of extra config files to parse.
+ :params list args:
+ The extra parsed arguments from the command-line.
+ """
+ #: Our instance of flake8.options.manager.OptionManager
+ self.option_manager = option_manager
+ #: The prog value for the cli parser
+ self.program_name = option_manager.program_name
+ #: Parsed extra arguments
+ self.args = args
+ #: Mapping of configuration option names to
+ #: :class:`~flake8.options.manager.Option` instances
+ self.config_options = option_manager.config_options_dict
+ #: List of extra config files
+ self.extra_config_files = extra_config_files or []
+ #: Our instance of our :class:`~ConfigFileFinder`
+ self.config_finder = ConfigFileFinder(self.program_name, self.args,
+ self.extra_config_files)
+
+ @staticmethod
+ def _normalize_value(option, value):
+ final_value = option.normalize(value)
+ LOG.debug('%r has been normalized to %r for option "%s"',
+ value, final_value, option.config_name)
+ return final_value
+
+ def _parse_config(self, config_parser):
+ config_dict = {}
+ for option_name in config_parser.options(self.program_name):
+ if option_name not in self.config_options:
+ LOG.debug('Option "%s" is not registered. Ignoring.',
+ option_name)
+ continue
+ option = self.config_options[option_name]
+
+ # Use the appropriate method to parse the config value
+ method = config_parser.get
+ if option.type in self.GETINT_TYPES:
+ method = config_parser.getint
+ elif option.action in self.GETBOOL_ACTIONS:
+ method = config_parser.getboolean
+
+ value = method(self.program_name, option_name)
+ LOG.debug('Option "%s" returned value: %r', option_name, value)
+
+ final_value = self._normalize_value(option, value)
+ config_dict[option_name] = final_value
+
+ return config_dict
+
+ def is_configured_by(self, config):
+ """Check if the specified config parser has an appropriate section."""
+ return config.has_section(self.program_name)
+
+ def parse_local_config(self):
+ """Parse and return the local configuration files."""
+ config = self.config_finder.local_configs()
+ if not self.is_configured_by(config):
+ LOG.debug('Local configuration files have no %s section',
+ self.program_name)
+ return {}
+
+ LOG.debug('Parsing local configuration files.')
+ return self._parse_config(config)
+
+ def parse_user_config(self):
+ """Parse and return the user configuration files."""
+ config = self.config_finder.user_config()
+ if not self.is_configured_by(config):
+ LOG.debug('User configuration files have no %s section',
+ self.program_name)
+ return {}
+
+ LOG.debug('Parsing user configuration files.')
+ return self._parse_config(config)
+
+ def parse_cli_config(self, config_path):
+ """Parse and return the file specified by --config."""
+ config = self.config_finder.cli_config(config_path)
+ if not self.is_configured_by(config):
+ LOG.debug('CLI configuration files have no %s section',
+ self.program_name)
+ return {}
+
+ LOG.debug('Parsing CLI configuration files.')
+ return self._parse_config(config)
+
+ def merge_user_and_local_config(self):
+ """Merge the parsed user and local configuration files.
+
+ :returns:
+ Dictionary of the parsed and merged configuration options.
+ :rtype:
+ dict
+ """
+ user_config = self.parse_user_config()
+ config = self.parse_local_config()
+
+ for option, value in user_config.items():
+ config.setdefault(option, value)
+
+ return config
+
+ def parse(self, cli_config=None, isolated=False):
+ """Parse and return the local and user config files.
+
+ First this copies over the parsed local configuration and then
+ iterates over the options in the user configuration and sets them if
+ they were not set by the local configuration file.
+
+ :param str cli_config:
+ Value of --config when specified at the command-line. Overrides
+ all other config files.
+ :param bool isolated:
+ Determines if we should parse configuration files at all or not.
+ If running in isolated mode, we ignore all configuration files
+ :returns:
+ Dictionary of parsed configuration options
+ :rtype:
+ dict
+ """
+ if isolated:
+ LOG.debug('Refusing to parse configuration files due to user-'
+ 'requested isolation')
+ return {}
+
+ if cli_config:
+ LOG.debug('Ignoring user and locally found configuration files. '
+ 'Reading only configuration from "%s" specified via '
+ '--config by the user', cli_config)
+ return self.parse_cli_config(cli_config)
+
+ return self.merge_user_and_local_config()
diff --git a/src/flake8/options/manager.py b/src/flake8/options/manager.py
new file mode 100644
index 0000000..439cba2
--- /dev/null
+++ b/src/flake8/options/manager.py
@@ -0,0 +1,256 @@
+"""Option handling and Option management logic."""
+import logging
+import optparse # pylint: disable=deprecated-module
+
+from flake8 import utils
+
+LOG = logging.getLogger(__name__)
+
+
+class Option(object):
+ """Our wrapper around an optparse.Option object to add features."""
+
+ def __init__(self, short_option_name=None, long_option_name=None,
+ # Options below here are taken from the optparse.Option class
+ action=None, default=None, type=None, dest=None,
+ nargs=None, const=None, choices=None, callback=None,
+ callback_args=None, callback_kwargs=None, help=None,
+ metavar=None,
+ # Options below here are specific to Flake8
+ parse_from_config=False, comma_separated_list=False,
+ normalize_paths=False):
+ """Initialize an Option instance wrapping optparse.Option.
+
+ The following are all passed directly through to optparse.
+
+ :param str short_option_name:
+ The short name of the option (e.g., ``-x``). This will be the
+ first argument passed to :class:`~optparse.Option`.
+ :param str long_option_name:
+ The long name of the option (e.g., ``--xtra-long-option``). This
+ will be the second argument passed to :class:`~optparse.Option`.
+ :param str action:
+ Any action allowed by :mod:`optparse`.
+ :param default:
+ Default value of the option.
+ :param type:
+ Any type allowed by :mod:`optparse`.
+ :param dest:
+ Attribute name to store parsed option value as.
+ :param nargs:
+ Number of arguments to parse for this option.
+ :param const:
+ Constant value to store on a common destination. Usually used in
+ conjuntion with ``action="store_const"``.
+ :param iterable choices:
+ Possible values for the option.
+ :param callable callback:
+ Callback used if the action is ``"callback"``.
+ :param iterable callback_args:
+ Additional positional arguments to the callback callable.
+ :param dictionary callback_kwargs:
+ Keyword arguments to the callback callable.
+ :param str help:
+ Help text displayed in the usage information.
+ :param str metavar:
+ Name to use instead of the long option name for help text.
+
+ The following parameters are for Flake8's option handling alone.
+
+ :param bool parse_from_config:
+ Whether or not this option should be parsed out of config files.
+ :param bool comma_separated_list:
+ Whether the option is a comma separated list when parsing from a
+ config file.
+ :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.
+ """
+ self.short_option_name = short_option_name
+ self.long_option_name = long_option_name
+ self.option_args = [
+ x for x in (short_option_name, long_option_name) if x is not None
+ ]
+ self.option_kwargs = {
+ 'action': action,
+ 'default': default,
+ 'type': type,
+ 'dest': self._make_dest(dest),
+ 'nargs': nargs,
+ 'const': const,
+ 'choices': choices,
+ 'callback': callback,
+ 'callback_args': callback_args,
+ 'callback_kwargs': callback_kwargs,
+ 'help': help,
+ 'metavar': metavar,
+ }
+ # Set attributes for our option arguments
+ for key, value in self.option_kwargs.items():
+ setattr(self, key, value)
+
+ # Set our custom attributes
+ self.parse_from_config = parse_from_config
+ self.comma_separated_list = comma_separated_list
+ self.normalize_paths = normalize_paths
+
+ self.config_name = None
+ if parse_from_config:
+ if not long_option_name:
+ raise ValueError('When specifying parse_from_config=True, '
+ 'a long_option_name must also be specified.')
+ self.config_name = long_option_name[2:].replace('-', '_')
+
+ self._opt = None
+
+ def __repr__(self):
+ """Simple representation of an Option class."""
+ return (
+ 'Option({0}, {1}, action={action}, default={default}, '
+ 'dest={dest}, type={type}, callback={callback}, help={help},'
+ ' callback={callback}, callback_args={callback_args}, '
+ 'callback_kwargs={callback_kwargs}, metavar={metavar})'
+ ).format(self.short_option_name, self.long_option_name,
+ **self.option_kwargs)
+
+ def _make_dest(self, dest):
+ if dest:
+ return dest
+
+ if self.long_option_name:
+ return self.long_option_name[2:].replace('-', '_')
+ return self.short_option_name[1]
+
+ def normalize(self, value):
+ """Normalize the value based on the option configuration."""
+ if self.normalize_paths:
+ # Decide whether to parse a list of paths or a single path
+ normalize = utils.normalize_path
+ if self.comma_separated_list:
+ normalize = utils.normalize_paths
+ return normalize(value)
+ elif self.comma_separated_list:
+ return utils.parse_comma_separated_list(value)
+ return value
+
+ def to_optparse(self):
+ """Convert a Flake8 Option to an optparse Option."""
+ if self._opt is None:
+ self._opt = optparse.Option(*self.option_args,
+ **self.option_kwargs)
+ return self._opt
+
+
+class OptionManager(object):
+ """Manage Options and OptionParser while adding post-processing."""
+
+ def __init__(self, prog=None, version=None,
+ usage='%prog [options] file file ...'):
+ """Initialize an instance of an OptionManager.
+
+ :param str prog:
+ Name of the actual program (e.g., flake8).
+ :param str version:
+ Version string for the program.
+ :param str usage:
+ Basic usage string used by the OptionParser.
+ """
+ self.parser = optparse.OptionParser(prog=prog, version=version,
+ usage=usage)
+ self.config_options_dict = {}
+ self.options = []
+ self.program_name = prog
+ self.version = version
+ self.registered_plugins = set()
+ self.extended_default_ignore = set()
+
+ @staticmethod
+ def format_plugin(plugin_tuple):
+ """Convert a plugin tuple into a dictionary mapping name to value."""
+ return dict(zip(["name", "version"], plugin_tuple))
+
+ def add_option(self, *args, **kwargs):
+ """Create and register a new option.
+
+ See parameters for :class:`~flake8.options.manager.Option` for
+ acceptable arguments to this method.
+
+ .. note::
+
+ ``short_option_name`` and ``long_option_name`` may be specified
+ positionally as they are with optparse normally.
+ """
+ if len(args) == 1 and args[0].startswith('--'):
+ args = (None, args[0])
+ option = Option(*args, **kwargs)
+ self.parser.add_option(option.to_optparse())
+ self.options.append(option)
+ if option.parse_from_config:
+ self.config_options_dict[option.config_name] = option
+ LOG.debug('Registered option "%s".', option)
+
+ def remove_from_default_ignore(self, error_codes):
+ """Remove specified error codes from the default ignore list.
+
+ :param list error_codes:
+ List of strings that are the error/warning codes to attempt to
+ remove from the extended default ignore list.
+ """
+ LOG.debug('Removing %r from the default ignore list', error_codes)
+ for error_code in error_codes:
+ try:
+ self.extend_default_ignore.remove(error_code)
+ except ValueError:
+ LOG.debug('Attempted to remove %s from default ignore'
+ ' but it was not a member of the list.', error_code)
+
+ def extend_default_ignore(self, error_codes):
+ """Extend the default ignore list with the error codes provided.
+
+ :param list error_codes:
+ List of strings that are the error/warning codes with which to
+ extend the default ignore list.
+ """
+ LOG.debug('Extending default ignore list with %r', error_codes)
+ self.extended_default_ignore.update(error_codes)
+
+ def generate_versions(self, format_str='%(name)s: %(version)s'):
+ """Generate a comma-separated list of versions of plugins."""
+ return ', '.join(
+ format_str % self.format_plugin(plugin)
+ for plugin in self.registered_plugins
+ )
+
+ def update_version_string(self):
+ """Update the flake8 version string."""
+ self.parser.version = (self.version + ' (' +
+ self.generate_versions() + ')')
+
+ def generate_epilog(self):
+ """Create an epilog with the version and name of each of plugin."""
+ plugin_version_format = '%(name)s: %(version)s'
+ self.parser.epilog = 'Installed plugins: ' + self.generate_versions(
+ plugin_version_format
+ )
+
+ def parse_args(self, args=None, values=None):
+ """Simple proxy to calling the OptionParser's parse_args method."""
+ self.generate_epilog()
+ self.update_version_string()
+ options, xargs = self.parser.parse_args(args, values)
+ for option in self.options:
+ old_value = getattr(options, option.dest)
+ setattr(options, option.dest, option.normalize(old_value))
+
+ return options, xargs
+
+ def register_plugin(self, name, version):
+ """Register a plugin relying on the OptionManager.
+
+ :param str name:
+ The name of the checker itself. This will be the ``name``
+ attribute of the class or function loaded from the entry-point.
+ :param str version:
+ The version of the checker that we're using.
+ """
+ self.registered_plugins.add((name, version))