This commit is contained in:
Ronny Pfannschmidt 2022-07-31 13:00:35 -03:00 committed by GitHub
commit 0ade1190bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 109 additions and 88 deletions

View File

@ -0,0 +1 @@
Internal Refactoring for finding/loading config files.

View File

@ -79,7 +79,7 @@ and can also be used to hold pytest configuration if they have a ``[pytest]`` se
.. code-block:: ini .. code-block:: ini
# tox.ini # tox.ini
[pytest] [tool:pytest]
minversion = 6.0 minversion = 6.0
addopts = -ra -q addopts = -ra -q
testpaths = testpaths =
@ -90,26 +90,7 @@ and can also be used to hold pytest configuration if they have a ``[pytest]`` se
setup.cfg setup.cfg
~~~~~~~~~ ~~~~~~~~~
``setup.cfg`` files are general purpose configuration files, used originally by :doc:`distutils <distutils/configfile>`, and can also be used to hold pytest configuration ``setup.cfg`` file usage for pytest has been deprecated, its recommended to use ``tox.ini`` or ``pyproject.toml``
if they have a ``[tool:pytest]`` section.
.. code-block:: ini
# setup.cfg
[tool:pytest]
minversion = 6.0
addopts = -ra -q
testpaths =
tests
integration
.. warning::
Usage of ``setup.cfg`` is not recommended unless for very simple use cases. ``.cfg``
files use a different parser than ``pytest.ini`` and ``tox.ini`` which might cause hard to track
down problems.
When possible, it is recommended to use the latter files, or ``pyproject.toml``, to hold your
pytest configuration.
.. _rootdir: .. _rootdir:

View File

