# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """TOML configuration support for coverage.py""" from __future__ import annotations import os import re from typing import Any, Callable, TypeVar from collections.abc import Iterable from coverage import config, env from coverage.exceptions import ConfigError from coverage.misc import import_third_party, isolate_module, substitute_variables from coverage.types import TConfigSectionOut, TConfigValueOut os = isolate_module(os) if env.PYVERSION >= (3, 11, 0, "alpha", 7): import tomllib # pylint: disable=import-error has_tomllib = True else: # TOML support on Python 3.10 and below is an install-time extra option. tomllib, has_tomllib = import_third_party("tomli") class TomlDecodeError(Exception): """An exception class that exists even when toml isn't installed.""" pass TWant = TypeVar("TWant") class TomlConfigParser: """TOML file reading with the interface of HandyConfigParser.""" # This class has the same interface as config.HandyConfigParser, no # need for docstrings. # pylint: disable=missing-function-docstring def __init__(self, our_file: bool) -> None: self.our_file = our_file self.data: dict[str, Any] = {} def read(self, filenames: Iterable[str]) -> list[str]: # RawConfigParser takes a filename or list of filenames, but we only # ever call this with a single filename. assert isinstance(filenames, (bytes, str, os.PathLike)) filename = os.fspath(filenames) try: with open(filename, encoding='utf-8') as fp: toml_text = fp.read() except OSError: return [] if has_tomllib: try: self.data = tomllib.loads(toml_text) except tomllib.TOMLDecodeError as err: raise TomlDecodeError(str(err)) from err 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 ConfigError(msg.format(filename)) return [] def _get_section(self, section: str) -> tuple[str | None, TConfigSectionOut | None]: """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."] 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: str, option: str) -> tuple[str, TConfigValueOut]: """Like .get, but returns the real section name and the value.""" name, data = self._get_section(section) if data is None: raise ConfigError(f"No section: {section!r}") assert name is not None try: value = data[option] except KeyError: raise ConfigError(f"No option {option!r} in section: {name!r}") from None return name, value def _get_single(self, section: str, option: str) -> Any: """Get a single-valued option. Performs environment substitution if the value is a string. Other types will be converted later as needed. """ name, value = self._get(section, option) if isinstance(value, str): value = substitute_variables(value, os.environ) return name, value def has_option(self, section: str, option: str) -> bool: _, data = self._get_section(section) if data is None: return False return option in data def real_section(self, section: str) -> str | None: name, _ = self._get_section(section) return name def has_section(self, section: str) -> bool: name, _ = self._get_section(section) return bool(name) def options(self, section: str) -> list[str]: _, data = self._get_section(section) if data is None: raise ConfigError(f"No section: {section!r}") return list(data.keys()) def get_section(self, section: str) -> TConfigSectionOut: _, data = self._get_section(section) return data or {} def get(self, section: str, option: str) -> Any: _, value = self._get_single(section, option) return value def _check_type( self, section: str, option: str, value: Any, type_: type[TWant], converter: Callable[[Any], TWant] | None, type_desc: str, ) -> TWant: """Check that `value` has the type we want, converting if needed. Returns the resulting value of the desired type. """ if isinstance(value, type_): return value if isinstance(value, str) and converter is not None: try: return converter(value) except Exception as e: raise ValueError( f"Option [{section}]{option} couldn't convert to {type_desc}: {value!r}", ) from e raise ValueError( f"Option [{section}]{option} is not {type_desc}: {value!r}", ) def getboolean(self, section: str, option: str) -> bool: name, value = self._get_single(section, option) bool_strings = {"true": True, "false": False} return self._check_type(name, option, value, bool, bool_strings.__getitem__, "a boolean") def getfile(self, section: str, option: str) -> str: _, value = self._get_single(section, option) return config.process_file_value(value) def _get_list(self, section: str, option: str) -> tuple[str, list[str]]: """Get a list of strings, substituting environment variables in the elements.""" name, values = self._get(section, option) values = self._check_type(name, option, values, list, None, "a list") values = [substitute_variables(value, os.environ) for value in values] return name, values def getlist(self, section: str, option: str) -> list[str]: _, values = self._get_list(section, option) return values def getregexlist(self, section: str, option: str) -> list[str]: name, values = self._get_list(section, option) return config.process_regexlist(name, option, values) def getint(self, section: str, option: str) -> int: name, value = self._get_single(section, option) return self._check_type(name, option, value, int, int, "an integer") def getfloat(self, section: str, option: str) -> float: name, value = self._get_single(section, option) if isinstance(value, int): value = float(value) return self._check_type(name, option, value, float, float, "a float")