Skip to content

Commit 38e8676

Browse files
committed
config: make Argument a wrapper around argparse.Action
Same as in `OptionGroup`, this continues making our Parser code just a simple wrapper around argparse.
1 parent 2de44c4 commit 38e8676

File tree

4 files changed

+71
-127
lines changed

4 files changed

+71
-127
lines changed

src/_pytest/config/__init__.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1208,12 +1208,11 @@ def fromdictargs(cls, option_dict: Mapping[str, Any], args: list[str]) -> Config
12081208
return config
12091209

12101210
def _processopt(self, opt: Argument) -> None:
1211-
for name in opt._short_opts + opt._long_opts:
1211+
for name in opt.names():
12121212
self._opt2dest[name] = opt.dest
12131213

1214-
if hasattr(opt, "default"):
1215-
if not hasattr(self.option, opt.dest):
1216-
setattr(self.option, opt.dest, opt.default)
1214+
if not hasattr(self.option, opt.dest):
1215+
setattr(self.option, opt.dest, opt.default)
12171216

12181217
@hookimpl(trylast=True)
12191218
def pytest_load_initial_conftests(self, early_config: Config) -> None:

src/_pytest/config/argparsing.py

Lines changed: 37 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
import argparse
55
from collections.abc import Callable
6-
from collections.abc import Mapping
76
from collections.abc import Sequence
87
import os
98
import sys
@@ -280,103 +279,37 @@ def get_ini_default_for_type(
280279
return ""
281280

282281

283-
class ArgumentError(Exception):
284-
"""Raised if an Argument instance is created with invalid or
285-
inconsistent arguments."""
286-
287-
def __init__(self, msg: str, option: Argument | str) -> None:
288-
self.msg = msg
289-
self.option_id = str(option)
290-
291-
def __str__(self) -> str:
292-
if self.option_id:
293-
return f"option {self.option_id}: {self.msg}"
294-
else:
295-
return self.msg
296-
297-
298282
class Argument:
299-
"""Class that mimics the necessary behaviour of optparse.Option.
283+
"""An option defined in an OptionGroup."""
300284

301-
It's currently a least effort implementation and ignoring choices
302-
and integer prefixes.
285+
def __init__(self, action: argparse.Action) -> None:
286+
self._action = action
303287

304-
https://docs.python.org/3/library/optparse.html#optparse-standard-option-types
305-
"""
288+
def attrs(self) -> dict[str, Any]:
289+
return self._action.__dict__
306290

307-
def __init__(self, *names: str, **attrs: Any) -> None:
308-
"""Store params in private vars for use in add_argument."""
309-
self._attrs = attrs
310-
self._short_opts: list[str] = []
311-
self._long_opts: list[str] = []
312-
try:
313-
self.type = attrs["type"]
314-
except KeyError:
315-
pass
316-
try:
317-
# Attribute existence is tested in Config._processopt.
318-
self.default = attrs["default"]
319-
except KeyError:
320-
pass
321-
self._set_opt_strings(names)
322-
dest: str | None = attrs.get("dest")
323-
if dest:
324-
self.dest = dest
325-
elif self._long_opts:
326-
self.dest = self._long_opts[0][2:].replace("-", "_")
327-
else:
328-
try:
329-
self.dest = self._short_opts[0][1:]
330-
except IndexError as e:
331-
self.dest = "???" # Needed for the error repr.
332-
raise ArgumentError("need a long or short option", self) from e
291+
def names(self) -> Sequence[str]:
292+
return self._action.option_strings
333293

334-
def names(self) -> list[str]:
335-
return self._short_opts + self._long_opts
336-
337-
def attrs(self) -> Mapping[str, Any]:
338-
return self._attrs
294+
@property
295+
def dest(self) -> str:
296+
return self._action.dest
339297

340-
def _set_opt_strings(self, opts: Sequence[str]) -> None:
341-
"""Directly from optparse.
298+
@property
299+
def default(self) -> Any:
300+
return self._action.default
342301

343-
Might not be necessary as this is passed to argparse later on.
344-
"""
345-
for opt in opts:
346-
if len(opt) < 2:
347-
raise ArgumentError(
348-
f"invalid option string {opt!r}: "
349-
"must be at least two characters long",
350-
self,
351-
)
352-
elif len(opt) == 2:
353-
if not (opt[0] == "-" and opt[1] != "-"):
354-
raise ArgumentError(
355-
f"invalid short option string {opt!r}: "
356-
"must be of the form -x, (x any non-dash char)",
357-
self,
358-
)
359-
self._short_opts.append(opt)
360-
else:
361-
if not (opt[0:2] == "--" and opt[2] != "-"):
362-
raise ArgumentError(
363-
f"invalid long option string {opt!r}: "
364-
"must start with --, followed by non-dash",
365-
self,
366-
)
367-
self._long_opts.append(opt)
302+
@property
303+
def type(self) -> Any | None:
304+
return self._action.type
368305

369306
def __repr__(self) -> str:
370307
args: list[str] = []
371-
if self._short_opts:
372-
args += ["_short_opts: " + repr(self._short_opts)]
373-
if self._long_opts:
374-
args += ["_long_opts: " + repr(self._long_opts)]
308+
args += ["opts: " + repr(self.names())]
375309
args += ["dest: " + repr(self.dest)]
376-
if hasattr(self, "type"):
310+
if self._action.type:
377311
args += ["type: " + repr(self.type)]
378-
if hasattr(self, "default"):
379-
args += ["default: " + repr(self.default)]
312+
args += ["default: " + repr(self.default)]
380313
return "Argument({})".format(", ".join(args))
381314

382315

@@ -406,6 +339,7 @@ def addoption(self, *opts: str, **attrs: Any) -> None:
406339
407340
:param opts:
408341
Option names, can be short or long options.
342+
Note that lower-case short options (e.g. `-x`) are reserved.
409343
:param attrs:
410344
Same attributes as the argparse library's :meth:`add_argument()
411345
<argparse.ArgumentParser.add_argument>` function accepts.
@@ -415,25 +349,27 @@ def addoption(self, *opts: str, **attrs: Any) -> None:
415349
)
416350
if conflict:
417351
raise ValueError(f"option names {conflict} already added")
418-
option = Argument(*opts, **attrs)
419-
self._addoption_instance(option, shortupper=False)
352+
self._addoption_inner(opts, attrs, allow_reserved=False)
420353

421354
def _addoption(self, *opts: str, **attrs: Any) -> None:
422-
option = Argument(*opts, **attrs)
423-
self._addoption_instance(option, shortupper=True)
355+
"""Like addoption(), but also allows registering short lower case options (e.g. -x),
356+
which are reserved for pytest core."""
357+
self._addoption_inner(opts, attrs, allow_reserved=True)
424358

425-
def _addoption_instance(self, option: Argument, shortupper: bool = False) -> None:
426-
if not shortupper:
427-
for opt in option._short_opts:
428-
if opt[0] == "-" and opt[1].islower():
429-
raise ValueError("lowercase shortoptions reserved")
359+
def _addoption_inner(
360+
self, opts: tuple[str, ...], attrs: dict[str, Any], allow_reserved: bool
361+
) -> None:
362+
if not allow_reserved:
363+
for opt in opts:
364+
if len(opt) >= 2 and opt[0] == "-" and opt[1].islower():
365+
raise ValueError("lowercase short options are reserved")
430366

367+
action = self._arggroup.add_argument(*opts, **attrs)
368+
option = Argument(action)
369+
self.options.append(option)
431370
if self.parser:
432371
self.parser.processoption(option)
433372

434-
self._arggroup.add_argument(*option.names(), **option.attrs())
435-
self.options.append(option)
436-
437373

438374
class PytestArgumentParser(argparse.ArgumentParser):
439375
def __init__(
@@ -495,10 +431,9 @@ def _format_action_invocation(self, action: argparse.Action) -> str:
495431
for option in options:
496432
if len(option) == 2 or option[2] == " ":
497433
continue
498-
if not option.startswith("--"):
499-
raise ArgumentError(
500-
f'long optional argument without "--": [{option}]', option
501-
)
434+
assert option.startswith("--"), (
435+
f'long optional argument without "--": [{option}]'
436+
)
502437
xxoption = option[2:]
503438
shortened = xxoption.replace("-", "")
504439
if shortened not in short_long or len(short_long[shortened]) < len(

src/_pytest/helpconfig.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ def __call__(
5959
def pytest_addoption(parser: Parser) -> None:
6060
group = parser.getgroup("debugconfig")
6161
group.addoption(
62-
"--version",
6362
"-V",
63+
"--version",
6464
action="count",
6565
default=0,
6666
dest="version",

testing/test_parseopt.py

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -34,33 +34,43 @@ def test_custom_prog(self, parser: parseopt.Parser) -> None:
3434
assert parser.optparser.prog == "custom-prog"
3535

3636
def test_argument(self) -> None:
37-
with pytest.raises(parseopt.ArgumentError):
38-
# need a short or long option
39-
argument = parseopt.Argument()
40-
argument = parseopt.Argument("-t")
41-
assert argument._short_opts == ["-t"]
42-
assert argument._long_opts == []
43-
assert argument.dest == "t"
44-
argument = parseopt.Argument("-t", "--test")
45-
assert argument._short_opts == ["-t"]
46-
assert argument._long_opts == ["--test"]
47-
assert argument.dest == "test"
48-
argument = parseopt.Argument("-t", "--test", dest="abc")
37+
parser = argparse.ArgumentParser()
38+
39+
action = parser.add_argument("-a")
40+
argument = parseopt.Argument(action)
41+
assert argument.names() == ["-a"]
42+
assert argument.dest == "a"
43+
44+
action = parser.add_argument("-b", "--boop")
45+
argument = parseopt.Argument(action)
46+
assert argument.names() == ["-b", "--boop"]
47+
assert argument.dest == "boop"
48+
49+
action = parser.add_argument("-c", "--coop", dest="abc")
50+
argument = parseopt.Argument(action)
4951
assert argument.dest == "abc"
50-
assert str(argument) == (
51-
"Argument(_short_opts: ['-t'], _long_opts: ['--test'], dest: 'abc')"
52+
assert (
53+
str(argument)
54+
== "Argument(opts: ['-c', '--coop'], dest: 'abc', default: None)"
5255
)
5356

5457
def test_argument_type(self) -> None:
55-
argument = parseopt.Argument("-t", dest="abc", type=int)
58+
parser = argparse.ArgumentParser()
59+
60+
action = parser.add_argument("-a", dest="aa", type=int)
61+
argument = parseopt.Argument(action)
5662
assert argument.type is int
57-
argument = parseopt.Argument("-t", dest="abc", type=str)
63+
64+
action = parser.add_argument("-b", dest="bb", type=str)
65+
argument = parseopt.Argument(action)
5866
assert argument.type is str
59-
argument = parseopt.Argument("-t", dest="abc", type=float)
67+
68+
action = parser.add_argument("-c", dest="cc", type=float)
69+
argument = parseopt.Argument(action)
6070
assert argument.type is float
61-
argument = parseopt.Argument(
62-
"-t", dest="abc", type=str, choices=["red", "blue"]
63-
)
71+
72+
action = parser.add_argument("-d", dest="dd", type=str, choices=["red", "blue"])
73+
argument = parseopt.Argument(action)
6474
assert argument.type is str
6575

6676
def test_group_add_and_get(self, parser: parseopt.Parser) -> None:

0 commit comments

Comments
 (0)