@ -10,8 +10,6 @@ from typing import Tuple
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import Union from typing import Union
import iniconfig
from .exceptions import UsageError from .exceptions import UsageError
from _pytest.outcomes import fail from _pytest.outcomes import fail
from _pytest.pathlib import absolutepath from _pytest.pathlib import absolutepath
@ -19,42 +17,57 @@ from _pytest.pathlib import commonpath
if TYPE_CHECKING: if TYPE_CHECKING:
from . import Config from . import Config
from iniconfig import IniConfig # NOQA: F401
PARSE_RESULT = Optional[Dict[str, Union[str, List[str]]]]
def _parse_ini_config(path: Path) -> iniconfig.IniConfig: def _parse_ini_config(path: Path) -> "IniConfig":
"""Parse the given generic '.ini' file using legacy IniConfig parser, returning """Parse the given generic '.ini' file using legacy IniConfig parser, returning
the parsed object. the parsed object.
Raise UsageError if the file cannot be parsed. Raise UsageError if the file cannot be parsed.
""" """
from iniconfig import IniConfig, ParseError # NOQA: F811
try: try:
return iniconfig.IniConfig(str(path)) return IniConfig(os.fspath(path), data=path.read_text())
except iniconfig.ParseError as exc: except ParseError as exc:
raise UsageError(str(exc)) from exc raise UsageError(str(exc)) from exc
def load_config_dict_from_file( def _parse_pytest_ini(path: Path) -> PARSE_RESULT:
filepath: Path, """Parse the legacy pytest.ini and return the contents of the pytest section
) -> Optional[Dict[str, Union[str, List[str]]]]:
"""Load pytest configuration from the given file path, if supported.
Return None if the file does not contain valid pytest configuration. if the file exists and lacks a pytest section, consider it empty"""
""" iniconfig = _parse_ini_config(path)
# Configuration from ini files are obtained from the [pytest] section, if present.
if filepath.suffix == ".ini":
iniconfig = _parse_ini_config(filepath)
if "pytest" in iniconfig: if "pytest" in iniconfig:
return dict(iniconfig["pytest"].items()) return dict(iniconfig["pytest"].items())
else: else:
# "pytest.ini" files are always the source of configuration, even if empty. # "pytest.ini" files are always the source of configuration, even if empty.
if filepath.name == "pytest.ini":
return {} return {}
# '.cfg' files are considered if they contain a "[tool:pytest]" section.
elif filepath.suffix == ".cfg": def _parse_ini_file(path: Path) -> PARSE_RESULT:
iniconfig = _parse_ini_config(filepath) """Parses .ini files with expected pytest.ini sections
todo: investigate if tool:pytest should be added
"""
iniconfig = _parse_ini_config(path)
if "pytest" in iniconfig:
return dict(iniconfig["pytest"].items())
return None
def _parse_cfg_file(path: Path) -> PARSE_RESULT:
"""Parses .cfg files, specifically used for setup.cfg support
tool:pytest as section name is required
"""
iniconfig = _parse_ini_config(path)
if "tool:pytest" in iniconfig.sections: if "tool:pytest" in iniconfig.sections:
return dict(iniconfig["tool:pytest"].items()) return dict(iniconfig["tool:pytest"].items())
@ -62,9 +75,15 @@ def load_config_dict_from_file(
# If a setup.cfg contains a "[pytest]" section, we raise a failure to indicate users that # If a setup.cfg contains a "[pytest]" section, we raise a failure to indicate users that
# plain "[pytest]" sections in setup.cfg files is no longer supported (#3086). # plain "[pytest]" sections in setup.cfg files is no longer supported (#3086).
fail(CFG_PYTEST_SECTION.format(filename="setup.cfg"), pytrace=False) fail(CFG_PYTEST_SECTION.format(filename="setup.cfg"), pytrace=False)
else:
return None
def _parse_pyproject_ini_options(
filepath: Path,
) -> PARSE_RESULT:
"""Load backward compatible ini options from pyproject.toml"""
# '.toml' files are considered if they contain a [tool.pytest.ini_options] table.
elif filepath.suffix == ".toml":
if sys.version_info >= (3, 11): if sys.version_info >= (3, 11):
import tomllib import tomllib
else: else:
@ -85,34 +104,54 @@ def load_config_dict_from_file(
return v if isinstance(v, list) else str(v) return v if isinstance(v, list) else str(v)
return {k: make_scalar(v) for k, v in result.items()} return {k: make_scalar(v) for k, v in result.items()}
else:
return None
CONFIG_LOADERS = {
"pytest.ini": _parse_pytest_ini,
".pytest.ini": _parse_pytest_ini,
"pyproject.toml": _parse_pyproject_ini_options,
"tox.ini": _parse_ini_file,
"setup.cfg": _parse_cfg_file,
}
CONFIG_SUFFIXES = {
".ini": _parse_ini_file,
".cfg": _parse_cfg_file,
".toml": _parse_pyproject_ini_options,
}
def load_config_dict_from_file(path: Path) -> PARSE_RESULT:
"""Load pytest configuration from the given file path, if supported.
Return None if the file does not contain valid pytest configuration.
"""
if path.name in CONFIG_LOADERS:
return CONFIG_LOADERS[path.name](path)
if path.suffix in CONFIG_SUFFIXES:
return CONFIG_SUFFIXES[path.suffix](path)
return None return None
def locate_config( def locate_config(
args: Iterable[Path], args: List[Path],
) -> Tuple[Optional[Path], Optional[Path], Dict[str, Union[str, List[str]]]]: ) -> Tuple[Optional[Path], Optional[Path], Dict[str, Union[str, List[str]]]]:
"""Search in the list of arguments for a valid ini-file for pytest, """Search in the list of arguments for a valid ini-file for pytest,
and return a tuple of (rootdir, inifile, cfg-dict).""" and return a tuple of (rootdir, inifile, cfg-dict)."""
config_names = [
"pytest.ini",
".pytest.ini",
"pyproject.toml",
"tox.ini",
"setup.cfg",
]
args = [x for x in args if not str(x).startswith("-")]
if not args: if not args:
args = [Path.cwd()] args = [Path.cwd()]
for arg in args: for arg in args:
argpath = absolutepath(arg) argpath = absolutepath(arg)
for base in (argpath, *argpath.parents): for base in (argpath, *argpath.parents):
for config_name in config_names: for config_name, loader in CONFIG_LOADERS.items():
p = base / config_name p = base / config_name
if p.is_file(): if p.is_file():
ini_config = load_config_dict_from_file(p) config = loader(p)
if ini_config is not None: if config is not None:
return base, p, ini_config return base, p, config
return None, None, {} return None, None, {}

View File

@ -113,7 +113,7 @@ class TestParseIni:
@pytest.mark.parametrize( @pytest.mark.parametrize(
"section, name", "section, name",
[ [
("tool:pytest", "setup.cfg"), pytest.param("tool:pytest", "setup.cfg"),
("pytest", "tox.ini"), ("pytest", "tox.ini"),
("pytest", "pytest.ini"), ("pytest", "pytest.ini"),
("pytest", ".pytest.ini"), ("pytest", ".pytest.ini"),