Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
264 changes: 131 additions & 133 deletions src/_pytest/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,29 @@ def filter_traceback_for_conftest_import_failure(
return filter_traceback(entry) and "importlib" not in str(entry.path).split(os.sep)


def print_conftest_import_error(e: ConftestImportFailure, file: TextIO) -> None:
exc_info = ExceptionInfo.from_exception(e.cause)
tw = TerminalWriter(file)
tw.line(f"ImportError while loading conftest '{e.path}'.", red=True)
exc_info.traceback = exc_info.traceback.filter(
filter_traceback_for_conftest_import_failure
)
exc_repr = (
exc_info.getrepr(style="short", chain=False)
if exc_info.traceback
else exc_info.exconly()
)
formatted_tb = str(exc_repr)
for line in formatted_tb.splitlines():
tw.line(line.rstrip(), red=True)


def print_usage_error(e: UsageError, file: TextIO) -> None:
tw = TerminalWriter(file)
for msg in e.args:
tw.line(f"ERROR: {msg}\n", red=True)


def main(
args: list[str] | os.PathLike[str] | None = None,
plugins: Sequence[str | _PluggyPlugin] | None = None,
Expand All @@ -167,34 +190,19 @@ def main(
try:
config = _prepareconfig(new_args, plugins)
except ConftestImportFailure as e:
exc_info = ExceptionInfo.from_exception(e.cause)
tw = TerminalWriter(sys.stderr)
tw.line(f"ImportError while loading conftest '{e.path}'.", red=True)
exc_info.traceback = exc_info.traceback.filter(
filter_traceback_for_conftest_import_failure
)
exc_repr = (
exc_info.getrepr(style="short", chain=False)
if exc_info.traceback
else exc_info.exconly()
)
formatted_tb = str(exc_repr)
for line in formatted_tb.splitlines():
tw.line(line.rstrip(), red=True)
print_conftest_import_error(e, file=sys.stderr)
return ExitCode.USAGE_ERROR
else:

try:
ret: ExitCode | int = config.hook.pytest_cmdline_main(config=config)
try:
ret: ExitCode | int = config.hook.pytest_cmdline_main(config=config)
try:
return ExitCode(ret)
except ValueError:
return ret
finally:
config._ensure_unconfigure()
return ExitCode(ret)
except ValueError:
return ret
finally:
config._ensure_unconfigure()
except UsageError as e:
tw = TerminalWriter(sys.stderr)
for msg in e.args:
tw.line(f"ERROR: {msg}\n", red=True)
print_usage_error(e, file=sys.stderr)
return ExitCode.USAGE_ERROR
finally:
if old_pytest_version is None:
Expand Down Expand Up @@ -1005,7 +1013,7 @@ class InvocationParams:
plugins: Sequence[str | _PluggyPlugin] | None
"""Extra plugins, might be `None`."""
dir: pathlib.Path
"""The directory from which :func:`pytest.main` was invoked. :type: pathlib.Path"""
"""The directory from which :func:`pytest.main` was invoked."""

def __init__(
self,
Expand Down Expand Up @@ -1041,9 +1049,6 @@ def __init__(
*,
invocation_params: InvocationParams | None = None,
) -> None:
from .argparsing import FILE_OR_DIR
from .argparsing import Parser

if invocation_params is None:
invocation_params = self.InvocationParams(
args=(), plugins=None, dir=pathlib.Path.cwd()
Expand All @@ -1061,9 +1066,8 @@ def __init__(
:type: InvocationParams
"""

_a = FILE_OR_DIR
self._parser = Parser(
usage=f"%(prog)s [options] [{_a}] [{_a}] [...]",
usage=f"%(prog)s [options] [{FILE_OR_DIR}] [{FILE_OR_DIR}] [...]",
processopt=self._processopt,
_ispytest=True,
)
Expand Down Expand Up @@ -1099,8 +1103,6 @@ def __init__(
def rootpath(self) -> pathlib.Path:
"""The path to the :ref:`rootdir <rootdir>`.

:type: pathlib.Path

.. versionadded:: 6.1
"""
return self._rootpath
Expand Down Expand Up @@ -1163,7 +1165,7 @@ def pytest_cmdline_parse(
elif (
getattr(self.option, "help", False) or "--help" in args or "-h" in args
):
self._parser._getparser().print_help()
self._parser.optparser.print_help()
sys.stdout.write(
"\nNOTE: displaying only minimal help due to UsageError.\n\n"
)
Expand Down Expand Up @@ -1246,19 +1248,18 @@ def pytest_load_initial_conftests(self, early_config: Config) -> None:
),
)

def _consider_importhook(self, args: Sequence[str]) -> None:
def _consider_importhook(self) -> None:
"""Install the PEP 302 import hook if using assertion rewriting.

Needs to parse the --assert=<mode> option from the commandline
and find all the installed plugins to mark them for rewriting
by the importhook.
"""
ns, _unknown_args = self._parser.parse_known_and_unknown_args(args)
mode = getattr(ns, "assertmode", "plain")
mode = getattr(self.known_args_namespace, "assertmode", "plain")

disable_autoload = getattr(ns, "disable_plugin_autoload", False) or bool(
os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD")
)
disable_autoload = getattr(
self.known_args_namespace, "disable_plugin_autoload", False
) or bool(os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"))
if mode == "rewrite":
import _pytest.assertion

Expand Down Expand Up @@ -1362,94 +1363,6 @@ def _decide_args(
result = [str(invocation_dir)]
return result, source

def _preparse(self, args: list[str], addopts: bool = True) -> None:
if addopts:
env_addopts = os.environ.get("PYTEST_ADDOPTS", "")
if len(env_addopts):
args[:] = (
self._validate_args(shlex.split(env_addopts), "via PYTEST_ADDOPTS")
+ args
)

ns, unknown_args = self._parser.parse_known_and_unknown_args(
args, namespace=copy.copy(self.option)
)
rootpath, inipath, inicfg, ignored_config_files = determine_setup(
inifile=ns.inifilename,
override_ini=ns.override_ini,
args=ns.file_or_dir + unknown_args,
rootdir_cmd_arg=ns.rootdir or None,
invocation_dir=self.invocation_params.dir,
)
self._rootpath = rootpath
self._inipath = inipath
self._ignored_config_files = ignored_config_files
self.inicfg = inicfg
self._parser.extra_info["rootdir"] = str(self.rootpath)
self._parser.extra_info["inifile"] = str(self.inipath)
self._parser.addini("addopts", "Extra command line options", "args")
self._parser.addini("minversion", "Minimally required pytest version")
self._parser.addini(
"pythonpath", type="paths", help="Add paths to sys.path", default=[]
)
self._parser.addini(
"required_plugins",
"Plugins that must be present for pytest to run",
type="args",
default=[],
)

if addopts:
args[:] = (
self._validate_args(self.getini("addopts"), "via addopts config") + args
)

self.known_args_namespace = self._parser.parse_known_args(
args, namespace=copy.copy(self.option)
)
self._checkversion()
self._consider_importhook(args)
self._configure_python_path()
self.pluginmanager.consider_preparse(args, exclude_only=False)
if (
not os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD")
and not self.known_args_namespace.disable_plugin_autoload
):
# Autoloading from distribution package entry point has
# not been disabled.
self.pluginmanager.load_setuptools_entrypoints("pytest11")
# Otherwise only plugins explicitly specified in PYTEST_PLUGINS
# are going to be loaded.
self.pluginmanager.consider_env()

self.known_args_namespace = self._parser.parse_known_args(
args, namespace=copy.copy(self.known_args_namespace)
)

self._validate_plugins()
self._warn_about_skipped_plugins()

if self.known_args_namespace.confcutdir is None:
if self.inipath is not None:
confcutdir = str(self.inipath.parent)
else:
confcutdir = str(self.rootpath)
self.known_args_namespace.confcutdir = confcutdir
try:
self.hook.pytest_load_initial_conftests(
early_config=self, args=args, parser=self._parser
)
except ConftestImportFailure as e:
if self.known_args_namespace.help or self.known_args_namespace.version:
# we don't want to prevent --help/--version to work
# so just let it pass and print a warning at the end
self.issue_config_time_warning(
PytestConfigWarning(f"could not load initial conftests: {e.path}"),
stacklevel=2,
)
else:
raise

@hookimpl(wrapper=True)
def pytest_collection(self) -> Generator[None, object, object]:
# Validate invalid configuration keys after collection is done so we
Expand Down Expand Up @@ -1533,18 +1446,103 @@ def parse(self, args: list[str], addopts: bool = True) -> None:
assert self.args == [], (
"can only parse cmdline args at most once per Config object"
)

self.hook.pytest_addhooks.call_historic(
kwargs=dict(pluginmanager=self.pluginmanager)
)
self._preparse(args, addopts=addopts)
self._parser.after_preparse = True # type: ignore

if addopts:
env_addopts = os.environ.get("PYTEST_ADDOPTS", "")
if len(env_addopts):
args[:] = (
self._validate_args(shlex.split(env_addopts), "via PYTEST_ADDOPTS")
+ args
)

ns = self._parser.parse_known_args(args, namespace=copy.copy(self.option))
rootpath, inipath, inicfg, ignored_config_files = determine_setup(
inifile=ns.inifilename,
override_ini=ns.override_ini,
args=ns.file_or_dir,
rootdir_cmd_arg=ns.rootdir or None,
invocation_dir=self.invocation_params.dir,
)
self._rootpath = rootpath
self._inipath = inipath
self._ignored_config_files = ignored_config_files
self.inicfg = inicfg
self._parser.extra_info["rootdir"] = str(self.rootpath)
self._parser.extra_info["inifile"] = str(self.inipath)

self._parser.addini("addopts", "Extra command line options", "args")
self._parser.addini("minversion", "Minimally required pytest version")
self._parser.addini(
"pythonpath", type="paths", help="Add paths to sys.path", default=[]
)
self._parser.addini(
"required_plugins",
"Plugins that must be present for pytest to run",
type="args",
default=[],
)

if addopts:
args[:] = (
self._validate_args(self.getini("addopts"), "via addopts config") + args
)

self.known_args_namespace = self._parser.parse_known_args(
args, namespace=copy.copy(self.option)
)
self._checkversion()
self._consider_importhook()
self._configure_python_path()
self.pluginmanager.consider_preparse(args, exclude_only=False)
if (
not os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD")
and not self.known_args_namespace.disable_plugin_autoload
):
# Autoloading from distribution package entry point has
# not been disabled.
self.pluginmanager.load_setuptools_entrypoints("pytest11")
# Otherwise only plugins explicitly specified in PYTEST_PLUGINS
# are going to be loaded.
self.pluginmanager.consider_env()

self._parser.parse_known_args(args, namespace=self.known_args_namespace)

self._validate_plugins()
self._warn_about_skipped_plugins()

if self.known_args_namespace.confcutdir is None:
if self.inipath is not None:
confcutdir = str(self.inipath.parent)
else:
confcutdir = str(self.rootpath)
self.known_args_namespace.confcutdir = confcutdir
try:
self.hook.pytest_load_initial_conftests(
early_config=self, args=args, parser=self._parser
)
except ConftestImportFailure as e:
if self.known_args_namespace.help or self.known_args_namespace.version:
# we don't want to prevent --help/--version to work
# so just let it pass and print a warning at the end
self.issue_config_time_warning(
PytestConfigWarning(f"could not load initial conftests: {e.path}"),
stacklevel=2,
)
else:
raise

try:
parsed = self._parser.parse(args, namespace=self.option)
self._parser.parse(args, namespace=self.option)
except PrintHelp:
return

self.args, self.args_source = self._decide_args(
args=getattr(parsed, FILE_OR_DIR),
pyargs=self.known_args_namespace.pyargs,
args=getattr(self.option, FILE_OR_DIR),
pyargs=self.option.pyargs,
testpaths=self.getini("testpaths"),
invocation_dir=self.invocation_params.dir,
rootpath=self.rootpath,
Expand Down
Loading