Skip to content
Open
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
1 change: 1 addition & 0 deletions changelog/9452.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added the :option:`--help-section` option to display help for a single option group, including custom options and options added by plugins.
8 changes: 8 additions & 0 deletions doc/en/reference/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3467,6 +3467,14 @@ Version and Help

Show help message and configuration info.

.. option:: --help-section=NAME

Show help for the option group identified by ``NAME``. Use ``custom`` for options
registered without an explicit group, or the name passed to
:meth:`pytest.Parser.getgroup` for a named group. To select a group named
``custom`` or one whose name starts with ``group:``, prefix its name with
``group:``.

Complete Help Output
~~~~~~~~~~~~~~~~~~~~

Expand Down
48 changes: 46 additions & 2 deletions src/_pytest/config/argparsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,11 @@ def __init__(
self.optparser = PytestArgumentParser(usage, self.extra_info, prog=prog)
anonymous_arggroup = self.optparser.add_argument_group("Custom options")
self._anonymous = OptionGroup(
anonymous_arggroup, "_anonymous", self, _ispytest=True
anonymous_arggroup,
"_anonymous",
self,
_ispytest=True,
description="Custom options",
)
self._groups = [self._anonymous]
# Maps option strings -> dest, e.g. "-V" and "--version" to "version".
Expand Down Expand Up @@ -178,7 +182,9 @@ def getgroup(
return group

arggroup = self.optparser.add_argument_group(description or name)
group = OptionGroup(arggroup, name, self, _ispytest=True)
group = OptionGroup(
arggroup, name, self, _ispytest=True, description=description
)
i = 0
for i, grp in enumerate(self._groups):
if grp.name == after:
Expand All @@ -189,6 +195,41 @@ def getgroup(
self.optparser._action_groups.insert(i + 1, self.optparser._action_groups.pop())
return group

def _format_group_help(self, name: str) -> str:
"""Format the help for one option group, identified by its name."""
if name == "custom":
group = self._anonymous
else:
group = next(
(
group
for group in self._groups[1:]
if self._get_group_help_name(group) == name
),
None,
)
if group is None:
available = [
"custom",
*(self._get_group_help_name(group) for group in self._groups[1:]),
]
raise ValueError(
f"unknown help section {name!r}; available sections: "
f"{', '.join(available)}"
)

formatter = self.optparser._get_formatter()
formatter.start_section(group.description)
formatter.add_arguments([option._action for option in group.options])
formatter.end_section()
return formatter.format_help()

@staticmethod
def _get_group_help_name(group: OptionGroup) -> str:
if group.name == "custom" or group.name.startswith("group:"):
return f"group:{group.name}"
return group.name

def addoption(self, *opts: str, **attrs: Any) -> None:
"""Register a command line option.

Expand Down Expand Up @@ -429,10 +470,13 @@ def __init__(
name: str,
parser: Parser | None,
_ispytest: bool = False,
*,
description: str | None = None,
) -> None:
check_ispytest(_ispytest)
self._arggroup = arggroup
self.name = name
self.description = description or name
self.options: list[Argument] = []
self.parser = parser

Expand Down
48 changes: 47 additions & 1 deletion src/_pytest/helpconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from _pytest.config import Config
from _pytest.config import ExitCode
from _pytest.config import PrintHelp
from _pytest.config import UsageError
from _pytest.config.argparsing import _ini_type_repr
from _pytest.config.argparsing import _split_help_text
from _pytest.config.argparsing import Parser
Expand Down Expand Up @@ -58,6 +59,31 @@ def __call__(
raise PrintHelp


class HelpSectionAction(argparse.Action):
def __init__(
self,
option_strings: Sequence[str],
dest: str,
*,
default: Any = None,
help: str | None = None,
) -> None:
super().__init__(
option_strings, dest, default=default, help=help, metavar="NAME"
)

def __call__(
self,
parser: argparse.ArgumentParser,
namespace: argparse.Namespace,
values: str | Sequence[Any] | None,
option_string: str | None = None,
) -> None:
setattr(namespace, self.dest, values)
if getattr(namespace, "_raise_print_help", False):
raise PrintHelp


def pytest_addoption(parser: Parser) -> None:
group = parser.getgroup("debugconfig")
group.addoption(
Expand All @@ -76,6 +102,12 @@ def pytest_addoption(parser: Parser) -> None:
dest="help",
help="Show help message and configuration info",
)
group.addoption(
"--help-section",
action=HelpSectionAction,
default=None,
help="Show help for one option group by its name.",
)
group._addoption( # private to use reserved lower-case short option
"-p",
action="append",
Expand Down Expand Up @@ -171,6 +203,13 @@ def pytest_cmdline_main(config: Config) -> int | ExitCode | None:
if config.option.version > 1:
show_version_verbose(config)
return ExitCode.OK
elif config.option.help_section is not None:
config._do_configure()
try:
showhelp(config, section=config.option.help_section)
finally:
config._ensure_unconfigure()
return ExitCode.OK
elif config.option.help:
config._do_configure()
showhelp(config)
Expand All @@ -179,12 +218,19 @@ def pytest_cmdline_main(config: Config) -> int | ExitCode | None:
return None


def showhelp(config: Config) -> None:
def showhelp(config: Config, *, section: str | None = None) -> None:
reporter: TerminalReporter | None = config.pluginmanager.get_plugin(
"terminalreporter"
)
assert reporter is not None
tw = reporter._tw
if section is not None:
try:
tw.write(config._parser._format_group_help(section))
except ValueError as exc:
raise UsageError(str(exc)) from exc
return

tw.write(config._parser.optparser.format_help())
tw.line()
tw.line(
Expand Down
86 changes: 86 additions & 0 deletions testing/test_helpconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,92 @@ def test_help(pytester: Pytester) -> None:
)


def test_help_section_custom(pytester: Pytester) -> None:
pytester.makeconftest(
"""
def pytest_addoption(parser):
parser.addoption("--custom-option", help="custom option help")
group = parser.getgroup("example-plugin", "Example plugin")
group.addoption("--plugin-option", help="plugin option help")
"""
)

result = pytester.runpytest("--help-section=custom")
assert result.ret == ExitCode.OK
result.stdout.fnmatch_lines(
["Custom options:", "*--custom-option=CUSTOM_OPTION*", "*custom option help*"]
)
result.stdout.no_fnmatch_line("*--plugin-option*")
result.stdout.no_fnmatch_line("*usage:*")


def test_help_section_skips_required_options(pytester: Pytester) -> None:
pytester.makeconftest(
"""
def pytest_addoption(parser):
parser.addoption("--required-option", required=True, help="required option")
"""
)

result = pytester.runpytest("--help-section=custom")
assert result.ret == ExitCode.OK
result.stdout.fnmatch_lines(["*--required-option=REQUIRED_OPTION*"])


def test_help_section_named_group(pytester: Pytester) -> None:
pytester.makeconftest(
"""
def pytest_addoption(parser):
parser.addoption("--custom-option", help="custom option help")
group = parser.getgroup("example-plugin", "Example plugin")
group.addoption("--plugin-option", help="plugin option help")
"""
)

result = pytester.runpytest("--help-section=example-plugin")
assert result.ret == ExitCode.OK
result.stdout.fnmatch_lines(
["Example plugin:", "*--plugin-option=PLUGIN_OPTION*", "*plugin option help*"]
)
result.stdout.no_fnmatch_line("*--custom-option*")
result.stdout.no_fnmatch_line("*usage:*")


def test_help_section_unknown(pytester: Pytester) -> None:
result = pytester.runpytest("--help-section=unknown")
assert result.ret == ExitCode.USAGE_ERROR
result.stderr.fnmatch_lines(
["*unknown help section 'unknown'; available sections: custom, *"]
)


def test_help_section_group_named_custom(pytester: Pytester) -> None:
pytester.makeconftest(
"""
def pytest_addoption(parser):
parser.addoption("--anonymous-option", help="anonymous option help")
group = parser.getgroup("custom", "Custom plugin")
group.addoption("--named-custom-option", help="named custom option help")
group = parser.getgroup("group:plugin", "Prefixed plugin")
group.addoption("--prefixed-option", help="prefixed option help")
"""
)

result = pytester.runpytest("--help-section=custom")
assert result.ret == ExitCode.OK
result.stdout.fnmatch_lines(["*--anonymous-option=ANONYMOUS_OPTION*"])
result.stdout.no_fnmatch_line("*--named-custom-option*")

result = pytester.runpytest("--help-section=group:custom")
assert result.ret == ExitCode.OK
result.stdout.fnmatch_lines(["*--named-custom-option=NAMED_CUSTOM_OPTION*"])
result.stdout.no_fnmatch_line("*--anonymous-option*")

result = pytester.runpytest("--help-section=group:group:plugin")
assert result.ret == ExitCode.OK
result.stdout.fnmatch_lines(["*--prefixed-option=PREFIXED_OPTION*"])


def test_help_ini_union_and_literal_types(pytester: Pytester) -> None:
"""Union and Literal ini options display their members/choices in --help."""
pytester.makeconftest(
Expand Down
Loading