--- a/DebugClients/Python/coverage/config.py Sat Apr 07 13:17:06 2018 +0200 +++ b/DebugClients/Python/coverage/config.py Sat Apr 07 13:35:10 2018 +0200 @@ -9,7 +9,7 @@ import sys from coverage.backward import configparser, iitems, string_class -from coverage.misc import CoverageException, isolate_module +from coverage.misc import contract, CoverageException, isolate_module os = isolate_module(os) @@ -17,28 +17,48 @@ class HandyConfigParser(configparser.RawConfigParser): """Our specialization of ConfigParser.""" - def __init__(self, section_prefix): + def __init__(self, our_file): + """Create the HandyConfigParser. + + `our_file` is True if this config file is specifically for coverage, + False if we are examining another config file (tox.ini, setup.cfg) + for possible settings. + """ + configparser.RawConfigParser.__init__(self) - self.section_prefix = section_prefix + self.section_prefixes = ["coverage:"] + if our_file: + self.section_prefixes.append("") - def read(self, filename): + def read(self, filenames): """Read a file name as UTF-8 configuration data.""" kwargs = {} if sys.version_info >= (3, 2): kwargs['encoding'] = "utf-8" - return configparser.RawConfigParser.read(self, filename, **kwargs) + return configparser.RawConfigParser.read(self, filenames, **kwargs) def has_option(self, section, option): - section = self.section_prefix + section - return configparser.RawConfigParser.has_option(self, section, option) + for section_prefix in self.section_prefixes: + real_section = section_prefix + section + has = configparser.RawConfigParser.has_option(self, real_section, option) + if has: + return has + return False def has_section(self, section): - section = self.section_prefix + section - return configparser.RawConfigParser.has_section(self, section) + for section_prefix in self.section_prefixes: + real_section = section_prefix + section + has = configparser.RawConfigParser.has_section(self, real_section) + if has: + return real_section + return False def options(self, section): - section = self.section_prefix + section - return configparser.RawConfigParser.options(self, section) + for section_prefix in self.section_prefixes: + real_section = section_prefix + section + if configparser.RawConfigParser.has_section(self, real_section): + return configparser.RawConfigParser.options(self, real_section) + raise configparser.NoSectionError def get_section(self, section): """Get the contents of a section, as a dictionary.""" @@ -47,7 +67,7 @@ d[opt] = self.get(section, opt) return d - def get(self, section, *args, **kwargs): + def get(self, section, option, *args, **kwargs): # pylint: disable=arguments-differ """Get a value, replacing environment variables also. The arguments are the same as `RawConfigParser.get`, but in the found @@ -57,8 +77,14 @@ Returns the finished value. """ - section = self.section_prefix + section - v = configparser.RawConfigParser.get(self, section, *args, **kwargs) + for section_prefix in self.section_prefixes: + real_section = section_prefix + section + if configparser.RawConfigParser.has_option(self, real_section, option): + break + else: + raise configparser.NoOptionError + + v = configparser.RawConfigParser.get(self, real_section, option, *args, **kwargs) def dollar_replace(m): """Called for each $replacement.""" # Only one of the groups will have matched, just get its text. @@ -122,12 +148,12 @@ # The default line exclusion regexes. DEFAULT_EXCLUDE = [ - r'(?i)#\s*pragma[:\s]?\s*no\s*cover', + r'#\s*(pragma|PRAGMA)[:\s]?\s*(no|NO)\s*(cover|COVER)', ] # The default partial branch regexes, to be modified by the user. DEFAULT_PARTIAL = [ - r'(?i)#\s*pragma[:\s]?\s*no\s*branch', + r'#\s*(pragma|PRAGMA)[:\s]?\s*(no|NO)\s*(branch|BRANCH)', ] # The default partial branch regexes, based on Python semantics. @@ -152,24 +178,31 @@ self.attempted_config_files = [] self.config_files = [] + # Defaults for [run] and [report] + self._include = None + self._omit = None + # Defaults for [run] self.branch = False self.concurrency = None self.cover_pylib = False self.data_file = ".coverage" self.debug = [] + self.disable_warnings = [] self.note = None self.parallel = False self.plugins = [] self.source = None + self.run_include = None + self.run_omit = None self.timid = False # Defaults for [report] self.exclude_list = DEFAULT_EXCLUDE[:] - self.fail_under = 0 + self.fail_under = 0.0 self.ignore_errors = False - self.include = None - self.omit = None + self.report_include = None + self.report_omit = None self.partial_always_list = DEFAULT_PARTIAL_ALWAYS[:] self.partial_list = DEFAULT_PARTIAL[:] self.precision = 0 @@ -191,7 +224,11 @@ # Options for plugins self.plugin_options = {} - MUST_BE_LIST = ["omit", "include", "debug", "plugins"] + MUST_BE_LIST = [ + "debug", "concurrency", "plugins", + "report_omit", "report_include", + "run_omit", "run_include", + ] def from_args(self, **kwargs): """Read config values from `kwargs`.""" @@ -201,17 +238,23 @@ v = [v] setattr(self, k, v) - def from_file(self, filename, section_prefix=""): + @contract(filename=str) + def from_file(self, filename, our_file): """Read configuration from a .rc file. `filename` is a file name to read. - Returns True or False, whether the file could be read. + `our_file` is True if this config file is specifically for coverage, + False if we are examining another config file (tox.ini, setup.cfg) + for possible settings. + + Returns True or False, whether the file could be read, and it had some + coverage.py settings in it. """ self.attempted_config_files.append(filename) - cp = HandyConfigParser(section_prefix) + cp = HandyConfigParser(our_file) try: files_read = cp.read(filename) except configparser.Error as err: @@ -221,9 +264,12 @@ self.config_files.extend(files_read) + any_set = False try: for option_spec in self.CONFIG_FILE_OPTIONS: - self._set_attr_from_config_option(cp, *option_spec) + was_set = self._set_attr_from_config_option(cp, *option_spec) + if was_set: + any_set = True except ValueError as err: raise CoverageException("Couldn't read config file %s: %s" % (filename, err)) @@ -234,13 +280,12 @@ all_options[section].add(option) for section, options in iitems(all_options): - if cp.has_section(section): + real_section = cp.has_section(section) + if real_section: for unknown in set(cp.options(section)) - options: - if section_prefix: - section = section_prefix + section raise CoverageException( "Unrecognized option '[%s] %s=' in config file %s" % ( - section, unknown, filename + real_section, unknown, filename ) ) @@ -248,13 +293,21 @@ if cp.has_section('paths'): for option in cp.options('paths'): self.paths[option] = cp.getlist('paths', option) + any_set = True # plugins can have options for plugin in self.plugins: if cp.has_section(plugin): self.plugin_options[plugin] = cp.get_section(plugin) + any_set = True - return True + # Was this file used as a config file? If it's specifically our file, + # then it was used. If we're piggybacking on someone else's file, + # then it was only used if we found some settings in it. + if our_file: + return True + else: + return any_set CONFIG_FILE_OPTIONS = [ # These are *args for _set_attr_from_config_option: @@ -267,29 +320,31 @@ # [run] ('branch', 'run:branch', 'boolean'), - ('concurrency', 'run:concurrency'), + ('concurrency', 'run:concurrency', 'list'), ('cover_pylib', 'run:cover_pylib', 'boolean'), ('data_file', 'run:data_file'), ('debug', 'run:debug', 'list'), - ('include', 'run:include', 'list'), + ('disable_warnings', 'run:disable_warnings', 'list'), ('note', 'run:note'), - ('omit', 'run:omit', 'list'), ('parallel', 'run:parallel', 'boolean'), ('plugins', 'run:plugins', 'list'), + ('run_include', 'run:include', 'list'), + ('run_omit', 'run:omit', 'list'), ('source', 'run:source', 'list'), ('timid', 'run:timid', 'boolean'), # [report] ('exclude_list', 'report:exclude_lines', 'regexlist'), - ('fail_under', 'report:fail_under', 'int'), + ('fail_under', 'report:fail_under', 'float'), ('ignore_errors', 'report:ignore_errors', 'boolean'), - ('include', 'report:include', 'list'), - ('omit', 'report:omit', 'list'), ('partial_always_list', 'report:partial_branches_always', 'regexlist'), ('partial_list', 'report:partial_branches', 'regexlist'), ('precision', 'report:precision', 'int'), + ('report_include', 'report:include', 'list'), + ('report_omit', 'report:omit', 'list'), ('show_missing', 'report:show_missing', 'boolean'), ('skip_covered', 'report:skip_covered', 'boolean'), + ('sort', 'report:sort'), # [html] ('extra_css', 'html:extra_css'), @@ -302,11 +357,17 @@ ] def _set_attr_from_config_option(self, cp, attr, where, type_=''): - """Set an attribute on self if it exists in the ConfigParser.""" + """Set an attribute on self if it exists in the ConfigParser. + + Returns True if the attribute was set. + + """ section, option = where.split(":") if cp.has_option(section, option): method = getattr(cp, 'get' + type_) setattr(self, attr, method(section, option)) + return True + return False def get_plugin_options(self, plugin): """Get a dictionary of options for the plugin named `plugin`.""" @@ -349,7 +410,6 @@ Returns the value of the option. """ - # Check all the hard-coded options. for option_spec in self.CONFIG_FILE_OPTIONS: attr, where = option_spec[:2] @@ -363,3 +423,67 @@ # If we get here, we didn't find the option. raise CoverageException("No such option: %r" % option_name) + + +def read_coverage_config(config_file, **kwargs): + """Read the coverage.py configuration. + + Arguments: + config_file: a boolean or string, see the `Coverage` class for the + tricky details. + all others: keyword arguments from the `Coverage` class, used for + setting values in the configuration. + + Returns: + config_file, config: + config_file is the value to use for config_file in other + invocations of coverage. + + config is a CoverageConfig object read from the appropriate + configuration file. + + """ + # Build the configuration from a number of sources: + # 1) defaults: + config = CoverageConfig() + + # 2) from a file: + if config_file: + # Some API users were specifying ".coveragerc" to mean the same as + # True, so make it so. + if config_file == ".coveragerc": + config_file = True + specified_file = (config_file is not True) + if not specified_file: + config_file = ".coveragerc" + + for fname, our_file in [(config_file, True), + ("setup.cfg", False), + ("tox.ini", False)]: + config_read = config.from_file(fname, our_file=our_file) + is_config_file = fname == config_file + + if not config_read and is_config_file and specified_file: + raise CoverageException("Couldn't read '%s' as a config file" % fname) + + if config_read: + break + + # 3) from environment variables: + env_data_file = os.environ.get('COVERAGE_FILE') + if env_data_file: + config.data_file = env_data_file + debugs = os.environ.get('COVERAGE_DEBUG') + if debugs: + config.debug.extend(d.strip() for d in debugs.split(",")) + + # 4) from constructor arguments: + config.from_args(**kwargs) + + # Once all the config has been collected, there's a little post-processing + # to do. + config.data_file = os.path.expanduser(config.data_file) + config.html_dir = os.path.expanduser(config.html_dir) + config.xml_output = os.path.expanduser(config.xml_output) + + return config_file, config