Skip to content

Commit 5eaad82

Browse files
authored
Merge pull request #13857 from bluetech/config-cleanups
Config cleanups
2 parents b3f3263 + 0c275c4 commit 5eaad82

File tree

5 files changed

+88
-117
lines changed

5 files changed

+88
-117
lines changed

src/_pytest/config/__init__.py

Lines changed: 52 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from collections.abc import Generator
1111
from collections.abc import Iterable
1212
from collections.abc import Iterator
13+
from collections.abc import Mapping
1314
from collections.abc import Sequence
1415
import contextlib
1516
import copy
@@ -55,6 +56,7 @@
5556
from _pytest._io import TerminalWriter
5657
from _pytest.compat import assert_never
5758
from _pytest.config.argparsing import Argument
59+
from _pytest.config.argparsing import FILE_OR_DIR
5860
from _pytest.config.argparsing import Parser
5961
import _pytest.deprecated
6062
import _pytest.hookspec
@@ -290,23 +292,21 @@ def directory_arg(path: str, optname: str) -> str:
290292

291293

292294
def get_config(
293-
args: list[str] | None = None,
295+
args: Iterable[str] | None = None,
294296
plugins: Sequence[str | _PluggyPlugin] | None = None,
295297
) -> Config:
296298
# Subsequent calls to main will create a fresh instance.
297299
pluginmanager = PytestPluginManager()
298-
config = Config(
299-
pluginmanager,
300-
invocation_params=Config.InvocationParams(
301-
args=args or (),
302-
plugins=plugins,
303-
dir=pathlib.Path.cwd(),
304-
),
300+
invocation_params = Config.InvocationParams(
301+
args=args or (),
302+
plugins=plugins,
303+
dir=pathlib.Path.cwd(),
305304
)
305+
config = Config(pluginmanager, invocation_params=invocation_params)
306306

307-
if args is not None:
307+
if invocation_params.args:
308308
# Handle any "-p no:plugin" args.
309-
pluginmanager.consider_preparse(args, exclude_only=True)
309+
pluginmanager.consider_preparse(invocation_params.args, exclude_only=True)
310310

311311
for spec in default_plugins:
312312
pluginmanager.import_plugin(spec)
@@ -1202,7 +1202,7 @@ def cwd_relative_nodeid(self, nodeid: str) -> str:
12021202
return nodeid
12031203

12041204
@classmethod
1205-
def fromdictargs(cls, option_dict, args) -> Config:
1205+
def fromdictargs(cls, option_dict: Mapping[str, Any], args: list[str]) -> Config:
12061206
"""Constructor usable for subprocesses."""
12071207
config = get_config(args)
12081208
config.option.__dict__.update(option_dict)
@@ -1246,35 +1246,6 @@ def pytest_load_initial_conftests(self, early_config: Config) -> None:
12461246
),
12471247
)
12481248

