Merge pull request #13864 from bluetech/config-cleanups-2 · pytest-dev/pytest@adb3658

@@ -142,6 +142,29 @@ def filter_traceback_for_conftest_import_failure(

142142

return filter_traceback(entry) and "importlib" not in str(entry.path).split(os.sep)

143143144144145+

def print_conftest_import_error(e: ConftestImportFailure, file: TextIO) -> None:

146+

exc_info = ExceptionInfo.from_exception(e.cause)

147+

tw = TerminalWriter(file)

148+

tw.line(f"ImportError while loading conftest '{e.path}'.", red=True)

149+

exc_info.traceback = exc_info.traceback.filter(

150+

filter_traceback_for_conftest_import_failure

151+

)

152+

exc_repr = (

153+

exc_info.getrepr(style="short", chain=False)

154+

if exc_info.traceback

155+

else exc_info.exconly()

156+

)

157+

formatted_tb = str(exc_repr)

158+

for line in formatted_tb.splitlines():

159+

tw.line(line.rstrip(), red=True)

160+161+162+

def print_usage_error(e: UsageError, file: TextIO) -> None:

163+

tw = TerminalWriter(file)

164+

for msg in e.args:

165+

tw.line(f"ERROR: {msg}\n", red=True)

166+167+145168

def main(

146169

args: list[str] | os.PathLike[str] | None = None,

147170

plugins: Sequence[str | _PluggyPlugin] | None = None,

@@ -167,34 +190,19 @@ def main(

167190

try:

168191

config = _prepareconfig(new_args, plugins)

169192

except ConftestImportFailure as e:

170-

exc_info = ExceptionInfo.from_exception(e.cause)

171-

tw = TerminalWriter(sys.stderr)

172-

tw.line(f"ImportError while loading conftest '{e.path}'.", red=True)

173-

exc_info.traceback = exc_info.traceback.filter(

174-

filter_traceback_for_conftest_import_failure

175-

)

176-

exc_repr = (

177-

exc_info.getrepr(style="short", chain=False)

178-

if exc_info.traceback

179-

else exc_info.exconly()

180-

)

181-

formatted_tb = str(exc_repr)

182-

for line in formatted_tb.splitlines():

183-

tw.line(line.rstrip(), red=True)

193+

print_conftest_import_error(e, file=sys.stderr)

184194

return ExitCode.USAGE_ERROR

185-

else:

195+196+

try:

197+

ret: ExitCode | int = config.hook.pytest_cmdline_main(config=config)

186198

try:

187-

ret: ExitCode | int = config.hook.pytest_cmdline_main(config=config)

188-

try:

189-

return ExitCode(ret)

190-

except ValueError:

191-

return ret

192-

finally:

193-

config._ensure_unconfigure()

199+

return ExitCode(ret)

200+

except ValueError:

201+

return ret

202+

finally:

203+

config._ensure_unconfigure()

194204

except UsageError as e:

195-

tw = TerminalWriter(sys.stderr)

196-

for msg in e.args:

197-

tw.line(f"ERROR: {msg}\n", red=True)

205+

print_usage_error(e, file=sys.stderr)

198206

return ExitCode.USAGE_ERROR

199207

finally:

200208

if old_pytest_version is None:

@@ -1006,7 +1014,7 @@ class InvocationParams:

10061014

plugins: Sequence[str | _PluggyPlugin] | None

10071015

"""Extra plugins, might be `None`."""

10081016

dir: pathlib.Path

1009-

"""The directory from which :func:`pytest.main` was invoked. :type: pathlib.Path"""

1017+

"""The directory from which :func:`pytest.main` was invoked."""

1010101810111019

def __init__(

10121020

self,

@@ -1042,9 +1050,6 @@ def __init__(

10421050

*,

10431051

invocation_params: InvocationParams | None = None,

10441052

) -> None:

1045-

from .argparsing import FILE_OR_DIR

1046-

from .argparsing import Parser

1047-10481053

if invocation_params is None:

10491054

invocation_params = self.InvocationParams(

10501055

args=(), plugins=None, dir=pathlib.Path.cwd()

@@ -1062,9 +1067,8 @@ def __init__(

10621067

:type: InvocationParams

10631068

"""

106410691065-

_a = FILE_OR_DIR

10661070

self._parser = Parser(

1067-

usage=f"%(prog)s [options] [{_a}] [{_a}] [...]",

1071+

usage=f"%(prog)s [options] [{FILE_OR_DIR}] [{FILE_OR_DIR}] [...]",

10681072

processopt=self._processopt,

10691073

_ispytest=True,

10701074

)

@@ -1100,8 +1104,6 @@ def __init__(

11001104

def rootpath(self) -> pathlib.Path:

11011105

"""The path to the :ref:`rootdir <rootdir>`.

110211061103-

:type: pathlib.Path

1104-11051107

.. versionadded:: 6.1

11061108

"""

11071109

return self._rootpath

@@ -1164,7 +1166,7 @@ def pytest_cmdline_parse(

11641166

elif (

11651167

getattr(self.option, "help", False) or "--help" in args or "-h" in args

11661168

):

1167-

self._parser._getparser().print_help()

1169+

self._parser.optparser.print_help()

11681170

sys.stdout.write(

11691171

"\nNOTE: displaying only minimal help due to UsageError.\n\n"

11701172

)

@@ -1247,19 +1249,18 @@ def pytest_load_initial_conftests(self, early_config: Config) -> None:

12471249

),

12481250

)

124912511250-

def _consider_importhook(self, args: Sequence[str]) -> None:

1252+

def _consider_importhook(self) -> None:

12511253

"""Install the PEP 302 import hook if using assertion rewriting.

1252125412531255

Needs to parse the --assert=<mode> option from the commandline

12541256

and find all the installed plugins to mark them for rewriting

12551257

by the importhook.

12561258

"""

1257-

ns, _unknown_args = self._parser.parse_known_and_unknown_args(args)

1258-

mode = getattr(ns, "assertmode", "plain")

1259+

mode = getattr(self.known_args_namespace, "assertmode", "plain")

125912601260-

disable_autoload = getattr(ns, "disable_plugin_autoload", False) or bool(

1261-

os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD")

1262-

)

1261+

disable_autoload = getattr(

1262+

self.known_args_namespace, "disable_plugin_autoload", False

1263+

) or bool(os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"))

12631264

if mode == "rewrite":

12641265

import _pytest.assertion

12651266

@@ -1363,94 +1364,6 @@ def _decide_args(

13631364

result = [str(invocation_dir)]

13641365

return result, source

136513661366-

def _preparse(self, args: list[str], addopts: bool = True) -> None:

1367-

if addopts:

1368-

env_addopts = os.environ.get("PYTEST_ADDOPTS", "")

1369-

if len(env_addopts):

1370-

args[:] = (

1371-

self._validate_args(shlex.split(env_addopts), "via PYTEST_ADDOPTS")

1372-

+ args

1373-

)

1374-1375-

ns, unknown_args = self._parser.parse_known_and_unknown_args(

1376-

args, namespace=copy.copy(self.option)

1377-

)

1378-

rootpath, inipath, inicfg, ignored_config_files = determine_setup(

1379-

inifile=ns.inifilename,

1380-

override_ini=ns.override_ini,

1381-

args=ns.file_or_dir + unknown_args,

1382-

rootdir_cmd_arg=ns.rootdir or None,

1383-

invocation_dir=self.invocation_params.dir,

1384-

)

1385-

self._rootpath = rootpath

1386-

self._inipath = inipath

1387-

self._ignored_config_files = ignored_config_files

1388-

self.inicfg = inicfg

1389-

self._parser.extra_info["rootdir"] = str(self.rootpath)

1390-

self._parser.extra_info["inifile"] = str(self.inipath)

1391-

self._parser.addini("addopts", "Extra command line options", "args")

1392-

self._parser.addini("minversion", "Minimally required pytest version")

1393-

self._parser.addini(

1394-

"pythonpath", type="paths", help="Add paths to sys.path", default=[]

1395-

)

1396-

self._parser.addini(

1397-

"required_plugins",

1398-

"Plugins that must be present for pytest to run",

1399-

type="args",

1400-

default=[],

1401-

)

1402-1403-

if addopts:

1404-

args[:] = (

1405-

self._validate_args(self.getini("addopts"), "via addopts config") + args

1406-

)

1407-1408-

self.known_args_namespace = self._parser.parse_known_args(

1409-

args, namespace=copy.copy(self.option)

1410-

)

1411-

self._checkversion()

1412-

self._consider_importhook(args)

1413-

self._configure_python_path()

1414-

self.pluginmanager.consider_preparse(args, exclude_only=False)

1415-

if (

1416-

not os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD")

1417-

and not self.known_args_namespace.disable_plugin_autoload

1418-

):

1419-

# Autoloading from distribution package entry point has

1420-

# not been disabled.

1421-

self.pluginmanager.load_setuptools_entrypoints("pytest11")

1422-

# Otherwise only plugins explicitly specified in PYTEST_PLUGINS

1423-

# are going to be loaded.

1424-

self.pluginmanager.consider_env()

1425-1426-

self.known_args_namespace = self._parser.parse_known_args(

1427-

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

1428-

)

1429-1430-

self._validate_plugins()

1431-

self._warn_about_skipped_plugins()

1432-1433-

if self.known_args_namespace.confcutdir is None:

1434-

if self.inipath is not None:

1435-

confcutdir = str(self.inipath.parent)

1436-

else:

1437-

confcutdir = str(self.rootpath)

1438-

self.known_args_namespace.confcutdir = confcutdir

1439-

try:

1440-

self.hook.pytest_load_initial_conftests(

1441-

early_config=self, args=args, parser=self._parser

1442-

)

1443-

except ConftestImportFailure as e:

1444-

if self.known_args_namespace.help or self.known_args_namespace.version:

1445-

# we don't want to prevent --help/--version to work

1446-

# so just let it pass and print a warning at the end

1447-

self.issue_config_time_warning(

1448-

PytestConfigWarning(f"could not load initial conftests: {e.path}"),

1449-

stacklevel=2,

1450-

)

1451-

else:

1452-

raise

1453-14541367

@hookimpl(wrapper=True)

14551368

def pytest_collection(self) -> Generator[None, object, object]:

14561369

# Validate invalid configuration keys after collection is done so we

@@ -1534,18 +1447,103 @@ def parse(self, args: list[str], addopts: bool = True) -> None:

15341447

assert self.args == [], (

15351448

"can only parse cmdline args at most once per Config object"

15361449

)

1450+15371451

self.hook.pytest_addhooks.call_historic(

15381452

kwargs=dict(pluginmanager=self.pluginmanager)

15391453

)

1540-

self._preparse(args, addopts=addopts)

1541-

self._parser.after_preparse = True # type: ignore

1454+1455+

if addopts:

1456+

env_addopts = os.environ.get("PYTEST_ADDOPTS", "")

1457+

if len(env_addopts):

1458+

args[:] = (

1459+

self._validate_args(shlex.split(env_addopts), "via PYTEST_ADDOPTS")

1460+

+ args

1461+

)

1462+1463+

ns = self._parser.parse_known_args(args, namespace=copy.copy(self.option))

1464+

rootpath, inipath, inicfg, ignored_config_files = determine_setup(

1465+

inifile=ns.inifilename,

1466+

override_ini=ns.override_ini,

1467+

args=ns.file_or_dir,

1468+

rootdir_cmd_arg=ns.rootdir or None,

1469+

invocation_dir=self.invocation_params.dir,

1470+

)

1471+

self._rootpath = rootpath

1472+

self._inipath = inipath

1473+

self._ignored_config_files = ignored_config_files

1474+

self.inicfg = inicfg

1475+

self._parser.extra_info["rootdir"] = str(self.rootpath)

1476+

self._parser.extra_info["inifile"] = str(self.inipath)

1477+1478+

self._parser.addini("addopts", "Extra command line options", "args")

1479+

self._parser.addini("minversion", "Minimally required pytest version")

1480+

self._parser.addini(

1481+

"pythonpath", type="paths", help="Add paths to sys.path", default=[]

1482+

)

1483+

self._parser.addini(

1484+

"required_plugins",

1485+

"Plugins that must be present for pytest to run",

1486+

type="args",

1487+

default=[],

1488+

)

1489+1490+

if addopts:

1491+

args[:] = (

1492+

self._validate_args(self.getini("addopts"), "via addopts config") + args

1493+

)

1494+1495+

self.known_args_namespace = self._parser.parse_known_args(

1496+

args, namespace=copy.copy(self.option)

1497+

)

1498+

self._checkversion()

1499+

self._consider_importhook()

1500+

self._configure_python_path()

1501+

self.pluginmanager.consider_preparse(args, exclude_only=False)

1502+

if (

1503+

not os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD")

1504+

and not self.known_args_namespace.disable_plugin_autoload

1505+

):

1506+

# Autoloading from distribution package entry point has

1507+

# not been disabled.

1508+

self.pluginmanager.load_setuptools_entrypoints("pytest11")

1509+

# Otherwise only plugins explicitly specified in PYTEST_PLUGINS

1510+

# are going to be loaded.

1511+

self.pluginmanager.consider_env()

1512+1513+

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

1514+1515+

self._validate_plugins()

1516+

self._warn_about_skipped_plugins()

1517+1518+

if self.known_args_namespace.confcutdir is None:

1519+

if self.inipath is not None:

1520+

confcutdir = str(self.inipath.parent)

1521+

else:

1522+

confcutdir = str(self.rootpath)

1523+

self.known_args_namespace.confcutdir = confcutdir

1524+

try:

1525+

self.hook.pytest_load_initial_conftests(

1526+

early_config=self, args=args, parser=self._parser

1527+

)

1528+

except ConftestImportFailure as e:

1529+

if self.known_args_namespace.help or self.known_args_namespace.version:

1530+

# we don't want to prevent --help/--version to work

1531+

# so just let it pass and print a warning at the end

1532+

self.issue_config_time_warning(

1533+

PytestConfigWarning(f"could not load initial conftests: {e.path}"),

1534+

stacklevel=2,

1535+

)

1536+

else:

1537+

raise

1538+15421539

try:

1543-

parsed = self._parser.parse(args, namespace=self.option)

1540+

self._parser.parse(args, namespace=self.option)

15441541

except PrintHelp:

15451542

return

1543+15461544

self.args, self.args_source = self._decide_args(

1547-

args=getattr(parsed, FILE_OR_DIR),

1548-

pyargs=self.known_args_namespace.pyargs,

1545+

args=getattr(self.option, FILE_OR_DIR),

1546+

pyargs=self.option.pyargs,

15491547

testpaths=self.getini("testpaths"),

15501548

invocation_dir=self.invocation_params.dir,

15511549

rootpath=self.rootpath,