diff options
-rw-r--r-- | coverage/tomlconfig.py | 159 | ||||
-rw-r--r-- | tests/test_config.py | 27 |
2 files changed, 103 insertions, 83 deletions
diff --git a/coverage/tomlconfig.py b/coverage/tomlconfig.py index f5978820..cf1d82b1 100644 --- a/coverage/tomlconfig.py +++ b/coverage/tomlconfig.py @@ -26,13 +26,11 @@ class TomlConfigParser: def __init__(self, our_file): self.our_file = our_file - self.getters = [lambda obj: obj['tool']['coverage']] - if self.our_file: - self.getters.append(lambda obj: obj) - - self._data = [] + self.data = None def read(self, filenames): + from coverage.optional import toml + # RawConfigParser takes a filename or list of filenames, but we only # ever call this with a single filename. assert isinstance(filenames, path_types) @@ -40,130 +38,137 @@ class TomlConfigParser: if env.PYVERSION >= (3, 6): filename = os.fspath(filename) - from coverage.optional import toml - if toml is None: - if self.our_file: - raise CoverageException("Can't read {!r} without TOML support".format(filename)) - try: with io.open(filename, encoding='utf-8') as fp: - toml_data = fp.read() - toml_data = substitute_variables(toml_data, os.environ) - if toml: - try: - self._data.append(toml.loads(toml_data)) - except toml.TomlDecodeError as err: - raise TomlDecodeError(*err.args) - elif re.search(r"^\[tool\.coverage\.", toml_data, flags=re.MULTILINE): - # Looks like they meant to read TOML, but we can't. - raise CoverageException("Can't read {!r} without TOML support".format(filename)) - else: - return [] + toml_text = fp.read() except IOError: return [] - return [filename] + if toml: + toml_text = substitute_variables(toml_text, os.environ) + try: + self.data = toml.loads(toml_text) + except toml.TomlDecodeError as err: + raise TomlDecodeError(*err.args) + return [filename] + else: + has_toml = re.search(r"^\[tool\.coverage\.", toml_text, flags=re.MULTILINE) + if self.our_file or has_toml: + # Looks like they meant to read TOML, but we can't read it. + msg = "Can't read {!r} without TOML support. Install with [toml] extra" + raise CoverageException(msg.format(filename)) + return [] + + def _get_section(self, section): + """Get a section from the data. + + Arguments: + section (str): A section name, which can be dotted. + + Returns: + name (str): the actual name of the section that was found, if any, + or None. + data (str): the dict of data in the section, or None if not found. + + """ + prefixes = ["tool.coverage."] + if self.our_file: + prefixes.append("") + for prefix in prefixes: + real_section = prefix + section + parts = real_section.split(".") + try: + data = self.data[parts[0]] + for part in parts[1:]: + data = data[part] + except KeyError: + continue + break + else: + return None, None + return real_section, data + + def _get(self, section, option): + """Like .get, but returns the real section name and the value.""" + name, data = self._get_section(section) + if data is None: + raise configparser.NoSectionError(section) + try: + return name, data[option] + except KeyError: + raise configparser.NoOptionError(option, name) def has_option(self, section, option): - for data in self._data: - for getter in self.getters: - try: - getter(data)[section][option] - except KeyError: - continue - return True - return False + _, data = self._get_section(section) + if data is None: + return False + return option in data def has_section(self, section): - for data in self._data: - for getter in self.getters: - try: - getter(data)[section] - except KeyError: - continue - return section - return False + name, _ = self._get_section(section) + return name def options(self, section): - for data in self._data: - for getter in self.getters: - try: - section = getter(data)[section] - except KeyError: - continue - return list(section.keys()) - raise configparser.NoSectionError(section) + _, data = self._get_section(section) + if data is None: + raise configparser.NoSectionError(section) + return list(data.keys()) def get_section(self, section): - d = {} - for opt in self.options(section): - d[opt] = self.get(section, opt) - return d + _, data = self._get_section(section) + return data def get(self, section, option): - found_section = False - for data in self._data: - for getter in self.getters: - try: - section = getter(data)[section] - except KeyError: - continue - - found_section = True - try: - value = section[option] - except KeyError: - continue - return value - if not found_section: - raise configparser.NoSectionError(section) - raise configparser.NoOptionError(option, section) + _, value = self._get(section, option) + return value def getboolean(self, section, option): - value = self.get(section, option) + name, value = self._get(section, option) if not isinstance(value, bool): raise ValueError( 'Option {!r} in section {!r} is not a boolean: {!r}' - .format(option, section, value) + .format(option, name, value) ) return value def getlist(self, section, option): - values = self.get(section, option) + name, values = self._get(section, option) if not isinstance(values, list): raise ValueError( 'Option {!r} in section {!r} is not a list: {!r}' - .format(option, section, values) + .format(option, name, values) ) return values def getregexlist(self, section, option): + # Use getlist for list-checking. values = self.getlist(section, option) + name, values = self._get(section, option) for value in values: value = value.strip() try: re.compile(value) except re.error as e: raise CoverageException( - "Invalid [%s].%s value %r: %s" % (section, option, value, e) + "Invalid [%s].%s value %r: %s" % (name, option, value, e) ) return values def getint(self, section, option): - value = self.get(section, option) + name, value = self._get(section, option) if not isinstance(value, int): raise ValueError( 'Option {!r} in section {!r} is not an integer: {!r}' - .format(option, section, value) + .format(option, name, value) ) return value def getfloat(self, section, option): - value = self.get(section, option) + name, value = self._get(section, option) if isinstance(value, int): value = float(value) if not isinstance(value, float): raise ValueError( 'Option {!r} in section {!r} is not a float: {!r}' - .format(option, section, value) + .format(option, name, value) ) return value diff --git a/tests/test_config.py b/tests/test_config.py index 74ff5f00..0f9a4929 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -65,17 +65,25 @@ class ConfigTest(CoverageTest): concurrency = ["a", "b"] timid = true data_file = ".hello_kitty.data" + plugins = ["plugins.a_plugin"] [tool.coverage.report] precision = 3 fail_under = 90.5 + [tool.coverage.plugins.a_plugin] + hello = "world" """) cov = coverage.Coverage(config_file="pyproject.toml") self.assertTrue(cov.config.timid) self.assertFalse(cov.config.branch) self.assertEqual(cov.config.concurrency, ["a", "b"]) self.assertEqual(cov.config.data_file, ".hello_kitty.data") + self.assertEqual(cov.config.plugins, ["plugins.a_plugin"]) self.assertEqual(cov.config.precision, 3) self.assertAlmostEqual(cov.config.fail_under, 90.5) + self.assertEqual( + cov.config.get_plugin_options("plugins.a_plugin"), + {'hello': 'world'} + ) # Test that our class doesn't reject integers when loading floats self.make_file("pyproject.toml", """\ @@ -153,7 +161,7 @@ class ConfigTest(CoverageTest): # Im-parsable values raise CoverageException, with details. bad_configs_and_msgs = [ ("[run]\ntimid = maybe?\n", r"maybe[?]"), - ("timid = 1\n", r"timid = 1"), + ("timid = 1\n", r"no section headers"), ("[run\n", r"\[run"), ("[report]\nexclude_lines = foo(\n", r"Invalid \[report\].exclude_lines value 'foo\(': " @@ -177,16 +185,15 @@ class ConfigTest(CoverageTest): # Im-parsable values raise CoverageException, with details. bad_configs_and_msgs = [ ("[tool.coverage.run]\ntimid = \"maybe?\"\n", r"maybe[?]"), - # ("timid = 1\n", r"timid = 1"), ("[tool.coverage.run\n", r"Key group"), ('[tool.coverage.report]\nexclude_lines = ["foo("]\n', - r"Invalid \[report\].exclude_lines value u?'foo\(': " + r"Invalid \[tool.coverage.report\].exclude_lines value u?'foo\(': " r"(unbalanced parenthesis|missing \))"), ('[tool.coverage.report]\npartial_branches = ["foo["]\n', - r"Invalid \[report\].partial_branches value u?'foo\[': " + r"Invalid \[tool.coverage.report\].partial_branches value u?'foo\[': " r"(unexpected end of regular expression|unterminated character set)"), ('[tool.coverage.report]\npartial_branches_always = ["foo***"]\n', - r"Invalid \[report\].partial_branches_always value " + r"Invalid \[tool.coverage.report\].partial_branches_always value " r"u?'foo\*\*\*': " r"multiple repeat"), ('[tool.coverage.run]\nconcurrency="foo"', "not a list"), @@ -367,7 +374,7 @@ class ConfigTest(CoverageTest): [tool.coverage.run] xyzzy = 17 """) - msg = r"Unrecognized option '\[run\] xyzzy=' in config file pyproject.toml" + msg = r"Unrecognized option '\[tool.coverage.run\] xyzzy=' in config file pyproject.toml" with self.assertRaisesRegex(CoverageException, msg): _ = coverage.Coverage() @@ -653,8 +660,16 @@ class ConfigFileTest(UsingModulesMixin, CoverageTest): self.assertFalse(cov.config.branch) self.assertEqual(cov.config.data_file, ".coverage") + def test_no_toml_installed_no_toml(self): + # Can't read a toml file that doesn't exist. + with coverage.optional.without('toml'): + msg = "Couldn't read 'cov.toml' as a config file" + with self.assertRaisesRegex(CoverageException, msg): + coverage.Coverage(config_file="cov.toml") + def test_no_toml_installed_explicit_toml(self): # Can't specify a toml config file if toml isn't installed. + self.make_file("cov.toml", "# A toml file!") with coverage.optional.without('toml'): msg = "Can't read 'cov.toml' without TOML support" with self.assertRaisesRegex(CoverageException, msg): |