1249-
def _initini(self, args: Sequence[str]) -> None:
1250-
ns, unknown_args = self._parser.parse_known_and_unknown_args(
1251-
args, namespace=copy.copy(self.option)
1252-
)
1253-
rootpath, inipath, inicfg, ignored_config_files = determine_setup(
1254-
inifile=ns.inifilename,
1255-
override_ini=ns.override_ini,
1256-
args=ns.file_or_dir + unknown_args,
1257-
rootdir_cmd_arg=ns.rootdir or None,
1258-
invocation_dir=self.invocation_params.dir,
1259-
)
1260-
self._rootpath = rootpath
1261-
self._inipath = inipath
1262-
self._ignored_config_files = ignored_config_files
1263-
self.inicfg = inicfg
1264-
self._parser.extra_info["rootdir"] = str(self.rootpath)
1265-
self._parser.extra_info["inifile"] = str(self.inipath)
1266-
self._parser.addini("addopts", "Extra command line options", "args")
1267-
self._parser.addini("minversion", "Minimally required pytest version")
1268-
self._parser.addini(
1269-
"pythonpath", type="paths", help="Add paths to sys.path", default=[]
1270-
)
1271-
self._parser.addini(
1272-
"required_plugins",
1273-
"Plugins that must be present for pytest to run",
1274-
type="args",
1275-
default=[],
1276-
)
1277-
12781249
def _consider_importhook(self, args: Sequence[str]) -> None:
12791250
"""Install the PEP 302 import hook if using assertion rewriting.
12801251
@@ -1336,13 +1307,13 @@ def _unconfigure_python_path(self) -> None:
13361307

13371308
def _validate_args(self, args: list[str], via: str) -> list[str]:
13381309
"""Validate known args."""
1339-
self._parser._config_source_hint = via # type: ignore
1310+
self._parser.extra_info["config source"] = via
13401311
try:
13411312
self._parser.parse_known_and_unknown_args(
13421313
args, namespace=copy.copy(self.option)
13431314
)
13441315
finally:
1345-
del self._parser._config_source_hint # type: ignore
1316+
self._parser.extra_info.pop("config source", None)
13461317

13471318
return args
13481319

@@ -1399,7 +1370,35 @@ def _preparse(self, args: list[str], addopts: bool = True) -> None:
13991370
self._validate_args(shlex.split(env_addopts), "via PYTEST_ADDOPTS")
14001371
+ args
14011372
)
1402-
self._initini(args)
1373+
1374+
ns, unknown_args = self._parser.parse_known_and_unknown_args(
1375+
args, namespace=copy.copy(self.option)
1376+
)
1377+
rootpath, inipath, inicfg, ignored_config_files = determine_setup(
1378+
inifile=ns.inifilename,
1379+
override_ini=ns.override_ini,
1380+
args=ns.file_or_dir + unknown_args,
1381+
rootdir_cmd_arg=ns.rootdir or None,
1382+
invocation_dir=self.invocation_params.dir,
1383+
)
1384+
self._rootpath = rootpath
1385+
self._inipath = inipath
1386+
self._ignored_config_files = ignored_config_files
1387+
self.inicfg = inicfg
1388+
self._parser.extra_info["rootdir"] = str(self.rootpath)
1389+
self._parser.extra_info["inifile"] = str(self.inipath)
1390+
self._parser.addini("addopts", "Extra command line options", "args")
1391+
self._parser.addini("minversion", "Minimally required pytest version")
1392+
self._parser.addini(
1393+
"pythonpath", type="paths", help="Add paths to sys.path", default=[]
1394+
)
1395+
self._parser.addini(
1396+
"required_plugins",
1397+
"Plugins that must be present for pytest to run",
1398+
type="args",
1399+
default=[],
1400+
)
1401+
14031402
if addopts:
14041403
args[:] = (
14051404
self._validate_args(self.getini("addopts"), "via addopts config") + args
@@ -1540,19 +1539,17 @@ def parse(self, args: list[str], addopts: bool = True) -> None:
15401539
self._preparse(args, addopts=addopts)
15411540
self._parser.after_preparse = True # type: ignore
15421541
try:
1543-
args = self._parser.parse_setoption(
1544-
args, self.option, namespace=self.option
1545-
)
1546-
self.args, self.args_source = self._decide_args(
1547-
args=args,
1548-
pyargs=self.known_args_namespace.pyargs,
1549-
testpaths=self.getini("testpaths"),
1550-
invocation_dir=self.invocation_params.dir,
1551-
rootpath=self.rootpath,
1552-
warn=True,
1553-
)
1542+
parsed = self._parser.parse(args, namespace=self.option)
15541543
except PrintHelp:
1555-
pass
1544+
return
1545+
self.args, self.args_source = self._decide_args(
1546+
args=getattr(parsed, FILE_OR_DIR),
1547+
pyargs=self.known_args_namespace.pyargs,
1548+
testpaths=self.getini("testpaths"),
1549+
invocation_dir=self.invocation_params.dir,
1550+
rootpath=self.rootpath,
1551+
warn=True,
1552+
)
15561553

15571554
def issue_config_time_warning(self, warning: Warning, stacklevel: int) -> None:
15581555
"""Issue and handle a warning during the "configure" stage.

src/_pytest/config/argparsing.py

