diff options
Diffstat (limited to 'src/flake8/options')
| -rw-r--r-- | src/flake8/options/__init__.py | 12 | ||||
| -rw-r--r-- | src/flake8/options/aggregator.py | 74 | ||||
| -rw-r--r-- | src/flake8/options/config.py | 279 | ||||
| -rw-r--r-- | src/flake8/options/manager.py | 256 |
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)) |
