From 42471b20dc77cd1107547df29b29d7903f4db586 Mon Sep 17 00:00:00 2001 From: timacias <147666644+timacias@users.noreply.github.com> Date: Tue, 10 Sep 2024 16:29:57 -0400 Subject: [PATCH 01/12] Initial awareness of 'help' command Initial addition of a `commands/help.py` file Add "help" to `commands/__init__.py` to make pipx aware of a new command --- src/pipx/commands/__init__.py | 2 ++ src/pipx/commands/help.py | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 src/pipx/commands/help.py diff --git a/src/pipx/commands/__init__.py b/src/pipx/commands/__init__.py index 69d3a1423a..c905418cd2 100644 --- a/src/pipx/commands/__init__.py +++ b/src/pipx/commands/__init__.py @@ -11,6 +11,7 @@ from pipx.commands.uninject import uninject from pipx.commands.uninstall import uninstall, uninstall_all from pipx.commands.upgrade import upgrade, upgrade_all, upgrade_shared +import pipx.commands.help __all__ = [ "upgrade", @@ -34,4 +35,5 @@ "pin", "unpin", "upgrade_interpreters", + "help", ] diff --git a/src/pipx/commands/help.py b/src/pipx/commands/help.py new file mode 100644 index 0000000000..dabd3347a1 --- /dev/null +++ b/src/pipx/commands/help.py @@ -0,0 +1,2 @@ +# The help command only reuses existing features +# This file exists to satisfy __all__ in pipx/commands/__init__.py From 93543aeceb487b99a1c88448a90bb752227e8be9 Mon Sep 17 00:00:00 2001 From: timacias <147666644+timacias@users.noreply.github.com> Date: Tue, 10 Sep 2024 16:32:34 -0400 Subject: [PATCH 02/12] Alias help command to --help flag in main.py Added an _add_help function which informs argparse of a "help" command, and an optional argument for it called "subcommand" Added a check for args.command == "help" --- src/pipx/main.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/pipx/main.py b/src/pipx/main.py index 4800f64313..fbdb418e36 100644 --- a/src/pipx/main.py +++ b/src/pipx/main.py @@ -423,6 +423,14 @@ def run_pipx_command(args: argparse.Namespace, subparsers: Dict[str, argparse.Ar return ExitCode(0) elif args.command == "environment": return commands.environment(value=args.value) + elif args.command == "help": + if args.subcommand is not None: + sys.argv[1] = args.subcommand + sys.argv[2] = "--help" + cli() + else: + parser, _ = get_command_parser() + parser.print_help() else: raise PipxError(f"Unknown command {args.command}") @@ -928,6 +936,21 @@ def _add_environment(subparsers: argparse._SubParsersAction, shared_parser: argp p.add_argument("--value", "-V", metavar="VARIABLE", help="Print the value of the variable.") +def _add_help(subparsers: argparse._SubParsersAction, shared_parser: argparse.ArgumentParser) -> None: + p = subparsers.add_parser( + "help", + help="Print out help info for pipx or for a particular command (alias of --help).", + description="Print out help info for pipx or for a particular command (alias of --help).", + parents=[shared_parser], + ) + p.add_argument( + "subcommand", + choices=commands.__all__, + nargs='?', + help="Print out help for the specified command, if provided, using pipx help COMMAND", + ) + + def get_command_parser() -> Tuple[argparse.ArgumentParser, Dict[str, argparse.ArgumentParser]]: venv_container = VenvContainer(paths.ctx.venvs) @@ -973,7 +996,8 @@ def get_command_parser() -> Tuple[argparse.ArgumentParser, Dict[str, argparse.Ar ) parser.man_short_description = PIPX_DESCRIPTION.splitlines()[1] # type: ignore[attr-defined] - subparsers = parser.add_subparsers(dest="command", description="Get help for commands with pipx COMMAND --help") + subparsers = parser.add_subparsers(dest="command", description="Get help for commands with pipx COMMAND --help\n" + "\t\t\t or pipx help COMMAND") subparsers_with_subcommands = {} _add_install(subparsers, shared_parser) @@ -995,6 +1019,7 @@ def get_command_parser() -> Tuple[argparse.ArgumentParser, Dict[str, argparse.Ar _add_runpip(subparsers, completer_venvs.use, shared_parser) _add_ensurepath(subparsers, shared_parser) _add_environment(subparsers, shared_parser) + _add_help(subparsers, shared_parser) parser.add_argument("--version", action="store_true", help="Print version and exit") subparsers.add_parser( From d9e3818577f975963da8769b34c516931004bd54 Mon Sep 17 00:00:00 2001 From: timacias <147666644+timacias@users.noreply.github.com> Date: Tue, 10 Sep 2024 16:36:13 -0400 Subject: [PATCH 03/12] Added changelog for Issue #1535 --- changelog.d/1535.feature.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelog.d/1535.feature.md diff --git a/changelog.d/1535.feature.md b/changelog.d/1535.feature.md new file mode 100644 index 0000000000..cd59f19e7a --- /dev/null +++ b/changelog.d/1535.feature.md @@ -0,0 +1,2 @@ +`pipx help COMMAND` can now be used as an alias for `pipx COMMAND --help`, +where COMMAND is a valid pipx command, or nothing. \ No newline at end of file From 3ebca3147c2f5b30a8ef6ea148737b4b6d874d96 Mon Sep 17 00:00:00 2001 From: timacias <147666644+timacias@users.noreply.github.com> Date: Tue, 10 Sep 2024 17:25:46 -0400 Subject: [PATCH 04/12] Add whitespace --- changelog.d/1535.feature.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog.d/1535.feature.md b/changelog.d/1535.feature.md index cd59f19e7a..390c3b56d1 100644 --- a/changelog.d/1535.feature.md +++ b/changelog.d/1535.feature.md @@ -1,2 +1,2 @@ -`pipx help COMMAND` can now be used as an alias for `pipx COMMAND --help`, -where COMMAND is a valid pipx command, or nothing. \ No newline at end of file +`pipx help COMMAND` can now be used as an alias for `pipx COMMAND --help`, +where COMMAND is a valid pipx command, or nothing. From 8018877d355d2d5296e56fd8367bf50afa2fa3ac Mon Sep 17 00:00:00 2001 From: timacias <147666644+timacias@users.noreply.github.com> Date: Tue, 10 Sep 2024 17:27:12 -0400 Subject: [PATCH 05/12] Remove unused import from __init__.py --- src/pipx/commands/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pipx/commands/__init__.py b/src/pipx/commands/__init__.py index c905418cd2..aba154f1df 100644 --- a/src/pipx/commands/__init__.py +++ b/src/pipx/commands/__init__.py @@ -11,7 +11,6 @@ from pipx.commands.uninject import uninject from pipx.commands.uninstall import uninstall, uninstall_all from pipx.commands.upgrade import upgrade, upgrade_all, upgrade_shared -import pipx.commands.help __all__ = [ "upgrade", From c20c26c6a0f0b0d2986354d50791413237cfef15 Mon Sep 17 00:00:00 2001 From: timacias <147666644+timacias@users.noreply.github.com> Date: Tue, 10 Sep 2024 17:32:42 -0400 Subject: [PATCH 06/12] Add missing return statements to main.py --- src/pipx/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pipx/main.py b/src/pipx/main.py index fbdb418e36..5e010d412b 100644 --- a/src/pipx/main.py +++ b/src/pipx/main.py @@ -427,10 +427,11 @@ def run_pipx_command(args: argparse.Namespace, subparsers: Dict[str, argparse.Ar if args.subcommand is not None: sys.argv[1] = args.subcommand sys.argv[2] = "--help" - cli() + return cli() else: parser, _ = get_command_parser() parser.print_help() + return ExitCode(0) else: raise PipxError(f"Unknown command {args.command}") From bce88bf90748cf5a9b162522b9750cdefa84c005 Mon Sep 17 00:00:00 2001 From: timacias <147666644+timacias@users.noreply.github.com> Date: Tue, 10 Sep 2024 17:55:06 -0400 Subject: [PATCH 07/12] Remove implicit string concat in main.py --- src/pipx/main.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pipx/main.py b/src/pipx/main.py index 5e010d412b..c3b7cab23d 100644 --- a/src/pipx/main.py +++ b/src/pipx/main.py @@ -997,9 +997,10 @@ def get_command_parser() -> Tuple[argparse.ArgumentParser, Dict[str, argparse.Ar ) parser.man_short_description = PIPX_DESCRIPTION.splitlines()[1] # type: ignore[attr-defined] - subparsers = parser.add_subparsers(dest="command", description="Get help for commands with pipx COMMAND --help\n" - "\t\t\t or pipx help COMMAND") - + subparsers = parser.add_subparsers( + dest="command", description="Get help for commands with pipx COMMAND --help\n\t\t\t or pipx help COMMAND" + ) + subparsers_with_subcommands = {} _add_install(subparsers, shared_parser) _add_install_all(subparsers, shared_parser) From 50b0e87161b97a07edd8d9eaed29ec3dc01fc595 Mon Sep 17 00:00:00 2001 From: timacias <147666644+timacias@users.noreply.github.com> Date: Tue, 10 Sep 2024 17:58:53 -0400 Subject: [PATCH 08/12] Update main.py Reflects requirements set by the linter --- src/pipx/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pipx/main.py b/src/pipx/main.py index c3b7cab23d..28a6f23365 100644 --- a/src/pipx/main.py +++ b/src/pipx/main.py @@ -947,7 +947,7 @@ def _add_help(subparsers: argparse._SubParsersAction, shared_parser: argparse.Ar p.add_argument( "subcommand", choices=commands.__all__, - nargs='?', + nargs="?", help="Print out help for the specified command, if provided, using pipx help COMMAND", ) @@ -1000,7 +1000,7 @@ def get_command_parser() -> Tuple[argparse.ArgumentParser, Dict[str, argparse.Ar subparsers = parser.add_subparsers( dest="command", description="Get help for commands with pipx COMMAND --help\n\t\t\t or pipx help COMMAND" ) - + subparsers_with_subcommands = {} _add_install(subparsers, shared_parser) _add_install_all(subparsers, shared_parser) From 6a6fb0efd21b923d0f76f843005a8f6d0705bcba Mon Sep 17 00:00:00 2001 From: timacias <147666644+timacias@users.noreply.github.com> Date: Wed, 25 Sep 2024 18:36:38 -0400 Subject: [PATCH 09/12] help subcommand now works with multiple arguments Example: `pipx help interpreter list` now has an identical output to `pipx interpreter list --help` --- src/pipx/main.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/pipx/main.py b/src/pipx/main.py index 28a6f23365..bdb400c15a 100644 --- a/src/pipx/main.py +++ b/src/pipx/main.py @@ -424,9 +424,10 @@ def run_pipx_command(args: argparse.Namespace, subparsers: Dict[str, argparse.Ar elif args.command == "environment": return commands.environment(value=args.value) elif args.command == "help": - if args.subcommand is not None: - sys.argv[1] = args.subcommand - sys.argv[2] = "--help" + if args.subcommands is not None: + for i in range(2, len(sys.argv)): + sys.argv[i - 1] = sys.argv[i] + sys.argv[len(sys.argv) - 1] = "--help" return cli() else: parser, _ = get_command_parser() @@ -945,9 +946,8 @@ def _add_help(subparsers: argparse._SubParsersAction, shared_parser: argparse.Ar parents=[shared_parser], ) p.add_argument( - "subcommand", - choices=commands.__all__, - nargs="?", + "subcommands", + nargs="*", help="Print out help for the specified command, if provided, using pipx help COMMAND", ) @@ -998,7 +998,7 @@ def get_command_parser() -> Tuple[argparse.ArgumentParser, Dict[str, argparse.Ar parser.man_short_description = PIPX_DESCRIPTION.splitlines()[1] # type: ignore[attr-defined] subparsers = parser.add_subparsers( - dest="command", description="Get help for commands with pipx COMMAND --help\n\t\t\t or pipx help COMMAND" + dest="command", description="Get help for commands with pipx COMMAND --help or pipx help COMMAND" ) subparsers_with_subcommands = {} From ce57a5f7b0b9be7ea2abc2e99e504b6c3e66a7ca Mon Sep 17 00:00:00 2001 From: timacias <147666644+timacias@users.noreply.github.com> Date: Thu, 26 Sep 2024 22:29:01 -0400 Subject: [PATCH 10/12] Remove redundant else branch for help command --- src/pipx/main.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/pipx/main.py b/src/pipx/main.py index bdb400c15a..21d9c1b5b3 100644 --- a/src/pipx/main.py +++ b/src/pipx/main.py @@ -424,15 +424,10 @@ def run_pipx_command(args: argparse.Namespace, subparsers: Dict[str, argparse.Ar elif args.command == "environment": return commands.environment(value=args.value) elif args.command == "help": - if args.subcommands is not None: - for i in range(2, len(sys.argv)): - sys.argv[i - 1] = sys.argv[i] - sys.argv[len(sys.argv) - 1] = "--help" - return cli() - else: - parser, _ = get_command_parser() - parser.print_help() - return ExitCode(0) + for i in range(2, len(sys.argv)): + sys.argv[i - 1] = sys.argv[i] + sys.argv[len(sys.argv) - 1] = "--help" + return cli() else: raise PipxError(f"Unknown command {args.command}") From 82855d9cc33ed8f49b82b968d4b810fcdd5ea8a0 Mon Sep 17 00:00:00 2001 From: timacias <147666644+timacias@users.noreply.github.com> Date: Thu, 26 Sep 2024 22:40:37 -0400 Subject: [PATCH 11/12] Add unit tests for the help subcommand The line `valid_commands = parser._subparsers._actions[4].choices.keys()` causes the linter to throw an error: ``` mypy.....................................................................Failed - hook id: mypy - exit code: 1 tests/test_help.py:23: error: Item "None" of "_ArgumentGroup | None" has no attribute "_actions" [union-attr] tests/test_help.py:23: error: Item "Iterable[Any]" of "Iterable[Any] | Any | None" has no attribute "keys" [union-attr] tests/test_help.py:23: error: Item "None" of "Iterable[Any] | Any | None" has no attribute "keys" [union-attr] Found 3 errors in 1 file (checked 76 source files) ``` All linter tests pass normally when that line is substituted with a hard-coded list of valid pipx commands. --- tests/test_help.py | 96 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 tests/test_help.py diff --git a/tests/test_help.py b/tests/test_help.py new file mode 100644 index 0000000000..70b37b5543 --- /dev/null +++ b/tests/test_help.py @@ -0,0 +1,96 @@ +from helpers import run_pipx_cli +from pipx.main import get_command_parser + + +def test_help(capsys): + try: + run_pipx_cli(["--help"]) + except SystemExit: + flag_capture = capsys.readouterr() + assert "usage: pipx [-h]" in flag_capture.out + + try: + run_pipx_cli(["help"]) + except SystemExit: + command_capture = capsys.readouterr() + assert "usage: pipx [-h]" in command_capture.out + + assert flag_capture == command_capture + + +def test_help_with_subcommands(capsys): + parser, _ = get_command_parser() + # The following line generates an attribute error from the linter, but executes normally + valid_commands = parser._subparsers._actions[4].choices.keys() # First four actions contain None + for command in valid_commands: + try: + run_pipx_cli([command, "--help"]) + except SystemExit: + flag_capture = capsys.readouterr() + assert "usage: pipx " + command in flag_capture.out + + try: + run_pipx_cli(["help", command]) + except SystemExit: + command_capture = capsys.readouterr() + assert "usage: pipx " + command in flag_capture.out + + assert flag_capture == command_capture + + +def test_help_with_multiple_subcommands(capsys): + try: + run_pipx_cli(["install", "cowsaypy", "black", "--help"]) + except SystemExit: + flag_capture = capsys.readouterr() + assert "usage: pipx install" in flag_capture.out + + try: + run_pipx_cli(["help", "install", "cowsaypy", "black"]) + except SystemExit: + command_capture = capsys.readouterr() + assert "usage: pipx install" in flag_capture.out + + assert flag_capture == command_capture + + try: + run_pipx_cli(["interpreter", "list", "--help"]) + except SystemExit: + flag_capture = capsys.readouterr() + assert "usage: pipx interpreter list" in flag_capture.out + + try: + run_pipx_cli(["help", "interpreter", "list"]) + except SystemExit: + command_capture = capsys.readouterr() + assert "usage: pipx interpreter list" in flag_capture.out + + assert flag_capture == command_capture + + try: + run_pipx_cli(["interpreter", "prune", "--help"]) + except SystemExit: + flag_capture = capsys.readouterr() + assert "usage: pipx interpreter prune" in flag_capture.out + + try: + run_pipx_cli(["help", "interpreter", "prune"]) + except SystemExit: + command_capture = capsys.readouterr() + assert "usage: pipx interpreter prune" in flag_capture.out + + assert flag_capture == command_capture + + try: + run_pipx_cli(["interpreter", "upgrade", "--help"]) + except SystemExit: + flag_capture = capsys.readouterr() + assert "usage: pipx interpreter upgrade" in flag_capture.out + + try: + run_pipx_cli(["help", "interpreter", "upgrade"]) + except SystemExit: + command_capture = capsys.readouterr() + assert "usage: pipx interpreter upgrade" in flag_capture.out + + assert flag_capture == command_capture From c1c5a5bbf1b1e5945f76eb2521883703233de0f3 Mon Sep 17 00:00:00 2001 From: timacias <147666644+timacias@users.noreply.github.com> Date: Fri, 4 Oct 2024 18:01:53 -0400 Subject: [PATCH 12/12] Use an explicit list of valid pipx commands Rather than accessing a protected attribute of `parser._subparsers` Note: a commented out line with the protected access is still included in the event that the list of valid pipx commands changes --- tests/test_help.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/tests/test_help.py b/tests/test_help.py index 70b37b5543..e11b47c418 100644 --- a/tests/test_help.py +++ b/tests/test_help.py @@ -20,8 +20,32 @@ def test_help(capsys): def test_help_with_subcommands(capsys): parser, _ = get_command_parser() - # The following line generates an attribute error from the linter, but executes normally - valid_commands = parser._subparsers._actions[4].choices.keys() # First four actions contain None + # The list of valid pipx commands was generated using the following line + # valid_commands = parser._subparsers._actions[4].choices.keys() # First four actions contain None + valid_commands = [ + "install", + "install-all", + "uninject", + "inject", + "pin", + "unpin", + "upgrade", + "upgrade-all", + "upgrade-shared", + "uninstall", + "uninstall-all", + "reinstall", + "reinstall-all", + "list", + "interpreter", + "run", + "runpip", + "ensurepath", + "environment", + "help", + "completions", + ] + for command in valid_commands: try: run_pipx_cli([command, "--help"])