Lines changed: 9 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from collections.abc import Sequence
88
import os
99
from typing import Any
10-
from typing import cast
1110
from typing import final
1211
from typing import Literal
1312
from typing import NoReturn
@@ -112,12 +111,12 @@ def parse(
112111
self.optparser = self._getparser()
113112
try_argcomplete(self.optparser)
114113
strargs = [os.fspath(x) for x in args]
115-
return self.optparser.parse_args(strargs, namespace=namespace)
114+
return self.optparser.parse_intermixed_args(strargs, namespace=namespace)
116115

117-
def _getparser(self) -> MyOptionParser:
116+
def _getparser(self) -> PytestArgumentParser:
118117
from _pytest._argcomplete import filescompleter
119118

120-
optparser = MyOptionParser(self, self.extra_info, prog=self.prog)
119+
optparser = PytestArgumentParser(self, self.extra_info, prog=self.prog)
121120
groups = [*self._groups, self._anonymous]
122121
for group in groups:
123122
if group.options:
@@ -133,17 +132,6 @@ def _getparser(self) -> MyOptionParser:
133132
file_or_dir_arg.completer = filescompleter # type: ignore
134133
return optparser
135134

136-
def parse_setoption(
137-
self,
138-
args: Sequence[str | os.PathLike[str]],
139-
option: argparse.Namespace,
140-
namespace: argparse.Namespace | None = None,
141-
) -> list[str]:
142-
parsedoption = self.parse(args, namespace=namespace)
143-
for name, value in parsedoption.__dict__.items():
144-
setattr(option, name, value)
145-
return cast(list[str], getattr(parsedoption, FILE_OR_DIR))
146-
147135
def parse_known_args(
148136
self,
149137
args: Sequence[str | os.PathLike[str]],
@@ -331,9 +319,7 @@ def names(self) -> list[str]:
331319

332320
def attrs(self) -> Mapping[str, Any]:
333321
# Update any attributes set by processopt.
334-
attrs = "default dest help".split()
335-
attrs.append(self.dest)
336-
for attr in attrs:
322+
for attr in ("default", "dest", "help", self.dest):
337323
try:
338324
self._attrs[attr] = getattr(self, attr)
339325
except AttributeError:
@@ -436,7 +422,7 @@ def _addoption_instance(self, option: Argument, shortupper: bool = False) -> Non
436422
self.options.append(option)
437423

438424

439-
class MyOptionParser(argparse.ArgumentParser):
425+
class PytestArgumentParser(argparse.ArgumentParser):
440426
def __init__(
441427
self,
442428
parser: Parser,
@@ -459,32 +445,12 @@ def __init__(
459445
def error(self, message: str) -> NoReturn:
460446
"""Transform argparse error message into UsageError."""
461447
msg = f"{self.prog}: error: {message}"
462-
463-
if hasattr(self._parser, "_config_source_hint"):
464-
msg = f"{msg} ({self._parser._config_source_hint})"
465-
448+
if self.extra_info:
449+
msg += "\n" + "\n".join(
450+
f" {k}: {v}" for k, v in sorted(self.extra_info.items())
451+
)
466452
raise UsageError(self.format_usage() + msg)
467453

468-
# Type ignored because typeshed has a very complex type in the superclass.
469-
def parse_args( # type: ignore
470-
self,
471-
args: Sequence[str] | None = None,
472-
namespace: argparse.Namespace | None = None,
473-
) -> argparse.Namespace:
474-
"""Allow splitting of positional arguments."""
475-
parsed, unrecognized = self.parse_known_args(args, namespace)
476-
if unrecognized:
477-
for arg in unrecognized:
478-
if arg and arg[0] == "-":
479-
lines = [
480-
"unrecognized arguments: {}".format(" ".join(unrecognized))
481-
]
482-
for k, v in sorted(self.extra_info.items()):
483-
lines.append(f" {k}: {v}")
484-
self.error("\n".join(lines))
485-
getattr(parsed, FILE_OR_DIR).extend(unrecognized)
486-
return parsed
487-
488454

489455
class DropShorterLongHelpFormatter(argparse.HelpFormatter):
490456
"""Shorten help for long options that differ only in extra hyphens.

src/_pytest/helpconfig.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,23 @@
33

44
from __future__ import annotations
55

6-
from argparse import Action
6+
import argparse
77
from collections.abc import Generator
8+
from collections.abc import Sequence
89
import os
910
import sys
11+
from typing import Any
1012

1113
from _pytest.config import Config
1214
from _pytest.config import ExitCode
1315
from _pytest.config import PrintHelp
1416
from _pytest.config.argparsing import Parser
17+
from _pytest.config.argparsing import PytestArgumentParser
1518
from _pytest.terminal import TerminalReporter
1619
import pytest
1720

1821

19-
class HelpAction(Action):
22+
class HelpAction(argparse.Action):
2023
"""An argparse Action that will raise an exception in order to skip the
2124
rest of the argument parsing when --help is passed.
2225
@@ -26,20 +29,29 @@ class HelpAction(Action):
2629
implemented by raising SystemExit.
2730
"""
2831

29-
def __init__(self, option_strings, dest=None, default=False, help=None):
32+
def __init__(
33+
self, option_strings: Sequence[str], dest: str, *, help: str | None = None
34+
) -> None:
3035
super().__init__(
3136
option_strings=option_strings,
3237
dest=dest,
33-
const=True,
34-
default=default,
3538
nargs=0,
39+
const=True,
40+
default=False,
3641
help=help,
3742
)
3843

39-
def __call__(self, parser, namespace, values, option_string=None):
44+
def __call__(
45+
self,
46+
parser: argparse.ArgumentParser,
47+
namespace: argparse.Namespace,
48+
values: str | Sequence[Any] | None,
49+
option_string: str | None = None,
50+
) -> None:
4051
setattr(namespace, self.dest, self.const)
4152

4253
# We should only skip the rest of the parsing after preparse is done.
54+
assert isinstance(parser, PytestArgumentParser)
4355
if getattr(parser._parser, "after_preparse", False):
4456
raise PrintHelp
4557

@@ -245,9 +257,6 @@ def showhelp(config: Config) -> None:
245257
tw.line("warning : " + warningreport.message, red=True)
246258

247259

248-
conftest_options = [("pytest_plugins", "list of plugin names to load")]
249-
250-
251260
def getpluginversioninfo(config: Config) -> list[str]:
252261
lines = []
253262
plugininfo = config.pluginmanager.list_plugin_distinfo()

testing/test_config.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1425,7 +1425,7 @@ def test_inifilename(self, tmp_path: Path) -> None:
14251425
)
14261426
with MonkeyPatch.context() as mp:
14271427
mp.chdir(cwd)
1428-
config = Config.fromdictargs(option_dict, ())
1428+
config = Config.fromdictargs(option_dict, [])
14291429
inipath = absolutepath(inifilename)
14301430

14311431
assert config.args == [str(cwd)]
@@ -2290,9 +2290,10 @@ def test_addopts_from_env_not_concatenated(
22902290
with pytest.raises(UsageError) as excinfo:
22912291
config._preparse(["cache_dir=ignored"], addopts=True)
22922292
assert (
2293-
"error: argument -o/--override-ini: expected one argument (via PYTEST_ADDOPTS)"
2293+
"error: argument -o/--override-ini: expected one argument"
22942294
in excinfo.value.args[0]
22952295
)
2296+
assert "via PYTEST_ADDOPTS" in excinfo.value.args[0]
22962297

22972298
def test_addopts_from_ini_not_concatenated(self, pytester: Pytester) -> None:
22982299
"""`addopts` from configuration should not take values from normal args (#4265)."""
@@ -2303,10 +2304,11 @@ def test_addopts_from_ini_not_concatenated(self, pytester: Pytester) -> None:
23032304
"""
23042305
)
23052306
result = pytester.runpytest("cache_dir=ignored")
2307+
config = pytester._request.config
23062308
result.stderr.fnmatch_lines(
23072309
[
2308-
f"{pytester._request.config._parser.optparser.prog}: error: "
2309-
f"argument -o/--override-ini: expected one argument (via addopts config)"
2310+
f"{config._parser.optparser.prog}: error: argument -o/--override-ini: expected one argument",
2311+
" config source: via addopts config",
23102312
]
23112313
)
23122314
assert result.ret == _pytest.config.ExitCode.USAGE_ERROR

0 commit comments

Comments
 (0)