Merge 4ac06da2f4
into f43ddd8acd
This commit is contained in:
commit
0ade1190bf
|
@ -0,0 +1 @@
|
||||||
|
Internal Refactoring for finding/loading config files.
|
|
@ -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:
|
||||||
|
|
|
@ -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, {}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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"),
|
||||||
|
|
Loading…
Reference in New Issue