From 67aff6d2ec0dbb558bcbee98e0c4a932d87f739c Mon Sep 17 00:00:00 2001 From: Ian Cordasco Date: Thu, 7 Jan 2016 09:37:34 -0600 Subject: [PATCH] Updates to config/options aggregation - Adds another flag to the Option class - Adds normalization to the config parsing - Removes dead code from _parse_config - Handles user specifying --append-config, --config, and --isolated but does not handle their mutual exclusion --- flake8/options/aggregator.py | 51 +++++++++++++++++++++ flake8/options/config.py | 88 ++++++++++++++++++++++++++++-------- flake8/options/manager.py | 27 +++++++++++ 3 files changed, 148 insertions(+), 18 deletions(-) create mode 100644 flake8/options/aggregator.py diff --git a/flake8/options/aggregator.py b/flake8/options/aggregator.py new file mode 100644 index 0000000..863fa05 --- /dev/null +++ b/flake8/options/aggregator.py @@ -0,0 +1,51 @@ +"""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.options import config +from flake8 import utils + +LOG = logging.getLogger(__name__) + + +def aggregate_options(manager, arglist=None, values=None): + """Function that aggregates the CLI and Config options.""" + # 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() + 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) + + # 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 + final_values, args = manager.parse_args(arglist, default_values) + return final_values, args diff --git a/flake8/options/config.py b/flake8/options/config.py index 708cd90..9317e7a 100644 --- a/flake8/options/config.py +++ b/flake8/options/config.py @@ -11,11 +11,16 @@ from . import utils LOG = logging.getLogger(__name__) +__all__('ConfigFileFinder', 'MergedConfigParser') + class ConfigFileFinder(object): PROJECT_FILENAMES = ('setup.cfg', 'tox.ini') - def __init__(self, program_name, args): + def __init__(self, program_name, args, extra_config_files): + # The values of --append-config from the CLI + self.extra_config_files = extra_config_files + # Platform specific settings self.is_windows = sys.platform == 'win32' self.xdg_home = os.environ.get('XDG_CONFIG_HOME', @@ -27,8 +32,6 @@ class ConfigFileFinder(object): # List of filenames to find in the local/project directory self.project_filenames = ('setup.cfg', 'tox.ini', self.program_config) - # List of filenames to find "globally" - self.global_filenames = (self.program_config,) self.local_directory = os.curdir @@ -42,6 +45,12 @@ class ConfigFileFinder(object): found_files = config.read(files) return (config, found_files) + def cli_config(self, files): + 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_config_files(self): tail = self.tail parent = self.parent @@ -57,7 +66,7 @@ class ConfigFileFinder(object): filename for filename in self.generate_possible_local_config_files() if os.path.exists(filename) - ] + ] + self.extra_config_files def local_configs(self): config, found_files = self._read_config(self.local_config_files()) @@ -81,20 +90,33 @@ class MergedConfigParser(object): GETINT_METHODS = set(['int', 'count']) GETBOOL_METHODS = set(['store_true', 'store_false']) - def __init__(self, option_manager, args=None): + def __init__(self, option_manager, extra_config_files=None, args=None): + # 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 + # Our instance of our ConfigFileFinder self.config_finder = ConfigFileFinder(self.program_name, self.args) + # Mapping of configuration option names to + # flake8.options.manager.Option instances self.config_options = option_manager.config_options_dict + self.extra_config_files = extra_config_files or [] + + @staticmethod + def _normalize_value(option, value): + if option.normalize_paths: + final_value = utils.normalize_paths(value) + elif option.comma_separated_list: + final_value = utils.parse_comma_separated_list(value) + else: + final_value = 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 = self.config_finder.local_configs() - if not config.has_section(self.program_name): - LOG.debug('Local configuration files have no %s section', - self.program_name) - return {} - config_dict = {} for option_name in config_parser.options(self.program_name): if option_name not in self.config_options: @@ -113,10 +135,7 @@ class MergedConfigParser(object): value = method(self.program_name, option_name) LOG.debug('Option "%s" returned value: %r', option_name, value) - final_value = value - if option.comma_separated_list: - final_value = utils.parse_comma_separated_list(value) - + final_value = self._normalize_value(value) config_dict[option_name] = final_value def is_configured_by(self, config): @@ -129,7 +148,7 @@ class MergedConfigParser(object): if not self.is_configured_by(config): LOG.debug('Local configuration files have no %s section', self.program_name) - return + return {} LOG.debug('Parsing local configuration files.') return self._parse_config(config) @@ -140,18 +159,51 @@ class MergedConfigParser(object): if not self.is_configured_by(config): LOG.debug('User configuration files have no %s section', self.program_name) - return + return {} LOG.debug('Parsing user configuration files.') return self._parse_config(config) - def parse(self): + 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 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) + user_config = self.parse_user_config() config = self.parse_local_config() diff --git a/flake8/options/manager.py b/flake8/options/manager.py index 4b32234..034444b 100644 --- a/flake8/options/manager.py +++ b/flake8/options/manager.py @@ -5,6 +5,7 @@ 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, @@ -13,6 +14,7 @@ class Option(object): 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. @@ -57,6 +59,9 @@ class Option(object): :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 @@ -72,11 +77,16 @@ class Option(object): '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, ' @@ -108,6 +118,16 @@ class OptionManager(object): self.version = version 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. + """ option = Option(*args, **kwargs) self.parser.add_option(option.to_optparse()) self.options.append(option) @@ -116,4 +136,11 @@ class OptionManager(object): LOG.debug('Registered option "%s".', option) def parse_args(self, args=None, values=None): + """Simple proxy to calling the OptionParser's parse_args method. + + .. todo:: + + Normalize values based on our extra attributes from + :class:`~flake8.options.manager.OptionManager`. + """ return self.parser.parse_args(args, values)