Skip to content

Commit ea032fe

Browse files
authored
Merge pull request #2351 from Dzhud/add-help-examples
Add usage examples to --help output
2 parents 3f292ef + 524facf commit ea032fe

File tree

6 files changed

+117
-10
lines changed

6 files changed

+117
-10
lines changed

changelog.d/1142.feature.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Add usage examples to the ``--help`` output for ``pip-compile`` and
2+
``pip-sync`` commands.
3+
4+
-- by {user}`Dzhud`

piptools/scripts/compile.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,29 @@ def _determine_linesep(
7474
}[strategy]
7575

7676

77-
@click.command(
78-
name="pip-compile",
79-
context_settings={"help_option_names": options.help_option_names},
80-
)
77+
COMPILE_EPILOG = """\b
78+
Examples:
79+
\b
80+
Compile requirements.in to requirements.txt:
81+
$ pip-compile
82+
\b
83+
Upgrade all packages to their latest versions:
84+
$ pip-compile --upgrade
85+
\b
86+
Upgrade specific packages:
87+
$ pip-compile -P django -P requests
88+
\b
89+
Include package hashes for extra security:
90+
$ pip-compile --generate-hashes
91+
\b
92+
Compile with optional extras:
93+
$ pip-compile --extra dev pyproject.toml
94+
"""
95+
96+
97+
@click.command(name="pip-compile")
8198
@click.pass_context
99+
@options.help_option(epilog=COMPILE_EPILOG)
82100
@options.version
83101
@options.color
84102
@options.verbose

piptools/scripts/options.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
from piptools.locations import CACHE_DIR, DEFAULT_CONFIG_FILE_NAMES
1010
from piptools.utils import UNSAFE_PACKAGES, override_defaults_from_config_file
1111

12+
_FC = _t.TypeVar("_FC", bound="_t.Callable[..., _t.Any] | click.Command")
13+
1214
BuildTargetT = _t.Literal["sdist", "wheel", "editable"]
1315
ALL_BUILD_TARGETS: tuple[BuildTargetT, ...] = (
1416
"editable",
@@ -17,6 +19,34 @@
1719
)
1820

1921

22+
def help_option(*, epilog: str | None = None) -> _t.Callable[[_FC], _FC]:
23+
"""A variant of the built-in click ``--help`` option, customized for pip-tools.
24+
25+
Unlike ``click.help_option``, this decorator accepts its own ``epilog`` text which
26+
is printed *without indentation* after help text.
27+
"""
28+
29+
def show_help(ctx: click.Context, param: click.Parameter, value: bool) -> None:
30+
"""Callback that print the help page on ``<stdout>`` and exits."""
31+
if value and not ctx.resilient_parsing:
32+
click.echo(ctx.get_help(), color=ctx.color)
33+
if epilog is not None:
34+
formatter = ctx.make_formatter()
35+
formatter.write_text(epilog)
36+
click.echo("\n" + formatter.getvalue().rstrip("\n"), color=ctx.color)
37+
ctx.exit()
38+
39+
return click.option( # type: ignore[return-value]
40+
"-h",
41+
"--help",
42+
help="Show this message and exit.",
43+
callback=show_help,
44+
is_eager=True,
45+
expose_value=False,
46+
is_flag=True,
47+
)
48+
49+
2050
def _get_default_option(option_name: str) -> _t.Any:
2151
"""
2252
Get default value of the pip's option (including option from pip.conf)
@@ -27,8 +57,6 @@ def _get_default_option(option_name: str) -> _t.Any:
2757
return getattr(default_values, option_name)
2858

2959

30-
help_option_names = ("-h", "--help")
31-
3260
# The options used by pip-compile and pip-sync are presented in no specific order.
3361

3462
version = click.version_option(package_name="pip-tools")

piptools/scripts/sync.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,25 @@
3030

3131
DEFAULT_REQUIREMENTS_FILE = "requirements.txt"
3232

33-
34-
@click.command(
35-
name="pip-sync", context_settings={"help_option_names": options.help_option_names}
36-
)
33+
SYNC_EPILOG = """\b
34+
Examples:
35+
\b
36+
Synchronize environment with requirements.txt:
37+
$ pip-sync
38+
\b
39+
Synchronize with multiple requirements files:
40+
$ pip-sync requirements.txt dev.txt
41+
\b
42+
Preview what would be installed or uninstalled:
43+
$ pip-sync --dry-run
44+
\b
45+
Ask for confirmation before making changes:
46+
$ pip-sync --ask
47+
"""
48+
49+
50+
@click.command(name="pip-sync")
51+
@options.help_option(epilog=SYNC_EPILOG)
3752
@options.version
3853
@options.ask
3954
@options.dry_run

tests/test_cli_compile.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,27 @@ def test_run_as_module_compile():
527527
assert b"Compile requirements.txt from source files" in result.stdout
528528

529529

530+
def test_compile_help_opt_supports_short_and_long_flag(runner):
531+
shortflag_result = runner.invoke(cli, ["-h"])
532+
longflag_result = runner.invoke(cli, ["--help"])
533+
assert shortflag_result.exit_code == 0
534+
assert longflag_result.exit_code == 0
535+
536+
assert shortflag_result.stdout.startswith("Usage:")
537+
assert longflag_result.stdout.startswith("Usage:")
538+
assert shortflag_result.stdout == longflag_result.stdout
539+
540+
541+
def test_compile_help_opt_shows_examples_section(runner):
542+
result = runner.invoke(cli, ["-h"])
543+
assert result.exit_code == 0
544+
assert result.stdout.startswith("Usage:")
545+
546+
# not only should there be an `Examples` section in the output, but it should have
547+
# no preceding whitespace where it is shown
548+
assert "\nExamples:\n" in result.stdout
549+
550+
530551
def test_editable_package(pip_conf, runner):
531552
"""piptools can compile an editable"""
532553
fake_package_dir = os.path.join(PACKAGES_PATH, "small_fake_with_deps")

tests/test_cli_sync.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,27 @@ def test_run_as_module_sync():
3333
assert b"Synchronize virtual environment with" in result.stdout
3434

3535

36+
def test_sync_help_opt_supports_short_and_long_flag(runner):
37+
shortflag_result = runner.invoke(cli, ["-h"])
38+
longflag_result = runner.invoke(cli, ["--help"])
39+
assert shortflag_result.exit_code == 0
40+
assert longflag_result.exit_code == 0
41+
42+
assert shortflag_result.stdout.startswith("Usage:")
43+
assert longflag_result.stdout.startswith("Usage:")
44+
assert shortflag_result.stdout == longflag_result.stdout
45+
46+
47+
def test_sync_help_opt_shows_examples_section(runner):
48+
result = runner.invoke(cli, ["-h"])
49+
assert result.exit_code == 0
50+
assert result.stdout.startswith("Usage:")
51+
52+
# not only should there be an `Examples` section in the output, but it should have
53+
# no preceding whitespace where it is shown
54+
assert "\nExamples:\n" in result.stdout
55+
56+
3657
@mock.patch("piptools.sync.run")
3758
def test_quiet_option(run, runner):
3859
"""sync command can be run with `--quiet` or `-q` flag."""

0 commit comments

Comments
 (0)