Skip to content

Commit b232d2d

Browse files
🐛 fix(docs): auto-generate manpage from CLI parser (#3911)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 84958f7 commit b232d2d

File tree

8 files changed

+469
-106
lines changed

8 files changed

+469
-106
lines changed

docs/changelog/3878.bugfix.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Auto-generate the manpage from the CLI argparse parser at wheel build time, fixing broken section headers and
2+
documenting all commands and options - by :user:`gaborbernat`.

docs/how-to/install.rst

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -112,14 +112,10 @@ system ``MANPATH``. Use the ``tox man`` command to set it up:
112112
Building from Source
113113
====================
114114

115-
Package maintainers building from source can generate the man page using Sphinx:
115+
The man page is compiled from ``docs/man/tox.1.rst`` during wheel build. To regenerate the RST source after CLI changes:
116116

117117
.. code-block:: bash
118118
119-
tox run -e docs
120-
121-
# Man page is generated at .tox/docs_out/man/tox.1
122-
install -D -m 644 .tox/docs_out/man/tox.1 /usr/share/man/man1/tox.1
123-
gzip -9 /usr/share/man/man1/tox.1
119+
tox run -e manpage
124120
125121
After installation, view with ``man tox``.

docs/man/tox.1.rst

Lines changed: 90 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,119 @@
11
:orphan:
22

3-
##########
4-
SYNOPSIS
5-
##########
3+
#####
4+
tox
5+
#####
66

7-
**tox** [*options*] [*command* [*command-options*]]
7+
************************************************
8+
virtualenv-based automation of test activities
9+
************************************************
810

9-
#############
10-
DESCRIPTION
11-
#############
11+
:Manual section: 1
12+
:Manual group: User Commands
13+
14+
SYNOPSIS
15+
========
16+
17+
**tox** [*options*] [**run** | **run-parallel** | **depends** | **man** | **list** | **devenv** | **schema** |
18+
**config** | **quickstart** | **exec** | **legacy**] [*command-options*]
19+
20+
DESCRIPTION
21+
===========
1222

1323
tox aims to automate and standardize testing in Python. It is part of a larger vision of easing the packaging, testing
1424
and release process of Python software.
1525

1626
tox creates virtual environments for multiple Python versions, installs project dependencies, and runs tests in each
1727
environment. It supports parallel execution, custom test commands, and extensive configuration.
1828

19-
##########
20-
COMMANDS
21-
##########
29+
COMMANDS
30+
========
31+
32+
**run** (*or* **r**)
33+
run environments
34+
35+
**run-parallel** (*or* **p**)
36+
run environments in parallel
2237

23-
**run** (*default*)
24-
Execute test environments. This is the default command if none is specified.
38+
**depends** (*or* **de**)
39+
visualize tox environment dependencies
40+
41+
**man**
42+
Set up tox man page for current shell
2543

2644
**list** (*or* **l**)
27-
List configured environments with their descriptions.
45+
list environments
46+
47+
**devenv** (*or* **d**)
48+
sets up a development environment at ENVDIR based on the tox configuration specified
49+
50+
**schema**
51+
Generate schema for tox configuration
2852

2953
**config** (*or* **c**)
30-
Show tox configuration details for debugging and inspection.
54+
show tox configuration
3155

32-
**exec** (*or* **e**)
33-
Execute a command in a tox environment without running the full test suite.
56+
**quickstart** (*or* **q**)
57+
Command line script to quickly create a tox config file for a Python project
3458

35-
**devenv** (*or* **d**)
36-
Create a development environment from a tox environment definition.
59+
**exec** (*or* **e**)
60+
execute an arbitrary command within a tox environment
3761

38-
**legacy**
39-
Legacy tox 3.x compatibility mode for older configurations.
62+
**legacy** (*or* **le**)
63+
legacy entry-point command
4064

4165
For command-specific help, use: **tox** *command* **--help**
4266

43-
#########
44-
OPTIONS
45-
#########
67+
OPTIONS
68+
=======
69+
70+
**-h**, **--help**
71+
show this help message and exit
72+
73+
**--colored**
74+
should output be enriched with colors, default is yes unless TERM=dumb or NO_COLOR is defined.
75+
76+
**--stderr-color**
77+
color for stderr output, use RESET for terminal defaults.
78+
79+
**-v**, **--verbose**
80+
increase verbosity
81+
82+
**-q**, **--quiet**
83+
decrease verbosity
4684

47-
For a complete list of options, run ``tox --help`` or see the online documentation at https://tox.wiki/
85+
**--exit-and-dump-after** *seconds*
86+
dump tox threads after n seconds and exit the app - useful to debug when tox hangs, 0 means disabled
4887

49-
Common options:
88+
**-c**, **--conf** *file*
89+
configuration file/folder for tox (if not specified will discover one)
5090

51-
**-h, --help**
52-
Show help message and exit.
91+
**--workdir** *dir*
92+
tox working directory (if not specified will be the folder of the config file)
5393

54-
**-v, --verbose**
55-
Increase verbosity (can be used multiple times).
94+
**--root** *dir*
95+
project root directory (if not specified will be the folder of the config file)
5696

57-
**-q, --quiet**
58-
Decrease verbosity (can be used multiple times).
97+
**--runner**
98+
the tox run engine to use when not explicitly stated in tox env configuration
5999

60-
**-r, --recreate**
61-
Recreate the test environment.
100+
**--version**
101+
show program's and plugins version number and exit
62102

63-
**-e** *ENV*
64-
Run specific test environments (comma-separated).
103+
**--no-provision** *REQ_JSON*
104+
do not perform provision, but fail and if a path was provided write provision metadata as JSON to it
65105

66-
**--conf** *FILE*
67-
Configuration file to use.
106+
**--no-recreate-provision**
107+
if recreate is set do not recreate provision tox environment
68108

69-
**--workdir** *DIR*
70-
tox working directory (default: .tox).
109+
**-r**, **--recreate**
110+
recreate the tox environments
71111

72-
**--override** *KEY=VALUE*, **-x** *KEY=VALUE*
73-
Override a configuration value.
112+
**-x**, **--override**
113+
configuration override(s), e.g., -x testenv:pypy3.ignore_errors=True
74114

75-
#######
76-
FILES
77-
#######
115+
FILES
116+
=====
78117

79118
**tox.toml**
80119
Primary configuration file in TOML format (recommended).
@@ -90,13 +129,11 @@ Common options:
90129

91130
The configuration files are searched in the order listed above. The first file found is used.
92131

93-
#######################
94-
ENVIRONMENT VARIABLES
95-
#######################
132+
ENVIRONMENT VARIABLES
133+
=====================
96134

97135
``TOX_*``
98-
Any tox configuration setting can be overridden via environment variables with the ``TOX_`` prefix. For example,
99-
``TOX_SKIP_ENV`` can override the ``skip_env`` setting.
136+
Any tox configuration setting can be overridden via environment variables with the ``TOX_`` prefix.
100137

101138
**NO_COLOR**
102139
When set to any non-empty value, disables colored output.
@@ -107,17 +144,15 @@ The configuration files are searched in the order listed above. The first file f
107144
**TOX_PARALLEL_NO_SPINNER**
108145
When set, disables the progress spinner during parallel execution.
109146

110-
##########
111-
SEE ALSO
112-
##########
147+
SEE ALSO
148+
========
113149

114150
Full documentation: https://tox.wiki/
115151

116152
**pip**\(1), **pytest**\(1), **virtualenv**\(1)
117153

118-
########
119-
AUTHOR
120-
########
154+
AUTHOR
155+
======
121156

122157
tox development team
123158

docs/reference/cli.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ Then configure your shell:
4646
Once configured, pressing ``<TAB>`` completes subcommands (``tox r`` → ``tox run``), flags (``tox run --``), and
4747
environment names (``tox run -e`` lists environments from your tox configuration).
4848

49+
**********
50+
Man page
51+
**********
52+
53+
tox ships a Unix man page accessible via ``man tox`` (see :ref:`howto` for setup). The man page source is at
54+
``docs/man/tox.1.rst`` and can be regenerated from the CLI parser with ``tox run -e manpage``.
55+
4956
**********************
5057
Command-line options
5158
**********************

hatch_build.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def initialize(self, version: str, build_data: dict[str, Any]) -> None: # noqa:
1919
for line in (root / "docs" / "man" / "tox.1.rst").read_text(encoding="utf-8").splitlines()
2020
if line.strip() != ":orphan:"
2121
),
22-
writer_name="manpage",
22+
writer="manpage",
2323
settings_overrides={"report_level": 5},
2424
)
2525
)

