/
niceSOFT
/
python3-pytest
Обзор
Документация
Войти
/
niceSOFT
/
python3-pytest
Код
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
testing/test_config.py
3 455 строк
116 KB
Ronny Pfannschmidt
Merge pull request #14807 from RonnyPfannschmidt/refactor/config-file-dispatch
06 авг 2026, 15:47
Не верифицирован
06 авг 2026, 15:47
28e86a6
Код
Авторство
О чём код?
# mypy: allow-untyped-defs from __future__ import annotations from collections.abc import Sequence import dataclasses import importlib.metadata import os from pathlib import Path import re import sys import textwrap from typing import Any from typing import Literal import _pytest._code from _pytest.config import _get_plugin_specs_as_list from _pytest.config import _get_prog_name from _pytest.config import _iter_rewritable_modules from _pytest.config import _strtobool from _pytest.config import Config from _pytest.config import ConftestImportFailure from _pytest.config import console_main from _pytest.config import ExitCode from _pytest.config import parse_warning_filter from _pytest.config.argparsing import get_ini_default_for_type from _pytest.config.argparsing import Parser from _pytest.config.exceptions import UsageError from _pytest.config.findpaths import ConfigValue from _pytest.config.findpaths import determine_setup from _pytest.config.findpaths import get_common_ancestor from _pytest.config.findpaths import locate_config from _pytest.monkeypatch import MonkeyPatch from _pytest.pathlib import absolutepath from _pytest.pytester import Pytester from _pytest.warning_types import PytestDeprecationWarning import pytest class TestParseIni: @pytest.mark.parametrize( "section, filename", [("pytest", "pytest.ini"), ("tool:pytest", "setup.cfg")] ) def test_getcfg_and_config( self, pytester: Pytester, tmp_path: Path, section: str, filename: str, monkeypatch: MonkeyPatch, ) -> None: sub = tmp_path / "sub" sub.mkdir() monkeypatch.chdir(sub) (tmp_path / filename).write_text( textwrap.dedent( f"""\ [{section}] name = value """ ), encoding="utf-8", ) _, _, cfg, _ = locate_config(Path.cwd(), [sub]) assert cfg["name"] == ConfigValue("value", origin="file", mode="ini") config = pytester.parseconfigure(str(sub)) assert config._inicfg["name"] == ConfigValue("value", origin="file", mode="ini") def test_setupcfg_uses_toolpytest_with_pytest(self, pytester: Pytester) -> None: p1 = pytester.makepyfile("def test(): pass") pytester.makefile( ".cfg", setup=f""" [tool:pytest] testpaths={p1.name} [pytest] testpaths=ignored """, ) result = pytester.runpytest() result.stdout.fnmatch_lines(["configfile: setup.cfg", "* 1 passed in *"]) assert result.ret == 0 def test_append_parse_args( self, pytester: Pytester, tmp_path: Path, monkeypatch: MonkeyPatch ) -> None: monkeypatch.setenv("PYTEST_ADDOPTS", '--color no -rs --tb="short"') tmp_path.joinpath("pytest.ini").write_text( textwrap.dedent( """\ [pytest] addopts = --verbose """ ), encoding="utf-8", ) config = pytester.parseconfig(tmp_path) assert config.option.color == "no" assert config.option.reportchars == "s" assert config.option.tbstyle == "short" assert config.option.verbose @pytest.mark.parametrize("flag", ("-r", "--report-chars=")) @pytest.mark.parametrize("value", ("fE", "A", "fs")) def test_report_chars_option( self, pytester: Pytester, tmp_path: Path, monkeypatch: MonkeyPatch, flag: str, value: str, ) -> None: """Test that -r/--report-chars is parsed correctly.""" monkeypatch.setenv("PYTEST_ADDOPTS", flag + value) config = pytester.parseconfig(tmp_path) assert config.option.reportchars == value def test_tox_ini_wrong_version(self, pytester: Pytester) -> None: pytester.makefile( ".ini", tox=""" [pytest] minversion=999.0 """, ) result = pytester.runpytest() assert result.ret != 0 result.stderr.fnmatch_lines( ["*tox.ini: 'minversion' requires pytest-999.0, actual pytest-*"] ) @pytest.mark.parametrize( "section, name", [ ("tool:pytest", "setup.cfg"), ("pytest", "tox.ini"), ("pytest", "pytest.ini"), ("pytest", ".pytest.ini"), ], ) def test_ini_names(self, pytester: Pytester, name, section) -> None: pytester.path.joinpath(name).write_text( textwrap.dedent( f""" [{section}] minversion = 3.36 """ ), encoding="utf-8", ) config = pytester.parseconfig() assert config.getini("minversion") == "3.36" @pytest.mark.parametrize("name", ["pytest.toml", ".pytest.toml"]) def test_toml_config_names(self, pytester: Pytester, name: str) -> None: pytester.path.joinpath(name).write_text( textwrap.dedent( """ [pytest] minversion = "3.36" """ ), encoding="utf-8", ) config = pytester.parseconfig() assert config.getini("minversion") == "3.36" @pytest.mark.parametrize("name", ["pytest.toml", ".pytest.toml"]) def test_toml_config_names_without_section_errors( self, pytester: Pytester, name: str ) -> None: config_path = pytester.path.joinpath(name) config_path.write_text( textwrap.dedent( """ minversion = "3.36" addopts = ["-v"] """ ), encoding="utf-8", ) with pytest.raises(UsageError) as excinfo: pytester.parseconfig() assert str(excinfo.value) == ( f"{config_path}: " "pytest configuration must be under a [pytest] table " "(found top-level options: minversion, addopts)" ) def test_pyproject_toml(self, pytester: Pytester) -> None: pyproject_toml = pytester.makepyprojecttoml( """ [tool.pytest.ini_options] minversion = "1.0" """ ) config = pytester.parseconfig() assert config.inipath == pyproject_toml assert config.getini("minversion") == "1.0" def test_empty_pyproject_toml(self, pytester: Pytester) -> None: """An empty pyproject.toml is considered as config if no other option is found.""" pyproject_toml = pytester.makepyprojecttoml("") config = pytester.parseconfig() assert config.inipath == pyproject_toml def test_empty_pyproject_toml_found_many(self, pytester: Pytester) -> None: """ In case we find multiple pyproject.toml files in our search, without a [tool.pytest] table and without finding other candidates, the closest to where we started wins. """ pytester.makefile( ".toml", **{ "pyproject": "", "foo/pyproject": "", "foo/bar/pyproject": "", }, ) config = pytester.parseconfig(pytester.path / "foo/bar") assert config.inipath == pytester.path / "foo/bar/pyproject.toml" def test_pytest_toml(self, pytester: Pytester) -> None: pytest_toml = pytester.path.joinpath("pytest.toml") pytest_toml = pytester.maketoml( """ [pytest] minversion = "1.0" """ ) config = pytester.parseconfig() assert config.inipath == pytest_toml assert config.getini("minversion") == "1.0" @pytest.mark.parametrize("name", ["pytest.toml", ".pytest.toml"]) def test_empty_pytest_toml(self, pytester: Pytester, name: str) -> None: """An empty pytest.toml is considered as config if no other option is found.""" pytest_toml = pytester.path / name pytest_toml.write_text("", encoding="utf-8") config = pytester.parseconfig() assert config.inipath == pytest_toml def test_pytest_toml_trumps_pyproject_toml(self, pytester: Pytester) -> None: """A pytest.toml always takes precedence over a pyproject.toml file.""" pytester.makepyprojecttoml( """ [tool.pytest] minversion = "1.0" """ ) pytest_toml = pytester.maketoml( """ [pytest] minversion = "2.0" """ ) config = pytester.parseconfig() assert config.inipath == pytest_toml assert config.getini("minversion") == "2.0" def test_pytest_toml_trumps_pytest_ini(self, pytester: Pytester) -> None: """A pytest.toml always takes precedence over a pytest.ini file.""" pytester.makeini( """ [pytest] minversion = 1.0 """, ) pytest_toml = pytester.maketoml( """ [pytest] minversion = "2.0" """, ) config = pytester.parseconfig() assert config.inipath == pytest_toml assert config.getini("minversion") == "2.0" def test_dot_pytest_toml_trumps_pytest_ini(self, pytester: Pytester) -> None: """A .pytest.toml always takes precedence over a pytest.ini file.""" pytester.makeini( """ [pytest] minversion = 1.0 """, ) pytest_toml = pytester.maketoml( """ [pytest] minversion = "2.0" """ ) config = pytester.parseconfig() assert config.inipath == pytest_toml assert config.getini("minversion") == "2.0" def test_pytest_ini_trumps_pyproject_toml(self, pytester: Pytester) -> None: """A pytest.ini always take precedence over a pyproject.toml file.""" pytester.makepyprojecttoml( """ [tool.pytest] minversion = "1.0" """ ) pytest_ini = pytester.makefile(".ini", pytest="") config = pytester.parseconfig() assert config.inipath == pytest_ini def test_toxini_before_lower_pytestini(self, pytester: Pytester) -> None: sub = pytester.mkdir("sub") sub.joinpath("tox.ini").write_text( textwrap.dedent( """ [pytest] minversion = 2.0 """ ), encoding="utf-8", ) pytester.path.joinpath("pytest.ini").write_text( textwrap.dedent( """ [pytest] minversion = 1.5 """ ), encoding="utf-8", ) config = pytester.parseconfigure(sub) assert config.getini("minversion") == "2.0" def test_ini_parse_error(self, pytester: Pytester) -> None: pytester.path.joinpath("pytest.ini").write_text( "addopts = -x", encoding="utf-8" ) result = pytester.runpytest() assert result.ret != 0 result.stderr.fnmatch_lines("ERROR: *pytest.ini:1: no section header defined") def test_toml_parse_error(self, pytester: Pytester) -> None: pytester.makepyprojecttoml( """ \\" """ ) result = pytester.runpytest() assert result.ret != 0 result.stderr.fnmatch_lines("ERROR: *pyproject.toml: Invalid statement*") def test_pytest_toml_parse_error(self, pytester: Pytester) -> None: pytester.path.joinpath("pytest.toml").write_text( """ \\" """, encoding="utf-8", ) result = pytester.runpytest() assert result.ret != 0 result.stderr.fnmatch_lines("ERROR: *pytest.toml: Invalid statement*") def test_confcutdir_default_without_configfile(self, pytester: Pytester) -> None: # If --confcutdir is not specified, and there is no configfile, default # to the rootpath. sub = pytester.mkdir("sub") os.chdir(sub) config = pytester.parseconfigure() assert config.pluginmanager._confcutdir == sub def test_confcutdir_default_with_configfile(self, pytester: Pytester) -> None: # If --confcutdir is not specified, and there is a configfile, default # to the configfile's directory. pytester.makeini("[pytest]") sub = pytester.mkdir("sub") os.chdir(sub) config = pytester.parseconfigure() assert config.pluginmanager._confcutdir == pytester.path @pytest.mark.xfail(reason="probably not needed") def test_confcutdir(self, pytester: Pytester) -> None: sub = pytester.mkdir("sub") os.chdir(sub) pytester.makeini( """ [pytest] addopts = --qwe """ ) result = pytester.inline_run("--confcutdir=.") assert result.ret == 0 @pytest.mark.parametrize( "ini_file_text, invalid_keys, warning_output, exception_text", [ pytest.param( """ [pytest] unknown_ini = value1 another_unknown_ini = value2 """, ["unknown_ini", "another_unknown_ini"], [ "=*= warnings summary =*=", "*PytestConfigWarning:*Unknown config option: another_unknown_ini", "*PytestConfigWarning:*Unknown config option: unknown_ini", ], "Unknown config option: another_unknown_ini", id="2-unknowns", ), pytest.param( """ [pytest] unknown_ini = value1 minversion = 5.0.0 """, ["unknown_ini"], [ "=*= warnings summary =*=", "*PytestConfigWarning:*Unknown config option: unknown_ini", ], "Unknown config option: unknown_ini", id="1-unknown", ), pytest.param( """ [some_other_header] unknown_ini = value1 [pytest] minversion = 5.0.0 """, [], [], "", id="unknown-in-other-header", ), pytest.param( """ [pytest] minversion = 5.0.0 """, [], [], "", id="no-unknowns", ), pytest.param( """ [pytest] conftest_ini_key = 1 """, [], [], "", id="1-known", ), ], ) @pytest.mark.filterwarnings("default") def test_invalid_config_options( self, pytester: Pytester, ini_file_text, invalid_keys, warning_output, exception_text, ) -> None: pytester.makeconftest( """ def pytest_addoption(parser): parser.addini("conftest_ini_key", "") """ ) pytester.makepyfile("def test(): pass") pytester.makeini(ini_file_text) config = pytester.parseconfig() assert sorted(config._get_unknown_ini_keys()) == sorted(invalid_keys) result = pytester.runpytest() result.stdout.fnmatch_lines(warning_output) result = pytester.runpytest("--strict-config") if exception_text: result.stderr.fnmatch_lines("ERROR: " + exception_text) assert result.ret == pytest.ExitCode.USAGE_ERROR else: result.stderr.no_fnmatch_line(exception_text) assert result.ret == pytest.ExitCode.OK @pytest.mark.filterwarnings("default") def test_silence_unknown_key_warning(self, pytester: Pytester) -> None: """Unknown config key warnings can be silenced using filterwarnings (#7620)""" pytester.makeini( """ [pytest] filterwarnings = ignore:Unknown config option:pytest.PytestConfigWarning foobar=1 """ ) result = pytester.runpytest() result.stdout.no_fnmatch_line("*PytestConfigWarning*") @pytest.mark.parametrize( "option", [ "strict_config = true", "strict = true", "addopts = --strict-config", ], ) def test_strict_config_ini_option(self, pytester: Pytester, option: str) -> None: """Test that strict_config and strict ini options enable strict config checking.""" pytester.makeini( f""" [pytest] unknown_option = 1 {option} """ ) result = pytester.runpytest() result.stderr.fnmatch_lines("ERROR: Unknown config option: unknown_option") assert result.ret == pytest.ExitCode.USAGE_ERROR @pytest.mark.filterwarnings("default::pytest.PytestConfigWarning") def test_disable_warnings_plugin_disables_config_warnings( self, pytester: Pytester ) -> None: """Disabling 'warnings' plugin also disables config time warnings""" pytester.makeconftest( """ import pytest def pytest_configure(config): config.issue_config_time_warning( pytest.PytestConfigWarning("custom config warning"), stacklevel=2, ) """ ) result = pytester.runpytest("-pno:warnings") result.stdout.no_fnmatch_line("*PytestConfigWarning*") @pytest.mark.parametrize( "ini_file_text, plugin_version, exception_text", [ pytest.param( """ [pytest] required_plugins = a z """, "1.5", "Missing required plugins: a, z", id="2-missing", ), pytest.param( """ [pytest] required_plugins = a z myplugin """, "1.5", "Missing required plugins: a, z", id="2-missing-1-ok", ), pytest.param( """ [pytest] required_plugins = myplugin """, "1.5", None, id="1-ok", ), pytest.param( """ [pytest] required_plugins = myplugin==1.5 """, "1.5", None, id="1-ok-pin-exact", ), pytest.param( """ [pytest] required_plugins = myplugin>1.0,<2.0 """, "1.5", None, id="1-ok-pin-loose", ), pytest.param( """ [pytest] required_plugins = myplugin """, "1.5a1", None, id="1-ok-prerelease", ), pytest.param( """ [pytest] required_plugins = myplugin==1.6 """, "1.5", "Missing required plugins: myplugin==1.6", id="missing-version", ), pytest.param( """ [pytest] required_plugins = myplugin==1.6 other==1.0 """, "1.5", "Missing required plugins: myplugin==1.6, other==1.0", id="missing-versions", ), pytest.param( """ [some_other_header] required_plugins = won't be triggered [pytest] """, "1.5", None, id="invalid-header", ), ], ) def test_missing_required_plugins( self, pytester: Pytester, monkeypatch: MonkeyPatch, ini_file_text: str, plugin_version: str, exception_text: str, ) -> None: """Check 'required_plugins' option with various settings. This test installs a mock "myplugin-1.5" which is used in the parametrized test cases. """ @dataclasses.dataclass class DummyEntryPoint: name: str module: str group: str = "pytest11" def load(self): return importlib.import_module(self.module) entry_points = [ DummyEntryPoint("myplugin1", "myplugin1_module"), ] @dataclasses.dataclass class DummyDist: entry_points: object files: object = () version: str = plugin_version @property def metadata(self): return {"name": "myplugin"} def my_dists(): return [DummyDist(entry_points)] pytester.makepyfile(myplugin1_module="# my plugin module") pytester.syspathinsert() monkeypatch.setattr(importlib.metadata, "distributions", my_dists) monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False) pytester.makeini(ini_file_text) if exception_text: with pytest.raises(pytest.UsageError, match=exception_text): pytester.parseconfig() else: pytester.parseconfig() def test_early_config_cmdline( self, pytester: Pytester, monkeypatch: MonkeyPatch ) -> None: """early_config contains options registered by third-party plugins. This is a regression involving pytest-cov (and possibly others) introduced in #7700. """ pytester.makepyfile( myplugin=""" def pytest_addoption(parser): parser.addoption('--foo', default=None, dest='foo') def pytest_load_initial_conftests(early_config, parser, args): assert early_config.known_args_namespace.foo == "1" """ ) monkeypatch.setenv("PYTEST_PLUGINS", "myplugin") pytester.syspathinsert() result = pytester.runpytest("--foo=1") result.stdout.fnmatch_lines("* no tests ran in *") def test_args_source_args(self, pytester: Pytester): config = pytester.parseconfig("--", "test_filename.py") assert config.args_source == Config.ArgsSource.ARGS def test_args_source_invocation_dir(self, pytester: Pytester): config = pytester.parseconfig() assert config.args_source == Config.ArgsSource.INVOCATION_DIR def test_args_source_testpaths(self, pytester: Pytester): pytester.makeini( """ [pytest] testpaths=* """ ) config = pytester.parseconfig() assert config.args_source == Config.ArgsSource.TESTPATHS class TestConfigCmdlineParsing: def test_parsing_again_fails(self, pytester: Pytester) -> None: config = pytester.parseconfig() with pytest.raises(AssertionError): config.parse([]) def test_explicitly_specified_config_file_is_loaded( self, pytester: Pytester ) -> None: pytester.makeconftest( """ def pytest_addoption(parser): parser.addini("custom", "") """ ) pytester.makeini( """ [pytest] custom = 0 """ ) pytester.makefile( ".ini", custom=""" [pytest] custom = 1 """, ) config = pytester.parseconfig("-c", "custom.ini") assert config.getini("custom") == "1" config = pytester.parseconfig("--config-file", "custom.ini") assert config.getini("custom") == "1" pytester.makefile( ".cfg", custom_tool_pytest_section=""" [tool:pytest] custom = 1 """, ) config = pytester.parseconfig("-c", "custom_tool_pytest_section.cfg") assert config.getini("custom") == "1" config = pytester.parseconfig("--config-file", "custom_tool_pytest_section.cfg") assert config.getini("custom") == "1" pytester.makefile( ".toml", custom=""" [tool.pytest.ini_options] custom = 1 value = [ ] # this is here on purpose, as it makes this an invalid '.ini' file """, ) config = pytester.parseconfig("-c", "custom.toml") assert config.getini("custom") == "1" config = pytester.parseconfig("--config-file", "custom.toml") assert config.getini("custom") == "1" # A custom TOML file also reads [pytest], the table pytest's own # configuration files use (#14705). pytester.makefile( ".toml", custom_pytest_table=""" [pytest] custom = "1" value = [ ] # this is here on purpose, as it makes this an invalid '.ini' file """, ) config = pytester.parseconfig("-c", "custom_pytest_table.toml") assert config.getini("custom") == "1" config = pytester.parseconfig("--config-file", "custom_pytest_table.toml") assert config.getini("custom") == "1" @pytest.mark.parametrize( "name", ["missing.ini", "missing.in", "missing.toml", "missing"] ) def test_explicitly_specified_config_file_missing( self, pytester: Pytester, name: str ) -> None: """A nonexistent -c path is a UsageError, whatever its extension (#14716). Previously this either silently proceeded with an empty configuration (unrecognized extension) or crashed with a raw FileNotFoundError traceback (recognized extension). """ with pytest.raises(UsageError, match=r"Config file .* not found"): pytester.parseconfig("-c", name) def test_explicitly_specified_config_file_unsupported_format( self, pytester: Pytester ) -> None: """An existing -c path with an unsupported extension is a UsageError (#14716).""" pytester.makefile(".in", config="[pytest]\naddopts = -v\n") with pytest.raises(UsageError, match="unsupported format"): pytester.parseconfig("-c", "config.in") def test_explicitly_specified_config_file_is_a_directory( self, pytester: Pytester ) -> None: """A directory passed to -c is a UsageError rather than a confusing no-op.""" pytester.mkdir("somedir") with pytest.raises(UsageError, match="is a directory"): pytester.parseconfig("-c", "somedir") @pytest.mark.skipif( sys.platform.startswith("win32"), reason="requires a POSIX null device" ) def test_explicitly_specified_config_file_not_a_regular_file( self, pytester: Pytester ) -> None: """``--config-file=/dev/null`` loads no config and does not set rootdir to /dev. Deriving the rootdir from a character device made the cache plugin try to write to ``/dev/.pytest_cache`` (#11502). """ pytester.makepyfile(test_it="def test(): pass") config = pytester.parseconfig("--config-file", os.devnull, str(pytester.path)) assert config.rootpath == pytester.path assert config.inipath == Path(os.devnull) def test_absolute_win32_path(self, pytester: Pytester) -> None: temp_ini_file = pytester.makeini("[pytest]") from os.path import normpath temp_ini_file_norm = normpath(str(temp_ini_file)) ret = pytest.main(["-c", temp_ini_file_norm]) assert ret == ExitCode.NO_TESTS_COLLECTED ret = pytest.main(["--config-file", temp_ini_file_norm]) assert ret == ExitCode.NO_TESTS_COLLECTED class TestConfigAPI: def test_config_trace(self, pytester: Pytester) -> None: config = pytester.parseconfig() values: list[str] = [] config.trace.root.setwriter(values.append) config.trace("hello") assert len(values) == 1 assert values[0] == "hello [config]\n" def test_config_getoption_declared_option_name(self, pytester: Pytester) -> None: pytester.makeconftest( """ def pytest_addoption(parser): parser.addoption("--hello", "-X", dest="hello") """ ) config = pytester.parseconfig("--hello=this") for x in ("hello", "--hello", "-X"): assert config.getoption(x) == "this" with pytest.raises(ValueError): config.getoption("qweqwe") config_novalue = pytester.parseconfig() assert config_novalue.getoption("hello") is None assert config_novalue.getoption("hello", default=1) is None assert config_novalue.getoption("hello", default=1, skip=True) == 1 def test_config_getoption_undeclared_option_name(self, pytester: Pytester) -> None: config = pytester.parseconfig() with pytest.raises(ValueError): config.getoption("x") assert config.getoption("x", default=1) == 1 assert config.getoption("x", default=1, skip=True) == 1 def test_config_getoption_unicode(self, pytester: Pytester) -> None: pytester.makeconftest( """ def pytest_addoption(parser): parser.addoption('--hello', type=str) """ ) config = pytester.parseconfig("--hello=this") assert config.getoption("hello") == "this" def test_config_getvalueorskip(self, pytester: Pytester) -> None: config = pytester.parseconfig() with pytest.raises(pytest.skip.Exception): config.getvalueorskip("hello") verbose = config.getvalueorskip("verbose") assert verbose == config.option.verbose def test_config_getvalueorskip_None(self, pytester: Pytester) -> None: pytester.makeconftest( """ def pytest_addoption(parser): parser.addoption("--hello") """ ) config = pytester.parseconfig() with pytest.raises(pytest.skip.Exception): config.getvalueorskip("hello") def test_getconftest_pathlist(self, pytester: Pytester, tmp_path: Path) -> None: somepath = tmp_path.joinpath("x", "y", "z") p = tmp_path.joinpath("conftest.py") p.write_text(f"mylist = {['.', str(somepath)]}", encoding="utf-8") config = pytester.parseconfigure(p) assert config._getconftest_pathlist("notexist", path=tmp_path) is None assert config._getconftest_pathlist("mylist", path=tmp_path) == [ tmp_path, somepath, ] @pytest.mark.parametrize("maybe_type", ["not passed", "None", '"string"']) def test_addini(self, pytester: Pytester, maybe_type: str) -> None: if maybe_type == "not passed": type_string = "" else: type_string = f", {maybe_type}" pytester.makeconftest( f""" def pytest_addoption(parser): parser.addini("myname", "my new ini value"{type_string}) """ ) pytester.makeini( """ [pytest] myname=hello """ ) config = pytester.parseconfig() val = config.getini("myname") assert val == "hello" with pytest.raises(ValueError): config.getini("other") @pytest.mark.parametrize("config_type", ["ini", "pyproject"]) def test_addini_paths(self, pytester: Pytester, config_type: str) -> None: pytester.makeconftest( """ def pytest_addoption(parser): parser.addini("paths", "my new ini value", type="paths") parser.addini("abc", "abc value") """ ) if config_type == "ini": inipath = pytester.makeini( """ [pytest] paths=hello world/sub.py """ ) elif config_type == "pyproject": inipath = pytester.makepyprojecttoml( """ [tool.pytest.ini_options] paths=["hello", "world/sub.py"] """ ) config = pytester.parseconfig() values = config.getini("paths") assert len(values) == 2 assert values[0] == inipath.parent.joinpath("hello") assert values[1] == inipath.parent.joinpath("world/sub.py") with pytest.raises(ValueError): config.getini("other") def make_conftest_for_args(self, pytester: Pytester) -> None: pytester.makeconftest( """ def pytest_addoption(parser): parser.addini("args", "new args", type="args") parser.addini("a2", "", "args", default="1 2 3".split()) """ ) def test_addini_args_ini_files(self, pytester: Pytester) -> None: self.make_conftest_for_args(pytester) pytester.makeini( """ [pytest] args=123 "123 hello" "this" """ ) self.check_config_args(pytester) def test_addini_args_pyproject_toml(self, pytester: Pytester) -> None: self.make_conftest_for_args(pytester) pytester.makepyprojecttoml( """ [tool.pytest.ini_options] args = ["123", "123 hello", "this"] """ ) self.check_config_args(pytester) def check_config_args(self, pytester: Pytester) -> None: config = pytester.parseconfig() values = config.getini("args") assert values == ["123", "123 hello", "this"] values = config.getini("a2") assert values == list("123") def make_conftest_for_linelist(self, pytester: Pytester) -> None: pytester.makeconftest( """ def pytest_addoption(parser): parser.addini("xy", "", type="linelist") parser.addini("a2", "", "linelist") """ ) def test_addini_linelist_ini_files(self, pytester: Pytester) -> None: self.make_conftest_for_linelist(pytester) pytester.makeini( """ [pytest] xy= 123 345 second line """ ) self.check_config_linelist(pytester) def test_addini_linelist_pprojecttoml(self, pytester: Pytester) -> None: self.make_conftest_for_linelist(pytester) pytester.makepyprojecttoml( """ [tool.pytest.ini_options] xy = ["123 345", "second line"] """ ) self.check_config_linelist(pytester) def check_config_linelist(self, pytester: Pytester) -> None: config = pytester.parseconfig() values = config.getini("xy") assert len(values) == 2 assert values == ["123 345", "second line"] values = config.getini("a2") assert values == [] @pytest.mark.parametrize( "str_val, bool_val", [("True", True), ("no", False), ("no-ini", True)] ) def test_addini_bool( self, pytester: Pytester, str_val: str, bool_val: bool ) -> None: pytester.makeconftest( """ def pytest_addoption(parser): parser.addini("strip", "", type="bool", default=True) """ ) if str_val != "no-ini": pytester.makeini( f""" [pytest] strip={str_val} """ ) config = pytester.parseconfig() assert config.getini("strip") is bool_val @pytest.mark.parametrize("str_val, int_val", [("10", 10), ("no-ini", 2)]) def test_addini_int(self, pytester: Pytester, str_val: str, int_val: bool) -> None: pytester.makeconftest( """ def pytest_addoption(parser): parser.addini("ini_param", "", type="int", default=2) """ ) if str_val != "no-ini": pytester.makeini( f""" [pytest] ini_param={str_val} """ ) config = pytester.parseconfig() assert config.getini("ini_param") == int_val def test_addini_int_invalid(self, pytester: Pytester) -> None: pytester.makeconftest( """ def pytest_addoption(parser): parser.addini("ini_param", "", type="int", default=2) """ ) pytester.makepyprojecttoml( """ [tool.pytest.ini_options] ini_param=["foo"] """ ) config = pytester.parseconfig() with pytest.raises( UsageError, match="Expected an int string for option ini_param" ): _ = config.getini("ini_param") @pytest.mark.parametrize("str_val, float_val", [("10.5", 10.5), ("no-ini", 2.2)]) def test_addini_float( self, pytester: Pytester, str_val: str, float_val: bool ) -> None: pytester.makeconftest( """ def pytest_addoption(parser): parser.addini("ini_param", "", type="float", default=2.2) """ ) if str_val != "no-ini": pytester.makeini( f""" [pytest] ini_param={str_val} """ ) config = pytester.parseconfig() assert config.getini("ini_param") == float_val def test_addini_float_invalid(self, pytester: Pytester) -> None: pytester.makeconftest( """ def pytest_addoption(parser): parser.addini("ini_param", "", type="float", default=2.2) """ ) pytester.makepyprojecttoml( """ [tool.pytest.ini_options] ini_param=["foo"] """ ) config = pytester.parseconfig() with pytest.raises( UsageError, match="Expected a float string for option ini_param" ): _ = config.getini("ini_param") UNION_CONFTEST = """ def pytest_addoption(parser): parser.addini("ini_param", "", type=int | str, default=None) """ LITERAL_CONFTEST = """ from typing import Literal def pytest_addoption(parser): parser.addini( "ini_param", "", type=Literal["auto", "long"], default="auto" ) """ @pytest.mark.parametrize( "section, value, expected", [ # Native TOML: int and str are both accepted; the first union # member that matches wins (int before str). ("[tool.pytest]", '"7"', "7"), ("[tool.pytest]", "7", 7), # ini_options mode stringifies, then coerces to the first member. ("[tool.pytest.ini_options]", '"7"', 7), ("[tool.pytest.ini_options]", "7", 7), ], ids=["native-str", "native-int", "ini-options-str", "ini-options-int"], ) def test_addini_union_type( self, pytester: Pytester, section: str, value: str, expected: object ) -> None: pytester.makeconftest(self.UNION_CONFTEST) pytester.makepyprojecttoml( f""" {section} ini_param = {value} """ ) config = pytester.parseconfig() result = config.getini("ini_param") assert result == expected assert type(result) is type(expected) def test_addini_union_type_invalid_value(self, pytester: Pytester) -> None: pytester.makeconftest(self.UNION_CONFTEST) pytester.makepyprojecttoml( """ [tool.pytest] ini_param = [1, 2] """ ) config = pytester.parseconfig() with pytest.raises( UsageError, match=r"config option 'ini_param' expects one of int \| string" ): _ = config.getini("ini_param") def test_addini_plain_type(self, pytester: Pytester) -> None: """A plain Python type is accepted as an alias of its string tag.""" pytester.makeconftest( """ def pytest_addoption(parser): parser.addini("ini_param", "", type=int) """ ) pytester.makepyprojecttoml( """ [tool.pytest] ini_param = 7 """ ) config = pytester.parseconfig() assert config.getini("ini_param") == 7 @pytest.mark.parametrize("bad_type", ["integer", dict, int | dict]) def test_addini_invalid_type(self, bad_type: object) -> None: parser = Parser(_ispytest=True) with pytest.raises(ValueError, match="invalid type for ini option 'ini_param'"): parser.addini("ini_param", "", type=bad_type) # type: ignore[arg-type] def test_addini_union_type_requires_default(self) -> None: parser = Parser(_ispytest=True) with pytest.raises( ValueError, match="union type, which has no implicit default" ): parser.addini("ini_param", "", type=int | str) @pytest.mark.parametrize( "value, expected", [('"long"', "long"), (None, "auto")], ids=["value", "default"], ) def test_addini_literal_type( self, pytester: Pytester, value: str | None, expected: str ) -> None: """A Literal of strings restricts the value to the given choices.""" pytester.makeconftest(self.LITERAL_CONFTEST) if value is not None: pytester.makepyprojecttoml(f"[tool.pytest]\nini_param = {value}") config = pytester.parseconfig() assert config.getini("ini_param") == expected def test_addini_literal_type_ini_and_override(self, pytester: Pytester) -> None: pytester.makeconftest(self.LITERAL_CONFTEST) pytester.makeini( """ [pytest] ini_param = long """ ) assert pytester.parseconfig().getini("ini_param") == "long" assert ( pytester.parseconfig("-o", "ini_param=auto").getini("ini_param") == "auto" ) @pytest.mark.parametrize( "value, match", [ ('"short"', r"expects one of 'auto' \| 'long', got 'short'"), ("5", r"expects a string, got int: 5"), ], ids=["bad-choice", "bad-type"], ) def test_addini_literal_type_invalid_value( self, pytester: Pytester, value: str, match: str ) -> None: pytester.makeconftest(self.LITERAL_CONFTEST) pytester.makepyprojecttoml(f"[tool.pytest]\nini_param = {value}") config = pytester.parseconfig() with pytest.raises(UsageError, match=f"config option 'ini_param' {match}"): _ = config.getini("ini_param") UNION_LITERAL_CONFTEST = """ from typing import Literal def pytest_addoption(parser): parser.addini( "ini_param", "", type=int | Literal["auto"], default="auto" ) """ @pytest.mark.parametrize( "value, expected", [("3", 3), ('"auto"', "auto"), (None, "auto")], ids=["int", "literal", "default"], ) def test_addini_union_with_literal_toml( self, pytester: Pytester, value: str | None, expected: object ) -> None: """A Literal of strings may be a union member, e.g. int | Literal["auto"].""" pytester.makeconftest(self.UNION_LITERAL_CONFTEST) if value is not None: pytester.makepyprojecttoml(f"[tool.pytest]\nini_param = {value}") assert pytester.parseconfig().getini("ini_param") == expected def test_addini_union_with_literal_ini_and_override( self, pytester: Pytester ) -> None: pytester.makeconftest(self.UNION_LITERAL_CONFTEST) pytester.makeini( """ [pytest] ini_param = 3 """ ) assert pytester.parseconfig().getini("ini_param") == 3 assert ( pytester.parseconfig("-o", "ini_param=auto").getini("ini_param") == "auto" ) def test_addini_union_with_literal_invalid_value(self, pytester: Pytester) -> None: pytester.makeconftest(self.UNION_LITERAL_CONFTEST) pytester.makepyprojecttoml('[tool.pytest]\nini_param = "3"') config = pytester.parseconfig() with pytest.raises( UsageError, match=r"config option 'ini_param' expects one of int \| 'auto', " r"got str: '3'", ): _ = config.getini("ini_param") def test_addini_union_with_literal_non_str_choice(self) -> None: parser = Parser(_ispytest=True) with pytest.raises(ValueError, match="Literal choices must be strings"): parser.addini("ini_param", "", type=str | Literal[1], default="") def test_addini_literal_type_requires_default(self) -> None: parser = Parser(_ispytest=True) with pytest.raises( ValueError, match="Literal type, which has no implicit default" ): parser.addini("ini_param", "", type=Literal["auto", "long"]) def test_addini_literal_type_non_str_choice(self) -> None: parser = Parser(_ispytest=True) with pytest.raises(ValueError, match="Literal choices must be strings"): parser.addini("ini_param", "", type=Literal["auto", 1], default="auto") def test_addinivalue_line_existing(self, pytester: Pytester) -> None: pytester.makeconftest( """ def pytest_addoption(parser): parser.addini("xy", "", type="linelist") """ ) pytester.makeini( """ [pytest] xy= 123 """ ) config = pytester.parseconfig() values = config.getini("xy") assert len(values) == 1 assert values == ["123"] config.addinivalue_line("xy", "456") values = config.getini("xy") assert len(values) == 2 assert values == ["123", "456"] def test_addinivalue_line_new(self, pytester: Pytester) -> None: pytester.makeconftest( """ def pytest_addoption(parser): parser.addini("xy", "", type="linelist") """ ) config = pytester.parseconfig() assert not config.getini("xy") config.addinivalue_line("xy", "456") values = config.getini("xy") assert len(values) == 1 assert values == ["456"] config.addinivalue_line("xy", "123") values = config.getini("xy") assert len(values) == 2 assert values == ["456", "123"] def test_addini_default_values(self, pytester: Pytester) -> None: """Tests the default values for configuration based on config type """ pytester.makeconftest( """ def pytest_addoption(parser): parser.addini("linelist1", "", type="linelist") parser.addini("paths1", "", type="paths") parser.addini("pathlist1", "", type="pathlist") parser.addini("args1", "", type="args") parser.addini("bool1", "", type="bool") parser.addini("string1", "", type="string") parser.addini("none_1", "", type="linelist", default=None) parser.addini("none_2", "", default=None) parser.addini("no_type", "") """ ) config = pytester.parseconfig() # default for linelist, paths, pathlist and args is [] value = config.getini("linelist1") assert value == [] value = config.getini("paths1") assert value == [] value = config.getini("pathlist1") assert value == [] value = config.getini("args1") assert value == [] # default for bool is False value = config.getini("bool1") assert value is False # default for string is "" value = config.getini("string1") assert value == "" # should return None if None is explicitly set as default value # irrespective of the type argument value = config.getini("none_1") assert value is None value = config.getini("none_2") assert value is None # in case no type is provided and no default set # treat it as string and default value will be "" value = config.getini("no_type") assert value == "" def test_addini_with_aliases(self, pytester: Pytester) -> None: """Test that ini options can have aliases.""" pytester.makeconftest( """ def pytest_addoption(parser): parser.addini("new_name", "my option", aliases=["old_name"]) """ ) pytester.makeini( """ [pytest] old_name = hello """ ) config = pytester.parseconfig() # Should be able to access via canonical name. assert config.getini("new_name") == "hello" # Should also be able to access via alias. assert config.getini("old_name") == "hello" def test_addini_aliases_with_canonical_in_file(self, pytester: Pytester) -> None: """Test that canonical name takes precedence over alias in configuration file.""" pytester.makeconftest( """ def pytest_addoption(parser): parser.addini("new_name", "my option", aliases=["old_name"]) """ ) pytester.makeini( """ [pytest] old_name = from_alias new_name = from_canonical """ ) config = pytester.parseconfig() # Canonical name should take precedence. assert config.getini("new_name") == "from_canonical" assert config.getini("old_name") == "from_canonical" def test_addini_aliases_multiple(self, pytester: Pytester) -> None: """Test that ini option can have multiple aliases.""" pytester.makeconftest( """ def pytest_addoption(parser): parser.addini("current_name", "my option", aliases=["old_name", "legacy_name"]) """ ) pytester.makeini( """ [pytest] old_name = value1 """ ) config = pytester.parseconfig() assert config.getini("current_name") == "value1" assert config.getini("old_name") == "value1" assert config.getini("legacy_name") == "value1" def test_addini_aliases_with_override_of_old(self, pytester: Pytester) -> None: """Test that aliases work with --override-ini -- ini sets old.""" pytester.makeconftest( """ def pytest_addoption(parser): parser.addini("new_name", "my option", aliases=["old_name"]) """ ) pytester.makeini( """ [pytest] old_name = from_file """ ) # Override using alias. config = pytester.parseconfig("-o", "old_name=overridden") assert config.getini("new_name") == "overridden" assert config.getini("old_name") == "overridden" # Override using canonical name. config = pytester.parseconfig("-o", "new_name=overridden2") assert config.getini("new_name") == "overridden2" def test_addini_aliases_with_override_of_new(self, pytester: Pytester) -> None: """Test that aliases work with --override-ini -- ini sets new.""" pytester.makeconftest( """ def pytest_addoption(parser): parser.addini("new_name", "my option", aliases=["old_name"]) """ ) pytester.makeini( """ [pytest] new_name = from_file """ ) # Override using alias. config = pytester.parseconfig("-o", "old_name=overridden") assert config.getini("new_name") == "overridden" assert config.getini("old_name") == "overridden" # Override using canonical name. config = pytester.parseconfig("-o", "new_name=overridden2") assert config.getini("new_name") == "overridden2" def test_addini_aliases_with_types(self, pytester: Pytester) -> None: """Test that aliases work with different types.""" pytester.makeconftest( """ def pytest_addoption(parser): parser.addini("mylist", "list option", type="linelist", aliases=["oldlist"]) parser.addini("mybool", "bool option", type="bool", aliases=["oldbool"]) """ ) pytester.makeini( """ [pytest] oldlist = line1 line2 oldbool = true """ ) config = pytester.parseconfig() assert config.getini("mylist") == ["line1", "line2"] assert config.getini("oldlist") == ["line1", "line2"] assert config.getini("mybool") is True assert config.getini("oldbool") is True def test_addini_aliases_conflict_error(self, pytester: Pytester) -> None: """Test that registering an alias that conflicts with an existing option raises an error.""" pytester.makeconftest( """ def pytest_addoption(parser): parser.addini("existing", "first option") try: parser.addini("new_option", "second option", aliases=["existing"]) except ValueError as e: assert "alias 'existing' conflicts with existing configuration option" in str(e) else: assert False, "Should have raised ValueError" """ ) pytester.parseconfig() def test_addini_aliases_duplicate_error(self, pytester: Pytester) -> None: """Test that registering the same alias twice raises an error.""" pytester.makeconftest( """ def pytest_addoption(parser): parser.addini("option1", "first option", aliases=["shared_alias"]) try: parser.addini("option2", "second option", aliases=["shared_alias"]) raise AssertionError("Should have raised ValueError") except ValueError as e: assert "'shared_alias' is already an alias of 'option1'" in str(e) """ ) pytester.parseconfig() @pytest.mark.parametrize( "type, expected", [ pytest.param(None, "", id="None"), pytest.param("string", "", id="string"), pytest.param("paths", [], id="paths"), pytest.param("pathlist", [], id="pathlist"), pytest.param("args", [], id="args"), pytest.param("linelist", [], id="linelist"), pytest.param("bool", False, id="bool"), ], ) def test_get_ini_default_for_type(self, type: Any, expected: Any) -> None: assert get_ini_default_for_type(type) == expected def test_confcutdir_check_isdir(self, pytester: Pytester) -> None: """Give an error if --confcutdir is not a valid directory (#2078)""" exp_match = r"^--confcutdir must be a directory, given: " with pytest.raises(pytest.UsageError, match=exp_match): pytester.parseconfig("--confcutdir", pytester.path.joinpath("file")) with pytest.raises(pytest.UsageError, match=exp_match): pytester.parseconfig("--confcutdir", pytester.path.joinpath("nonexistent")) p = pytester.mkdir("dir") config = pytester.parseconfig("--confcutdir", p) assert config.getoption("confcutdir") == str(p) @pytest.mark.parametrize( "names, expected", [ # dist-info based distributions root are files as will be put in PYTHONPATH (["bar.py"], ["bar"]), (["foo/bar.py"], ["bar"]), (["foo/bar.pyc"], []), (["foo/__init__.py"], ["foo"]), (["bar/__init__.py", "xz.py"], ["bar", "xz"]), (["setup.py"], []), # egg based distributions root contain the files from the dist root (["src/bar/__init__.py"], ["bar"]), (["src/bar/__init__.py", "setup.py"], ["bar"]), (["source/python/bar/__init__.py", "setup.py"], ["bar"]), # editable installation finder modules (["__editable___xyz_finder.py"], []), (["bar/__init__.py", "__editable___xyz_finder.py"], ["bar"]), ], ) def test_iter_rewritable_modules(self, names, expected) -> None: assert list(_iter_rewritable_modules(names)) == expected def test_add_cleanup(self, pytester: Pytester) -> None: config = Config.fromdictargs({}, []) config._do_configure() report = [] class MyError(BaseException): pass @config.add_cleanup def cleanup_last(): report.append("cleanup_last") @config.add_cleanup def raise_2(): report.append("raise_2") raise MyError("raise_2") @config.add_cleanup def raise_1(): report.append("raise_1") raise MyError("raise_1") @config.add_cleanup def cleanup_first(): report.append("cleanup_first") with pytest.raises(MyError, match=r"raise_2"): config._ensure_unconfigure() assert report == ["cleanup_first", "raise_1", "raise_2", "cleanup_last"] class TestConfigFromdictargs: def test_basic_behavior(self, _sys_snapshot) -> None: option_dict = {"verbose": 444, "foo": "bar", "capture": "no"} args = ["a", "b"] config = Config.fromdictargs(option_dict, args) with pytest.raises(AssertionError): config.parse(["should refuse to parse again"]) assert config.option.verbose == 444 assert config.option.foo == "bar" assert config.option.capture == "no" assert config.args == args def test_invocation_params_args(self, _sys_snapshot) -> None: """Show that fromdictargs can handle args in their "orig" format""" option_dict: dict[str, object] = {} args = ["-vvvv", "-s", "a", "b"] config = Config.fromdictargs(option_dict, args) assert config.args == ["a", "b"] assert config.invocation_params.args == tuple(args) assert config.option.verbose == 4 assert config.option.capture == "no" def test_inifilename(self, tmp_path: Path) -> None: d1 = tmp_path.joinpath("foo") d1.mkdir() p1 = d1.joinpath("bar.ini") p1.touch() p1.write_text( textwrap.dedent( """\ [pytest] name = value """ ), encoding="utf-8", ) inifilename = "../../foo/bar.ini" option_dict = {"inifilename": inifilename, "capture": "no"} cwd = tmp_path.joinpath("a/b") cwd.mkdir(parents=True) p2 = cwd.joinpath("pytest.ini") p2.touch() p2.write_text( textwrap.dedent( """\ [pytest] name = wrong-value should_not_be_set = true """ ), encoding="utf-8", ) with MonkeyPatch.context() as mp: mp.chdir(cwd) config = Config.fromdictargs(option_dict, []) inipath = absolutepath(inifilename) assert config.args == [str(cwd)] assert config.option.inifilename == inifilename assert config.option.capture == "no" # this indicates this is the file used for getting configuration values assert config.inipath == inipath assert config._inicfg.get("name") == ConfigValue( "value", origin="file", mode="ini" ) assert config._inicfg.get("should_not_be_set") is None def test_options_on_small_file_do_not_blow_up(pytester: Pytester) -> None: def runfiletest(opts: Sequence[str]) -> None: reprec = pytester.inline_run(*opts) passed, skipped, failed = reprec.countoutcomes() assert failed == 2 assert skipped == passed == 0 path = str( pytester.makepyfile( """ def test_f1(): assert 0 def test_f2(): assert 0 """ ) ) runfiletest([path]) runfiletest(["-l", path]) runfiletest(["-s", path]) runfiletest(["--tb=no", path]) runfiletest(["--tb=short", path]) runfiletest(["--tb=long", path]) runfiletest(["--fulltrace", path]) runfiletest(["--traceconfig", path]) runfiletest(["-v", path]) runfiletest(["-v", "-v", path]) def test_preparse_ordering_with_setuptools( pytester: Pytester, monkeypatch: MonkeyPatch ) -> None: monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False) class EntryPoint: name = "mytestplugin" group = "pytest11" def load(self): class PseudoPlugin: x = 42 return PseudoPlugin() class Dist: files = () metadata = {"name": "foo"} entry_points = (EntryPoint(),) def my_dists(): return (Dist,) monkeypatch.setattr(importlib.metadata, "distributions", my_dists) pytester.makeconftest( """ pytest_plugins = "mytestplugin", """ ) monkeypatch.setenv("PYTEST_PLUGINS", "mytestplugin") config = pytester.parseconfig() plugin = config.pluginmanager.getplugin("mytestplugin") assert plugin.x == 42 def test_setuptools_importerror_issue1479( pytester: Pytester, monkeypatch: MonkeyPatch ) -> None: monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False) class DummyEntryPoint: name = "mytestplugin" group = "pytest11" def load(self): raise ImportError("Don't hide me!") class Distribution: version = "1.0" files = ("foo.txt",) metadata = {"name": "foo"} entry_points = (DummyEntryPoint(),) def distributions(): return (Distribution(),) monkeypatch.setattr(importlib.metadata, "distributions", distributions) with pytest.raises(ImportError): pytester.parseconfig() def test_importlib_metadata_broken_distribution( pytester: Pytester, monkeypatch: MonkeyPatch ) -> None: """Integration test for broken distributions with 'files' metadata being None (#5389)""" monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False) class DummyEntryPoint: name = "mytestplugin" group = "pytest11" def load(self): return object() class Distribution: version = "1.0" files = None metadata = {"name": "foo"} entry_points = (DummyEntryPoint(),) def distributions(): return (Distribution(),) monkeypatch.setattr(importlib.metadata, "distributions", distributions) pytester.parseconfig() @pytest.mark.parametrize("block_it", [True, False]) def test_plugin_preparse_prevents_setuptools_loading( pytester: Pytester, monkeypatch: MonkeyPatch, block_it: bool ) -> None: monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False) plugin_module_placeholder = object() class DummyEntryPoint: name = "mytestplugin" group = "pytest11" def load(self): return plugin_module_placeholder class Distribution: version = "1.0" files = ("foo.txt",) metadata = {"name": "foo"} entry_points = (DummyEntryPoint(),) def distributions(): return (Distribution(),) monkeypatch.setattr(importlib.metadata, "distributions", distributions) args = ("-p", "no:mytestplugin") if block_it else () config = pytester.parseconfig(*args) config.pluginmanager.import_plugin("mytestplugin") if block_it: assert "mytestplugin" not in sys.modules assert config.pluginmanager.get_plugin("mytestplugin") is None else: assert ( config.pluginmanager.get_plugin("mytestplugin") is plugin_module_placeholder ) @pytest.mark.parametrize("disable_plugin_method", ["env_var", "flag", ""]) @pytest.mark.parametrize("enable_plugin_method", ["env_var", "flag", ""]) def test_disable_plugin_autoload( pytester: Pytester, monkeypatch: MonkeyPatch, enable_plugin_method: str, disable_plugin_method: str, ) -> None: class DummyEntryPoint: project_name = name = "mytestplugin" group = "pytest11" version = "1.0" def load(self): return sys.modules[self.name] class Distribution: metadata = {"name": "foo"} entry_points = (DummyEntryPoint(),) files = () class PseudoPlugin: x = 42 attrs_used = [] def __getattr__(self, name): assert name in ("__loader__", "__spec__") self.attrs_used.append(name) return object() def distributions(): return (Distribution(),) parse_args: list[str] = [] if disable_plugin_method == "env_var": monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1") elif disable_plugin_method == "flag": monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD") parse_args.append("--disable-plugin-autoload") else: assert disable_plugin_method == "" monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD") if enable_plugin_method == "env_var": monkeypatch.setenv("PYTEST_PLUGINS", "mytestplugin") elif enable_plugin_method == "flag": parse_args.extend(["-p", "mytestplugin"]) else: assert enable_plugin_method == "" monkeypatch.setattr(importlib.metadata, "distributions", distributions) monkeypatch.setitem(sys.modules, "mytestplugin", PseudoPlugin()) config = pytester.parseconfig(*parse_args) has_loaded = config.pluginmanager.get_plugin("mytestplugin") is not None # it should load if it's enabled, or we haven't disabled autoloading assert has_loaded == (bool(enable_plugin_method) or not disable_plugin_method) # The reason for the discrepancy between 'has_loaded' and __loader__ being accessed # appears to be the monkeypatching of importlib.metadata.distributions; where # files being empty means that _mark_plugins_for_rewrite doesn't find the plugin. # But enable_method==flag ends up in mark_rewrite being called and __loader__ # being accessed. assert ("__loader__" in PseudoPlugin.attrs_used) == ( has_loaded and not (enable_plugin_method in ("env_var", "") and not disable_plugin_method) ) # __spec__ is accessed in AssertionRewritingHook.exec_module, which is never # reached here: since PYTEST_PLUGINS also considers entry points (#12624), # the plugin is loaded through its entry point (like with -p) instead of # being imported through the rewrite hook. assert "__spec__" not in PseudoPlugin.attrs_used def test_plugin_loading_order(pytester: Pytester) -> None: """Test order of plugin loading with `-p`.""" p1 = pytester.makepyfile( """ def test_terminal_plugin(request): import myplugin assert myplugin.terminal_plugin == [False, True] """, myplugin=""" terminal_plugin = [] def pytest_configure(config): terminal_plugin.append(bool(config.pluginmanager.get_plugin("terminalreporter"))) def pytest_sessionstart(session): config = session.config terminal_plugin.append(bool(config.pluginmanager.get_plugin("terminalreporter"))) """, ) pytester.syspathinsert() result = pytester.runpytest("-p", "myplugin", str(p1)) assert result.ret == 0 def test_invalid_options_show_extra_information(pytester: Pytester) -> None: """Display extra information when pytest exits due to unrecognized options in the command-line.""" pytester.makeini( """ [pytest] addopts = --invalid-option """ ) result = pytester.runpytest() result.stderr.fnmatch_lines( [ "*error: unrecognized arguments: --invalid-option*", "* inifile: {}*".format(pytester.path.joinpath("tox.ini")), f"* rootdir: {pytester.path}*", ] ) @pytest.mark.parametrize( "args", [ ["dir1", "dir2", "-v"], ["dir1", "-v", "dir2"], ["dir2", "-v", "dir1"], ["-v", "dir2", "dir1"], ], ) def test_consider_args_after_options_for_rootdir( pytester: Pytester, args: list[str] ) -> None: """ Consider all arguments in the command-line for rootdir discovery, even if they happen to occur after an option. #949 """ # replace "dir1" and "dir2" from "args" into their real directory root = pytester.mkdir("myroot") d1 = root.joinpath("dir1") d1.mkdir() d2 = root.joinpath("dir2") d2.mkdir() for i, arg in enumerate(args): if arg == "dir1": args[i] = str(d1) elif arg == "dir2": args[i] = str(d2) with MonkeyPatch.context() as mp: mp.chdir(root) result = pytester.runpytest(*args) result.stdout.fnmatch_lines(["*rootdir: *myroot"]) def test_toolongargs_issue224(pytester: Pytester) -> None: result = pytester.runpytest("-m", "hello" * 500) assert result.ret == ExitCode.NO_TESTS_COLLECTED def test_config_in_subdirectory_colon_command_line_issue2148( pytester: Pytester, ) -> None: conftest_source = """ def pytest_addoption(parser): parser.addini('foo', 'foo') """ pytester.makefile( ".ini", **{"pytest": "[pytest]\nfoo = root", "subdir/pytest": "[pytest]\nfoo = subdir"}, ) pytester.makepyfile( **{ "conftest": conftest_source, "subdir/conftest": conftest_source, "subdir/test_foo": """\ def test_foo(pytestconfig): assert pytestconfig.getini('foo') == 'subdir' """, } ) result = pytester.runpytest("subdir/test_foo.py::test_foo") assert result.ret == 0 def test_notify_exception(pytester: Pytester, capfd) -> None: config = pytester.parseconfig() with pytest.raises(ValueError) as excinfo: raise ValueError(1) config.notify_exception(excinfo, config.option) _, err = capfd.readouterr() assert "ValueError" in err class A: def pytest_internalerror(self): return True config.pluginmanager.register(A()) config.notify_exception(excinfo, config.option) _, err = capfd.readouterr() assert not err config = pytester.parseconfig("-p", "no:terminal") with pytest.raises(ValueError) as excinfo: raise ValueError(1) config.notify_exception(excinfo, config.option) _, err = capfd.readouterr() assert "ValueError" in err def test_no_terminal_discovery_error(pytester: Pytester) -> None: pytester.makepyfile("raise TypeError('oops!')") result = pytester.runpytest("-p", "no:terminal", "--collect-only") assert result.ret == ExitCode.INTERRUPTED def test_load_initial_conftest_last_ordering(_config_for_test): pm = _config_for_test.pluginmanager class My: def pytest_load_initial_conftests(self): pass m = My() pm.register(m) hc = pm.hook.pytest_load_initial_conftests hookimpls = [ ( hookimpl.function.__module__, "wrapper" if (hookimpl.wrapper or hookimpl.hookwrapper) else "nonwrapper", ) for hookimpl in hc.get_hookimpls() ] assert hookimpls == [ ("_pytest.config", "nonwrapper"), (m.__module__, "nonwrapper"), ("_pytest.legacypath", "nonwrapper"), ("_pytest.capture", "wrapper"), ("_pytest.warnings", "wrapper"), ] def test_get_plugin_specs_as_list() -> None: def exp_match(val: object) -> str: return ( f"Plugins may be specified as a sequence or a ','-separated string " f"of plugin names. Got: {re.escape(repr(val))}" ) with pytest.raises(pytest.UsageError, match=exp_match({"foo"})): _get_plugin_specs_as_list({"foo"}) # type: ignore[arg-type] with pytest.raises(pytest.UsageError, match=exp_match({})): _get_plugin_specs_as_list(dict()) # type: ignore[arg-type] assert _get_plugin_specs_as_list(None) == [] assert _get_plugin_specs_as_list("") == [] assert _get_plugin_specs_as_list("foo") == ["foo"] assert _get_plugin_specs_as_list("foo,bar") == ["foo", "bar"] assert _get_plugin_specs_as_list(["foo", "bar"]) == ["foo", "bar"] assert _get_plugin_specs_as_list(("foo", "bar")) == ["foo", "bar"] def test_collect_pytest_prefix_bug_integration(pytester: Pytester) -> None: """Integration test for issue #3775""" p = pytester.copy_example("config/collect_pytest_prefix") result = pytester.runpytest(p) result.stdout.fnmatch_lines(["* 1 passed *"]) def test_collect_pytest_prefix_bug(pytestconfig): """Ensure we collect only actual functions from conftest files (#3775)""" class Dummy: class pytest_something: pass pm = pytestconfig.pluginmanager assert pm.parse_hookimpl_opts(Dummy(), "pytest_something") is None class TestRootdir: def test_simple_noini(self, tmp_path: Path, monkeypatch: MonkeyPatch) -> None: assert get_common_ancestor(Path.cwd(), [tmp_path]) == tmp_path a = tmp_path / "a" a.mkdir() assert get_common_ancestor(Path.cwd(), [a, tmp_path]) == tmp_path assert get_common_ancestor(Path.cwd(), [tmp_path, a]) == tmp_path monkeypatch.chdir(tmp_path) assert get_common_ancestor(Path.cwd(), []) == tmp_path no_path = tmp_path / "does-not-exist" assert get_common_ancestor(Path.cwd(), [no_path]) == tmp_path assert get_common_ancestor(Path.cwd(), [no_path / "a"]) == tmp_path @pytest.mark.parametrize( "name, contents", [ pytest.param("pytest.ini", "[pytest]\nx=10", id="pytest.ini"), pytest.param( "pyproject.toml", "[tool.pytest.ini_options]\nx=10", id="pyproject.toml" ), pytest.param("tox.ini", "[pytest]\nx=10", id="tox.ini"), pytest.param("setup.cfg", "[tool:pytest]\nx=10", id="setup.cfg"), ], ) def test_with_ini(self, tmp_path: Path, name: str, contents: str) -> None: inipath = tmp_path / name inipath.write_text(contents, encoding="utf-8") a = tmp_path / "a" a.mkdir() b = a / "b" b.mkdir() for args in ([str(tmp_path)], [str(a)], [str(b)]): rootpath, parsed_inipath, *_ = determine_setup( inifile=None, override_ini=None, args=args, rootdir_cmd_arg=None, invocation_dir=Path.cwd(), ) assert rootpath == tmp_path assert parsed_inipath == inipath rootpath, parsed_inipath, ini_config, _ = determine_setup( inifile=None, override_ini=None, args=[str(b), str(a)], rootdir_cmd_arg=None, invocation_dir=Path.cwd(), ) assert rootpath == tmp_path assert parsed_inipath == inipath assert ini_config["x"] == ConfigValue("10", origin="file", mode="ini") @pytest.mark.parametrize("pytest_ini", ["pytest.ini", ".pytest.ini"]) @pytest.mark.parametrize("other", ["setup.cfg", "tox.ini"]) def test_pytestini_overrides_empty_other( self, tmp_path: Path, pytest_ini: str, other: str ) -> None: inipath = tmp_path / pytest_ini inipath.touch() a = tmp_path / "a" a.mkdir() (a / other).touch() rootpath, parsed_inipath, *_ = determine_setup( inifile=None, override_ini=None, args=[str(a)], rootdir_cmd_arg=None, invocation_dir=Path.cwd(), ) assert rootpath == tmp_path assert parsed_inipath == inipath def test_setuppy_fallback(self, tmp_path: Path) -> None: a = tmp_path / "a" a.mkdir() (a / "setup.cfg").touch() (tmp_path / "setup.py").touch() rootpath, inipath, inicfg, _ = determine_setup( inifile=None, override_ini=None, args=[str(a)], rootdir_cmd_arg=None, invocation_dir=Path.cwd(), ) assert rootpath == tmp_path assert inipath is None assert inicfg == {} def test_nothing(self, tmp_path: Path, monkeypatch: MonkeyPatch) -> None: monkeypatch.chdir(tmp_path) rootpath, inipath, inicfg, _ = determine_setup( inifile=None, override_ini=None, args=[str(tmp_path)], rootdir_cmd_arg=None, invocation_dir=Path.cwd(), ) assert rootpath == tmp_path assert inipath is None assert inicfg == {} @pytest.mark.parametrize( "name, contents", [ # pytest.param("pytest.ini", "[pytest]\nx=10", id="pytest.ini"), pytest.param( "pyproject.toml", "[tool.pytest.ini_options]\nx=10", id="pyproject.toml" ), # pytest.param("tox.ini", "[pytest]\nx=10", id="tox.ini"), # pytest.param("setup.cfg", "[tool:pytest]\nx=10", id="setup.cfg"), ], ) def test_with_specific_inifile( self, tmp_path: Path, name: str, contents: str ) -> None: p = tmp_path / name p.touch() p.write_text(contents, encoding="utf-8") rootpath, inipath, ini_config, _ = determine_setup( inifile=str(p), override_ini=None, args=[str(tmp_path)], rootdir_cmd_arg=None, invocation_dir=Path.cwd(), ) assert rootpath == tmp_path assert inipath == p assert ini_config["x"] == ConfigValue("10", origin="file", mode="ini") def test_explicit_config_file_sets_rootdir( self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: tests_dir = tmp_path / "tests" tests_dir.mkdir() monkeypatch.chdir(tmp_path) # No config file is explicitly given: rootdir is determined to be cwd. rootpath, found_inipath, *_ = determine_setup( inifile=None, override_ini=None, args=[str(tests_dir)], rootdir_cmd_arg=None, invocation_dir=Path.cwd(), ) assert rootpath == tmp_path assert found_inipath is None # Config file is explicitly given: rootdir is determined to be inifile's directory. inipath = tmp_path / "pytest.ini" inipath.touch() rootpath, found_inipath, *_ = determine_setup( inifile=str(inipath), override_ini=None, args=[str(tests_dir)], rootdir_cmd_arg=None, invocation_dir=Path.cwd(), ) assert rootpath == tmp_path assert found_inipath == inipath @pytest.mark.skipif( sys.platform.startswith("win32"), reason="requires a POSIX null device" ) def test_non_regular_config_file_with_unrelated_args(self, tmp_path: Path) -> None: """A non-regular config file plus rootless args falls back to the invocation dir.""" rootpath, *_ = determine_setup( inifile=os.devnull, override_ini=None, args=[tmp_path.anchor], rootdir_cmd_arg=None, invocation_dir=tmp_path, ) assert rootpath == tmp_path @pytest.mark.skipif( sys.platform.startswith("win32"), reason="requires a POSIX null device" ) def test_non_regular_config_file_honours_explicit_rootdir( self, tmp_path: Path ) -> None: """``--rootdir`` still wins when the config file is not a regular file.""" explicit = tmp_path / "explicit" explicit.mkdir() rootpath, *_ = determine_setup( inifile=os.devnull, override_ini=None, args=[str(tmp_path)], rootdir_cmd_arg=str(explicit), invocation_dir=tmp_path, ) assert rootpath == explicit def test_with_arg_outside_cwd_without_inifile( self, tmp_path: Path, monkeypatch: MonkeyPatch ) -> None: monkeypatch.chdir(tmp_path) a = tmp_path / "a" a.mkdir() b = tmp_path / "b" b.mkdir() rootpath, inifile, *_ = determine_setup( inifile=None, override_ini=None, args=[str(a), str(b)], rootdir_cmd_arg=None, invocation_dir=Path.cwd(), ) assert rootpath == tmp_path assert inifile is None def test_with_arg_outside_cwd_with_inifile(self, tmp_path: Path) -> None: a = tmp_path / "a" a.mkdir() b = tmp_path / "b" b.mkdir() inipath = a / "pytest.ini" inipath.touch() rootpath, parsed_inipath, *_ = determine_setup( inifile=None, override_ini=None, args=[str(a), str(b)], rootdir_cmd_arg=None, invocation_dir=Path.cwd(), ) assert rootpath == a assert inipath == parsed_inipath @pytest.mark.parametrize("dirs", ([], ["does-not-exist"], ["a/does-not-exist"])) def test_with_non_dir_arg( self, dirs: Sequence[str], tmp_path: Path, monkeypatch: MonkeyPatch ) -> None: monkeypatch.chdir(tmp_path) rootpath, inipath, *_ = determine_setup( inifile=None, override_ini=None, args=dirs, rootdir_cmd_arg=None, invocation_dir=Path.cwd(), ) assert rootpath == tmp_path assert inipath is None def test_with_existing_file_in_subdir( self, tmp_path: Path, monkeypatch: MonkeyPatch ) -> None: a = tmp_path / "a" a.mkdir() (a / "exists").touch() monkeypatch.chdir(tmp_path) rootpath, inipath, *_ = determine_setup( inifile=None, override_ini=None, args=["a/exist"], rootdir_cmd_arg=None, invocation_dir=Path.cwd(), ) assert rootpath == tmp_path assert inipath is None def test_with_config_also_in_parent_directory( self, tmp_path: Path, monkeypatch: MonkeyPatch ) -> None: """Regression test for #7807.""" (tmp_path / "setup.cfg").write_text("[tool:pytest]\n", "utf-8") (tmp_path / "myproject").mkdir() (tmp_path / "myproject" / "setup.cfg").write_text("[tool:pytest]\n", "utf-8") (tmp_path / "myproject" / "tests").mkdir() monkeypatch.chdir(tmp_path / "myproject") rootpath, inipath, *_ = determine_setup( inifile=None, override_ini=None, args=["tests/"], rootdir_cmd_arg=None, invocation_dir=Path.cwd(), ) assert rootpath == tmp_path / "myproject" assert inipath == tmp_path / "myproject" / "setup.cfg" class TestOverrideIniArgs: @pytest.mark.parametrize("name", "setup.cfg tox.ini pytest.ini".split()) def test_override_ini_names(self, pytester: Pytester, name: str) -> None: section = "[pytest]" if name != "setup.cfg" else "[tool:pytest]" pytester.path.joinpath(name).write_text( textwrap.dedent( f""" {section} custom = 1.0""" ), encoding="utf-8", ) pytester.makeconftest( """ def pytest_addoption(parser): parser.addini("custom", "")""" ) pytester.makepyfile( """ def test_pass(pytestconfig): ini_val = pytestconfig.getini("custom") print('\\ncustom_option:%s\\n' % ini_val)""" ) result = pytester.runpytest("--override-ini", "custom=2.0", "-s") assert result.ret == 0 result.stdout.fnmatch_lines(["custom_option:2.0"]) result = pytester.runpytest( "--override-ini", "custom=2.0", "--override-ini=custom=3.0", "-s" ) assert result.ret == 0 result.stdout.fnmatch_lines(["custom_option:3.0"]) def test_override_ini_paths(self, pytester: Pytester) -> None: pytester.makeconftest( """ def pytest_addoption(parser): parser.addini("paths", "my new ini value", type="paths")""" ) pytester.makeini( """ [pytest] paths=blah.py""" ) pytester.makepyfile( r""" def test_overridden(pytestconfig): config_paths = pytestconfig.getini("paths") print(config_paths) for cpf in config_paths: print('\nuser_path:%s' % cpf.name) """ ) result = pytester.runpytest( "--override-ini", "paths=foo/bar1.py foo/bar2.py", "-s" ) result.stdout.fnmatch_lines(["user_path:bar1.py", "user_path:bar2.py"]) def test_override_multiple_and_default(self, pytester: Pytester) -> None: pytester.makeconftest( """ def pytest_addoption(parser): addini = parser.addini addini("custom_option_1", "", default="o1") addini("custom_option_2", "", default="o2") addini("custom_option_3", "", default=False, type="bool") addini("custom_option_4", "", default=True, type="bool")""" ) pytester.makeini( """ [pytest] custom_option_1=custom_option_1 custom_option_2=custom_option_2 """ ) pytester.makepyfile( """ def test_multiple_options(pytestconfig): prefix = "custom_option" for x in range(1, 5): ini_value=pytestconfig.getini("%s_%d" % (prefix, x)) print('\\nini%d:%s' % (x, ini_value)) """ ) result = pytester.runpytest( "--override-ini", "custom_option_1=fulldir=/tmp/user1", "-o", "custom_option_2=url=/tmp/user2?a=b&d=e", "-o", "custom_option_3=True", "-o", "custom_option_4=no", "-s", ) result.stdout.fnmatch_lines( [ "ini1:fulldir=/tmp/user1", "ini2:url=/tmp/user2?a=b&d=e", "ini3:True", "ini4:False", ] ) def test_override_ini_usage_error_bad_style(self, pytester: Pytester) -> None: pytester.makeini( """ [pytest] xdist_strict=False """ ) result = pytester.runpytest("--override-ini", "xdist_strict", "True") result.stderr.fnmatch_lines( [ "ERROR: -o/--override-ini expects option=value style (got: 'xdist_strict').", ] ) @pytest.mark.parametrize("with_ini", [True, False]) def test_override_ini_handled_asap( self, pytester: Pytester, with_ini: bool ) -> None: """-o should be handled as soon as possible and always override what's in config files (#2238)""" if with_ini: pytester.makeini( """ [pytest] python_files=test_*.py """ ) pytester.makepyfile( unittest_ini_handle=""" def test(): pass """ ) result = pytester.runpytest("--override-ini", "python_files=unittest_*.py") result.stdout.fnmatch_lines(["*1 passed in*"]) def test_addopts_before_initini( self, monkeypatch: MonkeyPatch, _config_for_test, _sys_snapshot ) -> None: cache_dir = ".custom_cache" monkeypatch.setenv("PYTEST_ADDOPTS", f"-o cache_dir={cache_dir}") config = _config_for_test config.parse([], addopts=True) assert config._inicfg.get("cache_dir") == ConfigValue( cache_dir, origin="override", mode="ini" ) def test_addopts_from_env_not_concatenated( self, monkeypatch: MonkeyPatch, _config_for_test ) -> None: """PYTEST_ADDOPTS should not take values from normal args (#4265).""" monkeypatch.setenv("PYTEST_ADDOPTS", "-o") config = _config_for_test with pytest.raises(UsageError) as excinfo: config.parse(["cache_dir=ignored"], addopts=True) assert ( "error: argument -o/--override-ini: expected one argument" in excinfo.value.args[0] ) assert "via PYTEST_ADDOPTS" in excinfo.value.args[0] def test_addopts_from_ini_not_concatenated(self, pytester: Pytester) -> None: """`addopts` from configuration should not take values from normal args (#4265).""" pytester.makeini( """ [pytest] addopts=-o """ ) result = pytester.runpytest("cache_dir=ignored") result.stderr.fnmatch_lines( [ "*: error: argument -o/--override-ini: expected one argument", " config source: via addopts config", ] ) assert result.ret == _pytest.config.ExitCode.USAGE_ERROR def test_override_ini_does_not_contain_paths( self, _config_for_test, _sys_snapshot ) -> None: """Check that -o no longer swallows all options after it (#3103)""" config = _config_for_test config.parse(["-o", "cache_dir=/cache", "/some/test/path"]) assert config._inicfg.get("cache_dir") == ConfigValue( "/cache", origin="override", mode="ini" ) def test_multiple_override_ini_options(self, pytester: Pytester) -> None: """Ensure a file path following a '-o' option does not generate an error (#3103)""" pytester.makepyfile( **{ "conftest.py": """ def pytest_addoption(parser): parser.addini('foo', default=None, help='some option') parser.addini('bar', default=None, help='some option') """, "test_foo.py": """ def test(pytestconfig): assert pytestconfig.getini('foo') == '1' assert pytestconfig.getini('bar') == '0' """, "test_bar.py": """ def test(): assert False """, } ) result = pytester.runpytest("-o", "foo=1", "-o", "bar=0", "test_foo.py") assert "ERROR:" not in result.stderr.str() result.stdout.fnmatch_lines(["collected 1 item", "*= 1 passed in *="]) def test_override_ini_without_config_file(self, pytester: Pytester) -> None: pytester.makepyfile(**{"src/override_ini_without_config_file.py": ""}) pytester.makepyfile( **{ "tests/test_override_ini_without_config_file.py": ( "import override_ini_without_config_file\ndef test(): pass" ), } ) result = pytester.runpytest("--override-ini", "pythonpath=src") result.assert_outcomes(passed=1) def test_override_ini_invalid_option(self, pytester: Pytester) -> None: result = pytester.runpytest("--override-ini", "doesnotexist=true") result.stdout.fnmatch_lines( [ "=*= warnings summary =*=", "*PytestConfigWarning:*Unknown config option: doesnotexist", ] ) def test_help_via_addopts(pytester: Pytester) -> None: pytester.makeini( """ [pytest] addopts = --unknown-option-should-allow-for-help --help """ ) result = pytester.runpytest() assert result.ret == 0 result.stdout.fnmatch_lines( [ "usage: *", "positional arguments:", # Displays full/default help. "to see available markers type: pytest --markers", ] ) def test_help_and_version_after_argument_error(pytester: Pytester) -> None: pytester.makeconftest( """ def validate(arg): raise argparse.ArgumentTypeError("argerror") def pytest_addoption(parser): group = parser.getgroup('cov') group.addoption( "--invalid-option-should-allow-for-help", type=validate, ) """ ) pytester.makeini( """ [pytest] addopts = --invalid-option-should-allow-for-help """ ) result = pytester.runpytest("--help") result.stdout.fnmatch_lines( [ "usage: *", "positional arguments:", "NOTE: displaying only minimal help due to UsageError.", ] ) result.stderr.fnmatch_lines( [ "ERROR: usage: *", "*: error: argument --invalid-option-should-allow-for-help: expected one argument", ] ) # Does not display full/default help. assert "to see available markers type: pytest --markers" not in result.stdout.lines assert result.ret == ExitCode.USAGE_ERROR result = pytester.runpytest("--version") result.stdout.fnmatch_lines([f"pytest {pytest.__version__}"]) assert result.ret == ExitCode.OK def test_help_formatter_uses_py_get_terminal_width(monkeypatch: MonkeyPatch) -> None: from _pytest.config.argparsing import DropShorterLongHelpFormatter monkeypatch.setenv("COLUMNS", "90") formatter = DropShorterLongHelpFormatter("prog") assert formatter._width == 90 monkeypatch.setattr("_pytest._io.get_terminal_width", lambda: 160) formatter = DropShorterLongHelpFormatter("prog") assert formatter._width == 160 formatter = DropShorterLongHelpFormatter("prog", width=42) assert formatter._width == 42 def test_config_does_not_load_blocked_plugin_from_args(pytester: Pytester) -> None: """This tests that pytest's config setup handles "-p no:X".""" p = pytester.makepyfile("def test(capfd): pass") result = pytester.runpytest(str(p), "-pno:capture") result.stdout.fnmatch_lines(["E fixture 'capfd' not found"]) assert result.ret == ExitCode.TESTS_FAILED result = pytester.runpytest(str(p), "-pno:capture", "-s") result.stderr.fnmatch_lines(["*: error: unrecognized arguments: -s"]) assert result.ret == ExitCode.USAGE_ERROR result = pytester.runpytest(str(p), "-p no:capture", "-s") result.stderr.fnmatch_lines(["*: error: unrecognized arguments: -s"]) assert result.ret == ExitCode.USAGE_ERROR result = pytester.runpytest(str(p), "-p no:/path/to/conftest.py", "-s") result.stderr.fnmatch_lines(["ERROR:*Blocking conftest files*"]) assert result.ret == ExitCode.USAGE_ERROR def test_invocation_args(pytester: Pytester) -> None: """Ensure that Config.invocation_* arguments are correctly defined""" class DummyPlugin: pass p = pytester.makepyfile("def test(): pass") plugin = DummyPlugin() rec = pytester.inline_run(p, "-v", plugins=[plugin]) calls = rec.getcalls("pytest_runtest_protocol") assert len(calls) == 1 call = calls[0] config = call.item.config assert config.invocation_params.args == (str(p), "-v") assert config.invocation_params.dir == pytester.path plugins = config.invocation_params.plugins assert len(plugins) == 2 assert plugins[0] is plugin # Installed by pytester.inline_run(). assert type(plugins[1]).__name__ == "PytesterHelperPlugin" # args cannot be None with pytest.raises(TypeError): Config.InvocationParams(args=None, plugins=None, dir=Path()) # type: ignore[arg-type] @pytest.mark.parametrize( "plugin", [ x for x in _pytest.config.default_plugins if x not in _pytest.config.essential_plugins ], ) def test_config_blocked_default_plugins(pytester: Pytester, plugin: str) -> None: p = pytester.makepyfile("def test(): pass") result = pytester.runpytest(str(p), f"-pno:{plugin}") if plugin == "python": assert result.ret == ExitCode.USAGE_ERROR result.stderr.fnmatch_lines( [ "ERROR: not found: */test_config_blocked_default_plugins.py", "(no match in any of *<Dir *>*", ] ) return assert result.ret == ExitCode.OK if plugin != "terminal": result.stdout.fnmatch_lines(["* 1 passed in *"]) p = pytester.makepyfile("def test(): assert 0") result = pytester.runpytest(str(p), f"-pno:{plugin}") assert result.ret == ExitCode.TESTS_FAILED if plugin != "terminal": result.stdout.fnmatch_lines(["* 1 failed in *"]) else: assert result.stdout.lines == [] class TestSetupCfg: def test_pytest_setup_cfg_unsupported(self, pytester: Pytester) -> None: pytester.makefile( ".cfg", setup=""" [pytest] addopts = --verbose """, ) with pytest.raises(pytest.fail.Exception): pytester.runpytest() def test_pytest_custom_cfg_unsupported(self, pytester: Pytester) -> None: pytester.makefile( ".cfg", custom=""" [pytest] addopts = --verbose """, ) with pytest.raises(pytest.fail.Exception): pytester.runpytest("-c", "custom.cfg") with pytest.raises(pytest.fail.Exception): pytester.runpytest("--config-file", "custom.cfg") class TestPytestPluginsVariable: def test_pytest_plugins_in_non_top_level_conftest_unsupported( self, pytester: Pytester ) -> None: pytester.makepyfile( **{ "subdirectory/conftest.py": """ pytest_plugins=['capture'] """ } ) pytester.makepyfile( """ def test_func(): pass """ ) res = pytester.runpytest() assert res.ret == 2 msg = "Defining 'pytest_plugins' in a non-top-level conftest is no longer supported" res.stdout.fnmatch_lines([f"*{msg}*", f"*subdirectory{os.sep}conftest.py*"]) @pytest.mark.parametrize("use_pyargs", [True, False]) def test_pytest_plugins_in_non_top_level_conftest_unsupported_pyargs( self, pytester: Pytester, use_pyargs: bool ) -> None: """When using --pyargs, do not emit the warning about non-top-level conftest warnings (#4039, #4044)""" files = { "src/pkg/__init__.py": "", "src/pkg/conftest.py": "", "src/pkg/test_root.py": "def test(): pass", "src/pkg/sub/__init__.py": "", "src/pkg/sub/conftest.py": "pytest_plugins=['capture']", "src/pkg/sub/test_bar.py": "def test(): pass", } pytester.makepyfile(**files) pytester.syspathinsert(pytester.path.joinpath("src")) args = ("--pyargs", "pkg") if use_pyargs else () res = pytester.runpytest(*args) assert res.ret == (0 if use_pyargs else 2) msg = "Defining 'pytest_plugins' in a non-top-level conftest is no longer supported" if use_pyargs: assert msg not in res.stdout.str() else: res.stdout.fnmatch_lines([f"*{msg}*"]) def test_pytest_plugins_in_non_top_level_conftest_unsupported_no_top_level_conftest( self, pytester: Pytester ) -> None: subdirectory = pytester.path.joinpath("subdirectory") subdirectory.mkdir() pytester.makeconftest( """ pytest_plugins=['capture'] """ ) pytester.path.joinpath("conftest.py").rename( subdirectory.joinpath("conftest.py") ) pytester.makepyfile( """ def test_func(): pass """ ) res = pytester.runpytest_subprocess() assert res.ret == 2 msg = "Defining 'pytest_plugins' in a non-top-level conftest is no longer supported" res.stdout.fnmatch_lines([f"*{msg}*", f"*subdirectory{os.sep}conftest.py*"]) def test_pytest_plugins_in_non_top_level_conftest_unsupported_no_false_positives( self, pytester: Pytester ) -> None: pytester.makepyfile( "def test_func(): pass", **{ "subdirectory/conftest": "pass", "conftest": """ import warnings warnings.filterwarnings('always', category=DeprecationWarning) pytest_plugins=['capture'] """, }, ) res = pytester.runpytest_subprocess() assert res.ret == 0 msg = "Defining 'pytest_plugins' in a non-top-level conftest is no longer supported" assert msg not in res.stdout.str() def test_conftest_import_error_repr(tmp_path: Path) -> None: """`ConftestImportFailure` should use a short error message and readable path to the failed conftest.py file.""" path = tmp_path.joinpath("foo/conftest.py") with pytest.raises( ConftestImportFailure, match=re.escape(f"RuntimeError: some error (from {path})"), ): try: raise RuntimeError("some error") except Exception as exc: raise ConftestImportFailure(path, cause=exc) from exc def test_strtobool() -> None: assert _strtobool("YES") assert not _strtobool("NO") with pytest.raises(ValueError): _strtobool("unknown") @pytest.mark.parametrize( "arg, escape, expected", [ ("ignore", False, ("ignore", "", Warning, "", 0)), ( "ignore::DeprecationWarning", False, ("ignore", "", DeprecationWarning, "", 0), ), ( "ignore:some msg:DeprecationWarning", False, ("ignore", "some msg", DeprecationWarning, "", 0), ), ( "ignore::DeprecationWarning:mod", False, ("ignore", "", DeprecationWarning, "mod", 0), ), ( "ignore::DeprecationWarning:mod:42", False, ("ignore", "", DeprecationWarning, "mod", 42), ), ("error:some\\msg:::", True, ("error", "some\\\\msg", Warning, "", 0)), ("error:::mod\\foo:", True, ("error", "", Warning, "mod\\\\foo\\Z", 0)), ], ) def test_parse_warning_filter( arg: str, escape: bool, expected: tuple[str, str, type[Warning], str, int] ) -> None: assert parse_warning_filter(arg, escape=escape) == expected @pytest.mark.parametrize( "arg", [ # Too much parts. ":" * 5, # Invalid action. "FOO::", # Class is not a Warning subclass. "::list::", # Negative line number. "::::-1", # Not a line number. "::::not-a-number", ], ) def test_parse_warning_filter_failure(arg: str) -> None: with pytest.raises(pytest.UsageError): parse_warning_filter(arg, escape=True) class TestDebugOptions: def test_without_debug_does_not_write_log(self, pytester: Pytester) -> None: result = pytester.runpytest() result.stderr.no_fnmatch_line( "*writing pytest debug information to*pytestdebug.log" ) result.stderr.no_fnmatch_line( "*wrote pytest debug information to*pytestdebug.log" ) assert not [f.name for f in pytester.path.glob("**/*.log")] def test_with_only_debug_writes_pytestdebug_log(self, pytester: Pytester) -> None: result = pytester.runpytest("--debug") result.stderr.fnmatch_lines( [ "*writing pytest debug information to*pytestdebug.log", "*wrote pytest debug information to*pytestdebug.log", ] ) assert "pytestdebug.log" in [f.name for f in pytester.path.glob("**/*.log")] def test_multiple_custom_debug_logs(self, pytester: Pytester) -> None: result = pytester.runpytest("--debug", "bar.log") result.stderr.fnmatch_lines( [ "*writing pytest debug information to*bar.log", "*wrote pytest debug information to*bar.log", ] ) result = pytester.runpytest("--debug", "foo.log") result.stderr.fnmatch_lines( [ "*writing pytest debug information to*foo.log", "*wrote pytest debug information to*foo.log", ] ) assert {"bar.log", "foo.log"} == { f.name for f in pytester.path.glob("**/*.log") } def test_debug_help(self, pytester: Pytester) -> None: result = pytester.runpytest("-h") result.stdout.fnmatch_lines( [ "*Store internal tracing debug information in this log*", "*file. This file is opened with 'w' and truncated as a*", "*Default: pytestdebug.log.", ] ) class TestVerbosity: SOME_OUTPUT_TYPE = Config.VERBOSITY_ASSERTIONS SOME_OUTPUT_VERBOSITY_LEVEL = 5 class VerbosityIni: def pytest_addoption(self, parser: Parser) -> None: Config._add_verbosity_ini( parser, TestVerbosity.SOME_OUTPUT_TYPE, help="some help text" ) def test_level_matches_verbose_when_not_specified( self, pytester: Pytester, tmp_path: Path ) -> None: tmp_path.joinpath("pytest.ini").write_text( textwrap.dedent( """\ [pytest] addopts = --verbose """ ), encoding="utf-8", ) pytester.plugins = [TestVerbosity.VerbosityIni()] config = pytester.parseconfig(tmp_path) assert ( config.get_verbosity(TestVerbosity.SOME_OUTPUT_TYPE) == config.option.verbose ) def test_level_matches_verbose_when_not_known_type( self, pytester: Pytester, tmp_path: Path ) -> None: tmp_path.joinpath("pytest.ini").write_text( textwrap.dedent( """\ [pytest] addopts = --verbose """ ), encoding="utf-8", ) pytester.plugins = [TestVerbosity.VerbosityIni()] config = pytester.parseconfig(tmp_path) assert config.get_verbosity("some fake verbosity type") == config.option.verbose def test_level_matches_specified_override( self, pytester: Pytester, tmp_path: Path ) -> None: setting_name = f"verbosity_{TestVerbosity.SOME_OUTPUT_TYPE}" tmp_path.joinpath("pytest.ini").write_text( textwrap.dedent( f"""\ [pytest] addopts = --verbose {setting_name} = {TestVerbosity.SOME_OUTPUT_VERBOSITY_LEVEL} """ ), encoding="utf-8", ) pytester.plugins = [TestVerbosity.VerbosityIni()] config = pytester.parseconfig(tmp_path) assert ( config.get_verbosity(TestVerbosity.SOME_OUTPUT_TYPE) == TestVerbosity.SOME_OUTPUT_VERBOSITY_LEVEL ) class TestNativeTomlConfig: """Test native TOML configuration parsing.""" def test_values(self, pytester: Pytester) -> None: """Test that values are parsed as expected in TOML mode.""" pytester.makepyprojecttoml( """ [tool.pytest] test_bool = true test_int = 5 test_float = 30.5 test_args = ["tests", "integration"] test_paths = ["src", "lib"] """ ) pytester.makeconftest( """ def pytest_addoption(parser): parser.addini("test_bool", "Test boolean config", type="bool", default=False) parser.addini("test_int", "Test integer config", type="int", default=0) parser.addini("test_float", "Test float config", type="float", default=0.0) parser.addini("test_args", "Test args config", type="args") parser.addini("test_paths", "Test paths config", type="paths") """ ) config = pytester.parseconfig() assert config.getini("test_bool") is True assert config.getini("test_int") == 5 assert config.getini("test_float") == 30.5 assert config.getini("test_args") == ["tests", "integration"] paths = config.getini("test_paths") assert len(paths) == 2 # Paths should be resolved relative to pyproject.toml location. assert all(isinstance(p, Path) for p in paths) def test_override_with_list(self, pytester: Pytester) -> None: """Test that -o overrides work with INI-style list syntax even when config uses TOML mode.""" pytester.makepyprojecttoml( """ [tool.pytest] test_override_list = ["tests"] """ ) pytester.makeconftest( """ def pytest_addoption(parser): parser.addini("test_override_list", "Test override list", type="args") """ ) # -o uses INI mode, so uses space-separated syntax. config = pytester.parseconfig("-o", "test_override_list=tests integration") assert config.getini("test_override_list") == ["tests", "integration"] def test_conflict_between_native_and_ini_options(self, pytester: Pytester) -> None: """Test that using both [tool.pytest] and [tool.pytest.ini_options] fails.""" pytester.makepyprojecttoml( """ [tool.pytest] test_conflict_1 = true [tool.pytest.ini_options] test_conflict_2 = true """, ) pytester.makeconftest( """ def pytest_addoption(parser): parser.addini("test_conflict_1", "Test conflict config 1", type="bool") parser.addini("test_conflict_2", "Test conflict config 2", type="bool") """ ) with pytest.raises(UsageError, match="Cannot use both"): pytester.parseconfig() def test_type_errors(self, pytester: Pytester) -> None: """Test all invalid-type cases in getini, reported as UsageError.""" pytester.maketoml( """ [pytest] paths_not_list = "should_be_list" paths_list_with_int = [1, 2] args_not_list = 123 args_list_with_int = ["valid", 456] linelist_not_list = true linelist_list_with_bool = ["valid", false] bool_not_bool = "true" int_not_int = "123" int_is_bool = true float_not_float = "3.14" float_is_bool = false string_not_string = 123 """ ) pytester.makeconftest( """ def pytest_addoption(parser): parser.addini("paths_not_list", "test", type="paths") parser.addini("paths_list_with_int", "test", type="paths") parser.addini("args_not_list", "test", type="args") parser.addini("args_list_with_int", "test", type="args") parser.addini("linelist_not_list", "test", type="linelist") parser.addini("linelist_list_with_bool", "test", type="linelist") parser.addini("bool_not_bool", "test", type="bool") parser.addini("int_not_int", "test", type="int") parser.addini("int_is_bool", "test", type="int") parser.addini("float_not_float", "test", type="float") parser.addini("float_is_bool", "test", type="float") parser.addini("string_not_string", "test", type="string") """ ) config = pytester.parseconfig() with pytest.raises( UsageError, match=r"expects a list for type 'paths'.*got str" ): config.getini("paths_not_list") with pytest.raises( UsageError, match=r"expects a list of strings.*item at index 0 is int" ): config.getini("paths_list_with_int") with pytest.raises( UsageError, match=r"expects a list for type 'args'.*got int" ): config.getini("args_not_list") with pytest.raises( UsageError, match=r"expects a list of strings.*item at index 1 is int" ): config.getini("args_list_with_int") with pytest.raises( UsageError, match=r"expects a list for type 'linelist'.*got bool" ): config.getini("linelist_not_list") with pytest.raises( UsageError, match=r"expects a list of strings.*item at index 1 is bool" ): config.getini("linelist_list_with_bool") with pytest.raises(UsageError, match=r"expects a bool.*got str"): config.getini("bool_not_bool") with pytest.raises(UsageError, match=r"expects an int.*got str"): config.getini("int_not_int") with pytest.raises(UsageError, match=r"expects an int.*got bool"): config.getini("int_is_bool") with pytest.raises(UsageError, match=r"expects a float.*got str"): config.getini("float_not_float") with pytest.raises(UsageError, match=r"expects a float.*got bool"): config.getini("float_is_bool") with pytest.raises(UsageError, match=r"expects a string.*got int"): config.getini("string_not_string") class TestInicfgDeprecation: """Tests for the deprecation of config.inicfg.""" def test_inicfg_deprecated(self, pytester: Pytester) -> None: """Test that accessing config.inicfg issues a deprecation warning.""" pytester.makeini( """ [pytest] minversion = 3.0 """ ) config = pytester.parseconfig() with pytest.warns( PytestDeprecationWarning, match=r"config\.inicfg is deprecated" ): inicfg = config.inicfg # type: ignore[deprecated] assert config.getini("minversion") == "3.0" assert inicfg["minversion"] == "3.0" assert inicfg.get("minversion") == "3.0" del inicfg["minversion"] inicfg["minversion"] = "4.0" assert list(inicfg.keys()) == ["minversion"] assert list(inicfg.items()) == [("minversion", "4.0")] assert len(inicfg) == 1 def test_issue_13946_setting_bool_no_longer_crashes( self, pytester: Pytester ) -> None: """Regression test for #13946 - setting inicfg doesn't cause a crash.""" pytester.makepyfile( """ def pytest_configure(config): config.inicfg["xfail_strict"] = True def test(): pass """ ) result = pytester.runpytest() assert result.ret == 0 class TestProgName: """Test program name display in help and error messages (issue #1764).""" def test_get_prog_name_direct_pytest(self) -> None: """When argv[0] is a pytest entry point, prog should be 'pytest'.""" assert _get_prog_name(["/usr/bin/pytest", "--help"]) == "pytest" assert _get_prog_name(["pytest", "-v"]) == "pytest" def test_get_prog_name_python_m_pytest(self) -> None: """When argv[0] is __main__.py, prog should be 'python -m pytest'.""" assert ( _get_prog_name(["/path/to/site-packages/pytest/__main__.py", "--help"]) == "python -m pytest" ) assert _get_prog_name(["__main__.py", "-v"]) == "python -m pytest" def test_get_prog_name_empty_argv(self) -> None: """When argv is empty, should default to 'pytest'.""" assert _get_prog_name([]) == "pytest" def test_prog_in_error_message_programmatic(self, pytester: Pytester) -> None: """Error messages should show 'pytest.main()' when called programmatically. runpytest_inprocess calls pytest.main() directly, so it should show pytest.main() as the program name. """ result = pytester.runpytest_inprocess("--invalid-option-xyz") result.stderr.fnmatch_lines(["*pytest.main(): error:*invalid-option-xyz*"]) def test_prog_in_error_message_cli(self, pytester: Pytester) -> None: """Error messages should show 'python -m pytest' when called from CLI subprocess. runpytest_subprocess runs pytest via 'python -m pytest', so it should show 'python -m pytest' as the program name. """ result = pytester.runpytest_subprocess("--invalid-option-xyz") result.stderr.fnmatch_lines(["*python -m pytest: error:*invalid-option-xyz*"]) def test_prog_in_usage_programmatic(self, pytester: Pytester) -> None: """Usage line should show 'pytest.main()' when called programmatically.""" result = pytester.runpytest_inprocess("--help") result.stdout.fnmatch_lines(["usage: pytest.main() *"]) def test_prog_in_usage_cli(self, pytester: Pytester) -> None: """Usage line should show 'python -m pytest' when called from CLI subprocess.""" result = pytester.runpytest_subprocess("--help") result.stdout.fnmatch_lines(["usage: python -m pytest *"]) def test_console_main_deprecated(self, monkeypatch: pytest.MonkeyPatch) -> None: """Calling pytest.console_main() should emit a deprecation warning.""" monkeypatch.setattr("_pytest.config._console_main", lambda: 0) with pytest.warns( pytest.PytestRemovedIn10Warning, match="pytest.console_main.*is deprecated", ): console_main()