Skip to content

Commit 75631c7

Browse files
authored
Add inverse option with "--no-" prefix for store_true options (#267)
Fixes upstream issue: #6846.
2 parents cdc7605 + 8975b73 commit 75631c7

File tree

7 files changed

+185
-78
lines changed

7 files changed

+185
-78
lines changed

src/_pytest/config/argparsing.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ def _getparser(self) -> "MyOptionParser":
110110

111111
optparser = MyOptionParser(self, self.extra_info, prog=self.prog)
112112
groups = self._groups + [self._anonymous]
113+
self._preprocess_options_for_no_prefix(groups)
113114
for group in groups:
114115
if group.options:
115116
desc = group.description or group.name
@@ -124,6 +125,33 @@ def _getparser(self) -> "MyOptionParser":
124125
file_or_dir_arg.completer = filescompleter # type: ignore
125126
return optparser
126127

128+
def _preprocess_options_for_no_prefix(self, groups: List["OptionGroup"]) -> None:
129+
"""Add "--no-"-prefixed option names for "store_true" actions."""
130+
all_names = []
131+
store_true_options = []
132+
for option in [option for group in groups for option in group.options]:
133+
names = option.names()
134+
if option.attrs().get("action") == "store_true":
135+
store_true_options.append((option, names))
136+
all_names.extend(names)
137+
138+
for option, option_names in store_true_options:
139+
new_option_strings = []
140+
for option_string in option_names:
141+
if option_string.startswith("--no-"):
142+
new = "--{}".format(option_string[5:])
143+
elif option_string.startswith("--"):
144+
new = "--no-{}".format(option_string[2:])
145+
else:
146+
continue
147+
if new not in all_names:
148+
new_option_strings.append(new)
149+
150+
if new_option_strings:
151+
option._long_opts.extend(new_option_strings)
152+
assert option._attrs["action"] == "store_true", option
153+
option._attrs["action"] = "store_true_with_no_prefix"
154+
127155
def parse_setoption(
128156
self,
129157
args: Sequence[Union[str, py.path.local]],
@@ -377,6 +405,16 @@ def _addoption_instance(self, option: "Argument", shortupper: bool = False) -> N
377405
self.options.append(option)
378406

379407

408+
class StoreTrueWithNoPrefixAction(argparse._StoreTrueAction):
409+
"""Handle `--no-foo` for `--foo` options."""
410+
411+
def __call__(self, parser, namespace, values, option_string=None):
412+
if option_string.startswith("--no-"):
413+
setattr(namespace, self.dest, False)
414+
else:
415+
setattr(namespace, self.dest, True)
416+
417+
380418
class MyOptionParser(argparse.ArgumentParser):
381419
def __init__(
382420
self,
@@ -396,6 +434,9 @@ def __init__(
396434
# extra_info is a dict of (param -> value) to display if there's
397435
# an usage error to provide more contextual information to the user
398436
self.extra_info = extra_info if extra_info else {}
437+
self.register(
438+
"action", "store_true_with_no_prefix", StoreTrueWithNoPrefixAction
439+
)
399440

400441
def error(self, message: str) -> "NoReturn":
401442
"""Transform argparse error message into UsageError."""
@@ -515,6 +556,19 @@ def _format_action_invocation(self, action: argparse.Action) -> str:
515556
return_list.append(option)
516557
if option[2:] == short_long.get(option.replace("-", "")):
517558
return_list.append(option.replace(" ", "=", 1))
559+
560+
if isinstance(action, StoreTrueWithNoPrefixAction):
561+
# Collapse "--foo, --no-foo" into "--[no-]foo".
562+
idx = 0
563+
while idx < len(return_list):
564+
option = return_list[idx]
565+
if option.startswith("--"):
566+
other_idx = return_list.index("--no-{}".format(option[2:]))
567+
return_list.pop(other_idx)
568+
option = "--[no-]" + option[2:]
569+
return_list = return_list[:idx] + [option] + return_list[idx + 1 :]
570+
idx += 1
571+
518572
formatted_action_invocation = ", ".join(return_list)
519573
action._formatted_action_invocation = formatted_action_invocation # type: ignore
520574
return formatted_action_invocation

src/_pytest/helpconfig.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ def pytest_addoption(parser):
4949
group.addoption(
5050
"--version",
5151
"-V",
52-
action="store_true",
52+
action="store_const",
53+
const=True,
5354
help="display pytest version and information about plugins.",
5455
)
5556
group._addoption(

src/_pytest/pytester.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from typing import Generator
1818
from typing import Iterable
1919
from typing import List
20+
from typing import Mapping
2021
from typing import Optional
2122
from typing import Sequence
2223
from typing import Tuple
@@ -654,25 +655,34 @@ def chdir(self):
654655
"""
655656
self.tmpdir.chdir()
656657

657-
def _makefile(self, ext, lines, files, encoding="utf-8"):
658+
def _makefile(
659+
self,
660+
ext: str,
661+
lines: Sequence[Union[bytes, str]],
662+
files: Mapping[str, Sequence[Union[bytes, str]]],
663+
encoding="utf-8",
664+
) -> py.path.local:
658665
items = list(files.items())
659666

660667
def to_text(s):
661668
return s.decode(encoding) if isinstance(s, bytes) else str(s)
662669

663670
if lines:
664-
source = "\n".join(to_text(x) for x in lines)
665-
basename = self.request.function.__name__
666-
items.insert(0, (basename, source))
671+
funcname = self.request.function.__name__ # type: str
672+
lines = "\n".join(to_text(x) for x in lines)
673+
items.insert(0, (funcname, lines))
674+
675+
if not items:
676+
raise ValueError("no files to create")
667677

668-
ret = None
678+
first_p = True
669679
for basename, value in items:
670680
p = self.tmpdir.join(basename).new(ext=ext)
671681
p.dirpath().ensure_dir()
672-
source = Source(value)
673-
source = "\n".join(to_text(line) for line in source.lines)
682+
source = "\n".join(to_text(x) for x in Source(value).lines)
674683
p.write(source.strip().encode(encoding), "wb")
675-
if ret is None:
684+
if first_p:
685+
first_p = False
676686
ret = p
677687
return ret
678688

@@ -710,7 +720,7 @@ def getinicfg(self, source):
710720
p = self.makeini(source)
711721
return py.iniconfig.IniConfig(p)["pytest"]
712722

713-
def makepyfile(self, *args, **kwargs):
723+
def makepyfile(self, *args, **kwargs) -> py.path.local:
714724
"""Shortcut for .makefile() with a .py extension."""
715725
return self._makefile(".py", args, kwargs)
716726

testing/test_conftest.py

Lines changed: 38 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import os
22
import textwrap
33

4-
import py
4+
import py.path
55

66
import pytest
77
from _pytest.config import ExitCode
88
from _pytest.config import PytestPluginManager
99
from _pytest.pathlib import Path
10+
from _pytest.pytester import Testdir
1011

1112

1213
def ConftestWithSetinitial(path):
@@ -174,19 +175,25 @@ def test_setinitial_conftest_subdirs(testdir, name):
174175
assert len(conftest._conftestpath2mod) == 0
175176

176177

177-
def test_conftest_confcutdir(testdir):
178-
testdir.makeconftest("assert 0")
179-
x = testdir.mkdir("x")
180-
x.join("conftest.py").write(
181-
textwrap.dedent(
182-
"""\
183-
def pytest_addoption(parser):
184-
parser.addoption("--xyz", action="store_true")
185-
"""
186-
)
178+
@pytest.fixture
179+
def conftest_in_tests(testdir: Testdir) -> py.path.local:
180+
return testdir.makepyfile(
181+
**{
182+
"tests/conftest": """
183+
def pytest_addoption(parser):
184+
parser.addoption("--xyz", action="store_const", const=True)
185+
"""
186+
}
187187
)
188+
189+
190+
def test_conftest_confcutdir(
191+
testdir: Testdir, conftest_in_tests: py.path.local
192+
) -> None:
193+
testdir.makeconftest("assert 0")
194+
x = conftest_in_tests.dirpath()
188195
result = testdir.runpytest("-h", "--confcutdir=%s" % x, x)
189-
result.stdout.fnmatch_lines(["*--xyz*"])
196+
result.stdout.fnmatch_lines(["custom options:", " --xyz"])
190197
result.stdout.no_fnmatch_line("*warning: could not load initial*")
191198

192199

@@ -319,31 +326,17 @@ def test_no_conftest(testdir):
319326
assert result.ret == ExitCode.USAGE_ERROR
320327

321328

322-
def test_conftest_existing_resultlog(testdir):
323-
x = testdir.mkdir("tests")
324-
x.join("conftest.py").write(
325-
textwrap.dedent(
326-
"""\
327-
def pytest_addoption(parser):
328-
parser.addoption("--xyz", action="store_true")
329-
"""
330-
)
331-
)
329+
def test_conftest_existing_resultlog(
330+
testdir: Testdir, conftest_in_tests: py.path.local
331+
) -> None:
332332
testdir.makefile(ext=".log", result="") # Writes result.log
333333
result = testdir.runpytest("-h", "--resultlog", "result.log")
334334
result.stdout.fnmatch_lines(["*--xyz*"])
335335

336336

337-
def test_conftest_existing_junitxml(testdir):
338-
x = testdir.mkdir("tests")
339-
x.join("conftest.py").write(
340-
textwrap.dedent(
341-
"""\
342-
def pytest_addoption(parser):
343-
parser.addoption("--xyz", action="store_true")
344-
"""
345-
)
346-
)
337+
def test_conftest_existing_junitxml(
338+
testdir: Testdir, conftest_in_tests: py.path.local
339+
) -> None:
347340
testdir.makefile(ext=".xml", junit="") # Writes junit.xml
348341
result = testdir.runpytest("-h", "--junitxml", "junit.xml")
349342
result.stdout.fnmatch_lines(["*--xyz*"])
@@ -409,24 +402,13 @@ def test_event_fixture(bar):
409402
result.stdout.fnmatch_lines(["*1 passed*"])
410403

411404

412-
def test_conftest_found_with_double_dash(testdir):
413-
sub = testdir.mkdir("sub")
414-
sub.join("conftest.py").write(
415-
textwrap.dedent(
416-
"""\
417-
def pytest_addoption(parser):
418-
parser.addoption("--hello-world", action="store_true")
419-
"""
420-
)
421-
)
422-
p = sub.join("test_hello.py")
405+
def test_conftest_found_with_double_dash_in_arg(
406+
testdir: Testdir, conftest_in_tests: py.path.local
407+
) -> None:
408+
p = conftest_in_tests.dirpath().join("test_hello.py")
423409
p.write("def test_hello(): pass")
424410
result = testdir.runpytest(str(p) + "::test_hello", "-h")
425-
result.stdout.fnmatch_lines(
426-
"""
427-
*--hello-world*
428-
"""
429-
)
411+
result.stdout.fnmatch_lines(["*--xyz*"])
430412

431413

432414
class TestConftestVisibility:
@@ -643,17 +625,13 @@ def pytest_ignore_collect(path, config):
643625
)
644626

645627

646-
def test_required_option_help(testdir):
647-
testdir.makeconftest("assert 0")
648-
x = testdir.mkdir("x")
649-
x.join("conftest.py").write(
650-
textwrap.dedent(
651-
"""\
652-
def pytest_addoption(parser):
653-
parser.addoption("--xyz", action="store_true", required=True)
654-
"""
655-
)
628+
def test_required_option_help(testdir: Testdir) -> None:
629+
testdir.makeconftest(
630+
"""
631+
def pytest_addoption(parser):
632+
parser.addoption("--xyz", action="store_true", required=True)
633+
"""
656634
)
657-
result = testdir.runpytest("-h", x)
635+
result = testdir.runpytest("-h")
658636
result.stdout.no_fnmatch_line("*argument --xyz is required*")
659-
assert "general:" in result.stdout.str()
637+
result.stdout.fnmatch_lines(["general:", "custom options:", " --[no-]xyz"])

testing/test_helpconfig.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import pytest
22
from _pytest.config import ExitCode
3+
from _pytest.pytester import Testdir
34

45

56
def test_version(testdir, pytestconfig):
@@ -14,15 +15,28 @@ def test_version(testdir, pytestconfig):
1415
result.stderr.fnmatch_lines(["*setuptools registered plugins:", "*at*"])
1516

1617

17-
def test_help(testdir):
18+
def test_help(testdir: Testdir) -> None:
1819
result = testdir.runpytest("--help", "-p", "no:[defaults]")
1920
assert result.ret == 0
2021
result.stdout.fnmatch_lines(
2122
"""
23+
usage: * [[]options[]] [[]file_or_dir[]] [[]file_or_dir[]] [[]...[]]
24+
25+
positional arguments:
26+
file_or_dir
27+
2228
-m MARKEXPR only run tests matching given mark expression.
2329
For example: -m 'mark1 and not mark2'.
2430
reporting:
2531
--durations=N *
32+
33+
collection:
34+
--[[]no-[]]collect-only, --[[]no-[]]co
35+
36+
test session debugging and configuration:
37+
-V, --version display pytest version and information about plugins.
38+
-h, --help show help message and configuration info
39+
2640
*setup.cfg*
2741
*minversion*
2842
*to see*markers*pytest --markers*

0 commit comments

Comments
 (0)