tests/docs/test_manpage.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
from __future__ import annotations
2+
3+
import re
4+
import subprocess
5+
import sys
6+
from pathlib import Path
7+
8+
import pytest
9+
10+
if sys.platform == "win32":
11+
pytest.skip("man command not available on Windows", allow_module_level=True)
12+
13+
ROOT = Path(__file__).parents[2]
14+
RST_PATH = ROOT / "docs" / "man" / "tox.1.rst"
15+
16+
17+
@pytest.fixture
18+
def manpage_troff() -> bytes:
19+
from docutils.core import publish_string # noqa: PLC0415
20+
21+
content = "\n".join(
22+
line for line in RST_PATH.read_text(encoding="utf-8").splitlines() if line.strip() != ":orphan:"
23+
)
24+
return publish_string(content, writer="manpage", settings_overrides={"report_level": 5})
25+
26+
27+
@pytest.fixture
28+
def manpage_rendered(manpage_troff: bytes, tmp_path: Path) -> str:
29+
man_file = tmp_path / "tox.1"
30+
man_file.write_bytes(manpage_troff)
31+
result = subprocess.run(
32+
["man", str(man_file)], # noqa: S607
33+
capture_output=True,
34+
text=True,
35+
env={"COLUMNS": "200", "LANG": "en_US.UTF-8", "PATH": "/usr/bin:/bin", "MANPAGER": "cat", "PAGER": "cat"},
36+
check=False,
37+
)
38+
return re.sub(r".\x08", "", result.stdout)
39+
40+
41+
def test_manpage_has_title_header(manpage_troff: bytes) -> None:
42+
output = manpage_troff.decode()
43+
match = re.search(r'^\.TH "([^"]*)" "([^"]*)" "([^"]*)" "([^"]*)" "([^"]*)"', output, re.MULTILINE)
44+
assert match is not None, f".TH header not found in:\n{output[:500]}"
45+
assert match.group(1) == "tox"
46+
assert match.group(2) == "1"
47+
assert match.group(5) == "User Commands"
48+
49+
50+
def test_manpage_has_name_section(manpage_troff: bytes) -> None:
51+
output = manpage_troff.decode()
52+
match = re.search(r"\.SH Name\n(.+)", output)
53+
assert match is not None, "Name section not found"
54+
assert "tox" in match.group(1)
55+
assert "virtualenv-based automation of test activities" in match.group(1)
56+
57+
58+
def test_manpage_has_all_sections(manpage_troff: bytes) -> None:
59+
output = manpage_troff.decode()
60+
sections = re.findall(r"^\.SH (.+)$", output, re.MULTILINE)
61+
expected = [
62+
"Name",
63+
"SYNOPSIS",
64+
"DESCRIPTION",
65+
"COMMANDS",
66+
"OPTIONS",
67+
"FILES",
68+
"ENVIRONMENT VARIABLES",
69+
"SEE ALSO",
70+
"AUTHOR",
71+
]
72+
assert sections == expected
73+
74+
75+
def test_manpage_renders_sections(manpage_rendered: str) -> None:
76+
assert "tox" in manpage_rendered
77+
for section in ("SYNOPSIS", "DESCRIPTION", "COMMANDS", "OPTIONS", "FILES", "SEE ALSO", "AUTHOR"):
78+
assert section in manpage_rendered, f"section {section!r} missing from rendered man output"
79+
80+
81+
def test_manpage_name_not_empty(manpage_rendered: str) -> None:
82+
lines = manpage_rendered.splitlines()
83+
name_idx = next((i for i, line in enumerate(lines) if "Name" in line or "NAME" in line), None)
84+
assert name_idx is not None, "NAME section not found in rendered output"
85+
name_line = lines[name_idx + 1].strip()
86+
assert "tox" in name_line
87+
assert "virtualenv-based automation of test activities" in name_line
88+
89+
90+
def test_manpage_header_shows_tox(manpage_rendered: str) -> None:
91+
first_line = manpage_rendered.splitlines()[0]
92+
assert "tox" in first_line.lower()
93+
94+
95+
def test_manpage_documents_all_commands() -> None:
96+
from argparse import _SubParsersAction # noqa: PLC0415, PLC2701
97+
98+
from tox.config.cli.parse import _get_parser_doc # noqa: PLC0415, PLC2701
99+
100+
parser = _get_parser_doc()
101+
rst = RST_PATH.read_text(encoding="utf-8")
102+
assert parser._subparsers is not None # noqa: SLF001
103+
for action in parser._subparsers._actions: # noqa: SLF001
104+
if isinstance(action, _SubParsersAction):
105+
for choice_action in action._choices_actions: # noqa: SLF001
106+
assert choice_action.dest in rst, (
107+
f"command {choice_action.dest!r} missing from manpage, regenerate with: "
108+
f"python tools/generate_manpage.py"
109+
)
110+
111+
112+
def test_manpage_documents_all_options() -> None:
113+
from argparse import SUPPRESS, _SubParsersAction # noqa: PLC0415, PLC2701
114+
115+
from tox.config.cli.parse import _get_parser_doc # noqa: PLC0415, PLC2701
116+
117+
parser = _get_parser_doc()
118+
rst = RST_PATH.read_text(encoding="utf-8")
119+
seen: set[int] = set()
120+
for action in parser._actions: # noqa: SLF001
121+
if id(action) in seen or action.help == SUPPRESS or isinstance(action, _SubParsersAction):
122+
continue
123+
seen.add(id(action))
124+
if not action.option_strings:
125+
continue
126+
long_opt = next((o for o in action.option_strings if o.startswith("--")), action.option_strings[0])
127+
assert long_opt in rst, (
128+
f"option {long_opt!r} missing from manpage, regenerate with: python tools/generate_manpage.py"
129+
)

0 commit comments

Comments
 (0)