From 4a8a3cebeb3d9b6a463c64bec0391859828157ef Mon Sep 17 00:00:00 2001 From: James Myatt Date: Wed, 8 May 2024 09:55:07 +0100 Subject: [PATCH 01/51] Naive `pipx inject -r requirements.txt` Fixes #934 --- src/pipx/commands/inject.py | 28 +++++++++++++++++++++++++++- src/pipx/main.py | 13 ++++++++++++- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/pipx/commands/inject.py b/src/pipx/commands/inject.py index 0e07457414..59000b9a14 100644 --- a/src/pipx/commands/inject.py +++ b/src/pipx/commands/inject.py @@ -1,4 +1,5 @@ import os +import re import sys from pathlib import Path from typing import List, Optional @@ -11,6 +12,8 @@ from pipx.util import PipxError, pipx_wrap from pipx.venv import Venv +COMMENT_RE = re.compile(r"(^|\s+)#.*$") + def inject_dep( venv_dir: Path, @@ -103,6 +106,7 @@ def inject( venv_dir: Path, package_name: Optional[str], package_specs: List[str], + requirement_files: List[str], pip_args: List[str], *, verbose: bool, @@ -112,10 +116,16 @@ def inject( suffix: bool = False, ) -> ExitCode: """Returns pipx exit code.""" + packages = package_specs[:] + for filename in requirement_files: + packages.extend(parse_requirements(filename)) + if not packages: + raise ValueError("No packages have been specified.") + if not include_apps and include_dependencies: include_apps = True all_success = True - for dep in package_specs: + for dep in packages: all_success &= inject_dep( venv_dir, None, @@ -130,3 +140,19 @@ def inject( # Any failure to install will raise PipxError, otherwise success return EXIT_CODE_OK if all_success else EXIT_CODE_INJECT_ERROR + + +def parse_requirements(filename: str) -> Generator[str, None, None]: + """ + Extracts package specifications from requirements file. + + Just returns all of the non-empty lines with comments removed. + """ + # Based on https://github.com/pypa/pip/blob/main/src/pip/_internal/req/req_file.py + with open(filename) as f: + for line in f: + # Strip comments and filter empty lines + line = COMMENT_RE.sub("", line) + line = line.strip() + if line: + yield line diff --git a/src/pipx/main.py b/src/pipx/main.py index 60f6145f1f..6803a3e597 100644 --- a/src/pipx/main.py +++ b/src/pipx/main.py @@ -297,6 +297,7 @@ def run_pipx_command(args: argparse.Namespace, subparsers: Dict[str, argparse.Ar venv_dir, None, args.dependencies, + args.requirements, pip_args, verbose=verbose, include_apps=args.include_apps, @@ -515,9 +516,19 @@ def _add_inject(subparsers, venv_completer: VenvCompleter, shared_parser: argpar ).completer = venv_completer p.add_argument( "dependencies", - nargs="+", + nargs="*", help="the packages to inject into the Virtual Environment--either package name or pip package spec", ) + p.add_argument( + "-r", + "--requirement", + dest="requirements", + action="append", + default=[], + metavar="file", + help="Read packages from the requirements file to inject into the Virtual Environment. " + "Note that only files that contain lists of packages are currently supported.", + ) p.add_argument( "--include-apps", action="store_true", From 9a338c0b611a3b6009bd06e7534819319925d6e6 Mon Sep 17 00:00:00 2001 From: James Myatt Date: Wed, 8 May 2024 09:55:07 +0100 Subject: [PATCH 02/51] Fix imports --- src/pipx/commands/inject.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pipx/commands/inject.py b/src/pipx/commands/inject.py index 59000b9a14..49c4e1777c 100644 --- a/src/pipx/commands/inject.py +++ b/src/pipx/commands/inject.py @@ -2,7 +2,7 @@ import re import sys from pathlib import Path -from typing import List, Optional +from typing import Generator, List, Optional from pipx import paths from pipx.colors import bold From 1e80bfe872e3d3a1851cca75aa618474a8b46643 Mon Sep 17 00:00:00 2001 From: James Myatt Date: Wed, 8 May 2024 09:55:07 +0100 Subject: [PATCH 03/51] Better combination of packages --- src/pipx/commands/inject.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/pipx/commands/inject.py b/src/pipx/commands/inject.py index 49c4e1777c..cfd4b960d2 100644 --- a/src/pipx/commands/inject.py +++ b/src/pipx/commands/inject.py @@ -1,3 +1,4 @@ +import logging import os import re import sys @@ -12,6 +13,8 @@ from pipx.util import PipxError, pipx_wrap from pipx.venv import Venv +logger = logging.getLogger(__name__) + COMMENT_RE = re.compile(r"(^|\s+)#.*$") @@ -116,12 +119,15 @@ def inject( suffix: bool = False, ) -> ExitCode: """Returns pipx exit code.""" - packages = package_specs[:] + # Combined list of packages + packages = list(package_specs) for filename in requirement_files: packages.extend(parse_requirements(filename)) + logger.debug("Injecting packages: %r", sorted(packages)) if not packages: - raise ValueError("No packages have been specified.") + raise PipxError("No packages have been specified.") + # Inject packages if not include_apps and include_dependencies: include_apps = True all_success = True @@ -142,7 +148,7 @@ def inject( return EXIT_CODE_OK if all_success else EXIT_CODE_INJECT_ERROR -def parse_requirements(filename: str) -> Generator[str, None, None]: +def parse_requirements(filename: os.PathLike) -> Generator[str, None, None]: """ Extracts package specifications from requirements file. From 500d6dda3f2ca756ef367797d6013d2495675687 Mon Sep 17 00:00:00 2001 From: James Myatt Date: Wed, 8 May 2024 09:55:08 +0100 Subject: [PATCH 04/51] Better help text for `inject -r` --- src/pipx/main.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pipx/main.py b/src/pipx/main.py index 6803a3e597..2827787f4d 100644 --- a/src/pipx/main.py +++ b/src/pipx/main.py @@ -524,10 +524,9 @@ def _add_inject(subparsers, venv_completer: VenvCompleter, shared_parser: argpar "--requirement", dest="requirements", action="append", - default=[], metavar="file", - help="Read packages from the requirements file to inject into the Virtual Environment. " - "Note that only files that contain lists of packages are currently supported.", + help="file containing the packages to inject into the Virtual Environment--" + "one package name or pip package spec per line." ) p.add_argument( "--include-apps", From 988bbe8a16c124e21a356b7bb9bbae86da026b0b Mon Sep 17 00:00:00 2001 From: James Myatt Date: Wed, 8 May 2024 09:55:08 +0100 Subject: [PATCH 05/51] Add unit test --- tests/test_inject.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/test_inject.py b/tests/test_inject.py index fb2d177e11..eddf81f2c6 100644 --- a/tests/test_inject.py +++ b/tests/test_inject.py @@ -1,3 +1,5 @@ +import textwrap + import pytest # type: ignore from helpers import PIPX_METADATA_LEGACY_VERSIONS, mock_legacy_venv, run_pipx_cli, skip_if_windows @@ -53,3 +55,20 @@ def test_inject_include_apps(pipx_temp_env, capsys, with_suffix): assert run_pipx_cli(["inject", "pycowsay", PKG["black"]["spec"], "--include-deps"]) assert not run_pipx_cli(["inject", f"pycowsay{suffix}", PKG["black"]["spec"], "--include-deps"]) + +def test_inject_with_req_file(pipx_temp_env, capsys, tmp_path): + req_file = tmp_path / "requirements.txt" + req_file.write_text( + textwrap.dedent( + f""" + {PKG["black"]["spec"]} + {PKG["nox"]["spec"]} + {PKG["pylint"]["spec"]} + """ + ).strip() + ) + assert not run_pipx_cli(["install", "pycowsay"]) + assert not run_pipx_cli(["inject", "pycowsay", "--requirement", str(req_file)]) + assert not run_pipx_cli( + ["inject", "pycowsay", PKG["black"]["spec"], "--requirement", str(req_file)] + ) From daa5c800d49e87d1c810da272ff25bc2dfe2a1c6 Mon Sep 17 00:00:00 2001 From: James Myatt Date: Wed, 8 May 2024 09:55:08 +0100 Subject: [PATCH 06/51] Add changelog --- changelog.d/0.feature.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 changelog.d/0.feature.md diff --git a/changelog.d/0.feature.md b/changelog.d/0.feature.md new file mode 100644 index 0000000000..e03e7caae0 --- /dev/null +++ b/changelog.d/0.feature.md @@ -0,0 +1,9 @@ +Add `--requirement` option to `inject` command. + +This reads the list of packages to inject from a text file, +can be used multiple times, +and can be used in parallel with command line packages. + +Note that this is not a full pip-requirements file. +If you require full pip features, then use the `runpip` command instead; +although the installed packages won't be recognised as "injected". From fe898e5524163b189a2483f2632fbc0484fd5f9b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 8 May 2024 09:55:08 +0100 Subject: [PATCH 07/51] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/pipx/commands/inject.py | 2 +- src/pipx/main.py | 2 +- tests/test_inject.py | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/pipx/commands/inject.py b/src/pipx/commands/inject.py index cfd4b960d2..788b2521f8 100644 --- a/src/pipx/commands/inject.py +++ b/src/pipx/commands/inject.py @@ -151,7 +151,7 @@ def inject( def parse_requirements(filename: os.PathLike) -> Generator[str, None, None]: """ Extracts package specifications from requirements file. - + Just returns all of the non-empty lines with comments removed. """ # Based on https://github.com/pypa/pip/blob/main/src/pip/_internal/req/req_file.py diff --git a/src/pipx/main.py b/src/pipx/main.py index 2827787f4d..1d83d50bdb 100644 --- a/src/pipx/main.py +++ b/src/pipx/main.py @@ -526,7 +526,7 @@ def _add_inject(subparsers, venv_completer: VenvCompleter, shared_parser: argpar action="append", metavar="file", help="file containing the packages to inject into the Virtual Environment--" - "one package name or pip package spec per line." + "one package name or pip package spec per line.", ) p.add_argument( "--include-apps", diff --git a/tests/test_inject.py b/tests/test_inject.py index eddf81f2c6..8c7a6ba56f 100644 --- a/tests/test_inject.py +++ b/tests/test_inject.py @@ -56,6 +56,7 @@ def test_inject_include_apps(pipx_temp_env, capsys, with_suffix): assert not run_pipx_cli(["inject", f"pycowsay{suffix}", PKG["black"]["spec"], "--include-deps"]) + def test_inject_with_req_file(pipx_temp_env, capsys, tmp_path): req_file = tmp_path / "requirements.txt" req_file.write_text( @@ -69,6 +70,4 @@ def test_inject_with_req_file(pipx_temp_env, capsys, tmp_path): ) assert not run_pipx_cli(["install", "pycowsay"]) assert not run_pipx_cli(["inject", "pycowsay", "--requirement", str(req_file)]) - assert not run_pipx_cli( - ["inject", "pycowsay", PKG["black"]["spec"], "--requirement", str(req_file)] - ) + assert not run_pipx_cli(["inject", "pycowsay", PKG["black"]["spec"], "--requirement", str(req_file)]) From 2f525a038a726f72bc6f0bc0e169c716660d68f7 Mon Sep 17 00:00:00 2001 From: James Myatt Date: Wed, 8 May 2024 09:55:08 +0100 Subject: [PATCH 08/51] Rename changelog file --- changelog.d/{0.feature.md => 1252.feature.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/{0.feature.md => 1252.feature.md} (100%) diff --git a/changelog.d/0.feature.md b/changelog.d/1252.feature.md similarity index 100% rename from changelog.d/0.feature.md rename to changelog.d/1252.feature.md From a6aee67ffadd2a8302d2d9f263028111310934dd Mon Sep 17 00:00:00 2001 From: James Myatt Date: Wed, 8 May 2024 09:55:08 +0100 Subject: [PATCH 09/51] Fix pylint and mypy errors --- src/pipx/commands/inject.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/pipx/commands/inject.py b/src/pipx/commands/inject.py index 788b2521f8..d8bd902b62 100644 --- a/src/pipx/commands/inject.py +++ b/src/pipx/commands/inject.py @@ -3,7 +3,7 @@ import re import sys from pathlib import Path -from typing import Generator, List, Optional +from typing import Generator, List, Optional, Union from pipx import paths from pipx.colors import bold @@ -148,7 +148,7 @@ def inject( return EXIT_CODE_OK if all_success else EXIT_CODE_INJECT_ERROR -def parse_requirements(filename: os.PathLike) -> Generator[str, None, None]: +def parse_requirements(filename: Union[str, os.PathLike]) -> Generator[str, None, None]: """ Extracts package specifications from requirements file. @@ -158,7 +158,6 @@ def parse_requirements(filename: os.PathLike) -> Generator[str, None, None]: with open(filename) as f: for line in f: # Strip comments and filter empty lines - line = COMMENT_RE.sub("", line) - line = line.strip() - if line: - yield line + pkgspec = COMMENT_RE.sub("", line).strip() + if pkgspec: + yield pkgspec From 0d6f4e2dad90d922ce0ccf95abb22b634c60b9b7 Mon Sep 17 00:00:00 2001 From: James Myatt Date: Wed, 8 May 2024 09:55:08 +0100 Subject: [PATCH 10/51] Fix default for requirements --- src/pipx/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pipx/main.py b/src/pipx/main.py index 1d83d50bdb..4ae4185600 100644 --- a/src/pipx/main.py +++ b/src/pipx/main.py @@ -524,6 +524,7 @@ def _add_inject(subparsers, venv_completer: VenvCompleter, shared_parser: argpar "--requirement", dest="requirements", action="append", + default=[], metavar="file", help="file containing the packages to inject into the Virtual Environment--" "one package name or pip package spec per line.", From ccb787c987b7cf440e18436dd027de6b1ae914d0 Mon Sep 17 00:00:00 2001 From: James Myatt Date: Wed, 8 May 2024 09:55:08 +0100 Subject: [PATCH 11/51] Use assignment operator since Python >= 3.8 --- src/pipx/commands/inject.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pipx/commands/inject.py b/src/pipx/commands/inject.py index d8bd902b62..33b0685175 100644 --- a/src/pipx/commands/inject.py +++ b/src/pipx/commands/inject.py @@ -158,6 +158,5 @@ def parse_requirements(filename: Union[str, os.PathLike]) -> Generator[str, None with open(filename) as f: for line in f: # Strip comments and filter empty lines - pkgspec = COMMENT_RE.sub("", line).strip() - if pkgspec: + if pkgspec := COMMENT_RE.sub("", line).strip(): yield pkgspec From 305d2effe0beff6f6cbb306d5385bb49c0e06049 Mon Sep 17 00:00:00 2001 From: James Myatt Date: Wed, 8 May 2024 09:55:09 +0100 Subject: [PATCH 12/51] Update src/pipx/commands/inject.py Co-authored-by: chrysle --- src/pipx/commands/inject.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pipx/commands/inject.py b/src/pipx/commands/inject.py index 33b0685175..af173740b9 100644 --- a/src/pipx/commands/inject.py +++ b/src/pipx/commands/inject.py @@ -150,9 +150,9 @@ def inject( def parse_requirements(filename: Union[str, os.PathLike]) -> Generator[str, None, None]: """ - Extracts package specifications from requirements file. + Extract package specifications from requirements file. - Just returns all of the non-empty lines with comments removed. + Return all of the non-empty lines with comments removed. """ # Based on https://github.com/pypa/pip/blob/main/src/pip/_internal/req/req_file.py with open(filename) as f: From d331d2a87a365446f63c3d772357e0c30552cdbd Mon Sep 17 00:00:00 2001 From: James Myatt Date: Wed, 8 May 2024 09:55:09 +0100 Subject: [PATCH 13/51] Update src/pipx/main.py Co-authored-by: chrysle --- src/pipx/main.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pipx/main.py b/src/pipx/main.py index 4ae4185600..4d9e9cc0fb 100644 --- a/src/pipx/main.py +++ b/src/pipx/main.py @@ -526,8 +526,11 @@ def _add_inject(subparsers, venv_completer: VenvCompleter, shared_parser: argpar action="append", default=[], metavar="file", - help="file containing the packages to inject into the Virtual Environment--" - "one package name or pip package spec per line.", + help=( + "file containing the packages to inject into the Virtual Environment--" + "one package name or pip package spec per line. " + "May be specified multiple times." + ), ) p.add_argument( "--include-apps", From 47a1ae5e34501a8333ede285caec6093791a0547 Mon Sep 17 00:00:00 2001 From: James Myatt Date: Wed, 8 May 2024 09:55:09 +0100 Subject: [PATCH 14/51] Update tests/test_inject.py Co-authored-by: chrysle --- tests/test_inject.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_inject.py b/tests/test_inject.py index 8c7a6ba56f..69dca279e8 100644 --- a/tests/test_inject.py +++ b/tests/test_inject.py @@ -58,7 +58,7 @@ def test_inject_include_apps(pipx_temp_env, capsys, with_suffix): def test_inject_with_req_file(pipx_temp_env, capsys, tmp_path): - req_file = tmp_path / "requirements.txt" + req_file = tmp_path / "inject-requirements.txt" req_file.write_text( textwrap.dedent( f""" From c1e1688b8e46a5baee97755c287c2c58171c772d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 8 May 2024 09:55:09 +0100 Subject: [PATCH 15/51] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/pipx/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pipx/main.py b/src/pipx/main.py index 4d9e9cc0fb..ad4d71eb10 100644 --- a/src/pipx/main.py +++ b/src/pipx/main.py @@ -530,7 +530,7 @@ def _add_inject(subparsers, venv_completer: VenvCompleter, shared_parser: argpar "file containing the packages to inject into the Virtual Environment--" "one package name or pip package spec per line. " "May be specified multiple times." - ), + ), ) p.add_argument( "--include-apps", From 1a268391abc8b85405f46259ab863d6a145d6a6a Mon Sep 17 00:00:00 2001 From: James Myatt Date: Wed, 8 May 2024 09:55:09 +0100 Subject: [PATCH 16/51] Update changelog.d/1252.feature.md Co-authored-by: chrysle --- changelog.d/1252.feature.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/changelog.d/1252.feature.md b/changelog.d/1252.feature.md index e03e7caae0..db8d92df9f 100644 --- a/changelog.d/1252.feature.md +++ b/changelog.d/1252.feature.md @@ -2,7 +2,9 @@ Add `--requirement` option to `inject` command. This reads the list of packages to inject from a text file, can be used multiple times, -and can be used in parallel with command line packages. +and can be used in parallel with dependencies specified +through the command line. + Note that this is not a full pip-requirements file. If you require full pip features, then use the `runpip` command instead; From d6b3c281bfd51bb3fab8b00f792fb16be3ff98a6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 8 May 2024 09:55:09 +0100 Subject: [PATCH 17/51] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- changelog.d/1252.feature.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/1252.feature.md b/changelog.d/1252.feature.md index db8d92df9f..c1a3d76daa 100644 --- a/changelog.d/1252.feature.md +++ b/changelog.d/1252.feature.md @@ -2,7 +2,7 @@ Add `--requirement` option to `inject` command. This reads the list of packages to inject from a text file, can be used multiple times, -and can be used in parallel with dependencies specified +and can be used in parallel with dependencies specified through the command line. From 4ac9fff0fd8e3f0a4696fa39b59b8709f37bb9bb Mon Sep 17 00:00:00 2001 From: James Myatt Date: Wed, 8 May 2024 09:55:09 +0100 Subject: [PATCH 18/51] Update tests/test_inject.py Add comments to test file Co-authored-by: chrysle --- tests/test_inject.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_inject.py b/tests/test_inject.py index 69dca279e8..59018a546f 100644 --- a/tests/test_inject.py +++ b/tests/test_inject.py @@ -62,9 +62,10 @@ def test_inject_with_req_file(pipx_temp_env, capsys, tmp_path): req_file.write_text( textwrap.dedent( f""" - {PKG["black"]["spec"]} + {PKG["black"]["spec"]} # a comment inline {PKG["nox"]["spec"]} {PKG["pylint"]["spec"]} + # comment on separate line """ ).strip() ) From 5bf3566a14ac413a5e5f41623a474d35e44f6929 Mon Sep 17 00:00:00 2001 From: James Myatt Date: Wed, 8 May 2024 09:55:09 +0100 Subject: [PATCH 19/51] Update test_inject.py Add a blank line in inject-requirements file --- tests/test_inject.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_inject.py b/tests/test_inject.py index 59018a546f..2a698a9430 100644 --- a/tests/test_inject.py +++ b/tests/test_inject.py @@ -64,6 +64,7 @@ def test_inject_with_req_file(pipx_temp_env, capsys, tmp_path): f""" {PKG["black"]["spec"]} # a comment inline {PKG["nox"]["spec"]} + {PKG["pylint"]["spec"]} # comment on separate line """ From f0144ab581d475e5046b771c901b27cce7f47103 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 8 May 2024 09:55:10 +0100 Subject: [PATCH 20/51] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_inject.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_inject.py b/tests/test_inject.py index 2a698a9430..a4187e2589 100644 --- a/tests/test_inject.py +++ b/tests/test_inject.py @@ -64,7 +64,7 @@ def test_inject_with_req_file(pipx_temp_env, capsys, tmp_path): f""" {PKG["black"]["spec"]} # a comment inline {PKG["nox"]["spec"]} - + {PKG["pylint"]["spec"]} # comment on separate line """ From 04b82e54f8b3f90400f04384893256b1c88d51ea Mon Sep 17 00:00:00 2001 From: James Myatt Date: Wed, 8 May 2024 09:55:10 +0100 Subject: [PATCH 21/51] Logging at INFO level Also move after exception --- src/pipx/commands/inject.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pipx/commands/inject.py b/src/pipx/commands/inject.py index af173740b9..2266dafcb1 100644 --- a/src/pipx/commands/inject.py +++ b/src/pipx/commands/inject.py @@ -123,9 +123,10 @@ def inject( packages = list(package_specs) for filename in requirement_files: packages.extend(parse_requirements(filename)) - logger.debug("Injecting packages: %r", sorted(packages)) + if not packages: raise PipxError("No packages have been specified.") + logger.info("Injecting packages: %r", sorted(packages)) # Inject packages if not include_apps and include_dependencies: From e3afb4087df33cc8134bc588e40419215ac4012a Mon Sep 17 00:00:00 2001 From: James Myatt Date: Wed, 8 May 2024 09:55:10 +0100 Subject: [PATCH 22/51] Discard duplicated package specifications --- src/pipx/commands/inject.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pipx/commands/inject.py b/src/pipx/commands/inject.py index 2266dafcb1..e4a4b66eac 100644 --- a/src/pipx/commands/inject.py +++ b/src/pipx/commands/inject.py @@ -119,10 +119,10 @@ def inject( suffix: bool = False, ) -> ExitCode: """Returns pipx exit code.""" - # Combined list of packages - packages = list(package_specs) + # Combined collection of package specifications + packages = set(package_specs) for filename in requirement_files: - packages.extend(parse_requirements(filename)) + packages.update(parse_requirements(filename)) if not packages: raise PipxError("No packages have been specified.") From 5509d0fb1ec2cb7d04cf9e223f52abb954713dbc Mon Sep 17 00:00:00 2001 From: James Myatt Date: Wed, 8 May 2024 09:55:10 +0100 Subject: [PATCH 23/51] Update 1252.feature.md --- changelog.d/1252.feature.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/changelog.d/1252.feature.md b/changelog.d/1252.feature.md index c1a3d76daa..fba88bc27d 100644 --- a/changelog.d/1252.feature.md +++ b/changelog.d/1252.feature.md @@ -5,7 +5,11 @@ can be used multiple times, and can be used in parallel with dependencies specified through the command line. +Each line is a separate package specifier with the same syntax as the command line. +Comments are supported with a `#` prefix. +This is a strict subset of the pip [requirements file format][pip-requirements] syntax. -Note that this is not a full pip-requirements file. -If you require full pip features, then use the `runpip` command instead; -although the installed packages won't be recognised as "injected". +If you require full pip functionality, then use the `runpip` command instead; +however, the installed packages won't be recognised as "injected". + +[pip-requirements]: https://pip.pypa.io/en/stable/reference/requirements-file-format/ From 7358cface0d001ded31ac5fabd89fb42e81277e4 Mon Sep 17 00:00:00 2001 From: James Myatt Date: Wed, 8 May 2024 09:55:10 +0100 Subject: [PATCH 24/51] Update install-all command --- src/pipx/commands/install.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/pipx/commands/install.py b/src/pipx/commands/install.py index 99c1a90092..adeff2b2b1 100644 --- a/src/pipx/commands/install.py +++ b/src/pipx/commands/install.py @@ -215,10 +215,11 @@ def install_all( # Install the injected packages for inject_package in venv_metadata.injected_packages.values(): commands.inject( - venv_dir, - None, - [generate_package_spec(inject_package)], - pip_args, + venv_dir=venv_dir, + package_name=None, + package_specs=[generate_package_spec(inject_package)], + requirement_files=[], + pip_args=pip_args, verbose=verbose, include_apps=inject_package.include_apps, include_dependencies=inject_package.include_dependencies, From 9b4b4ddf63f0246eb52b01365e755e7b50d10544 Mon Sep 17 00:00:00 2001 From: James Myatt Date: Wed, 8 May 2024 10:50:30 +0100 Subject: [PATCH 25/51] Expand pipx inject example --- docs/examples.md | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/docs/examples.md b/docs/examples.md index 4a887479a6..ec29d44e21 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -98,13 +98,36 @@ Then you can run it as follows: One use of the inject command is setting up a REPL with some useful extra packages. ``` -pipx install ptpython -pipx inject ptpython requests pendulum +> pipx install ptpython +> pipx inject ptpython requests pendulum ``` After running the above commands, you will be able to import and use the `requests` and `pendulum` packages inside a `ptpython` repl. +Equivalently, the extra packages can be listed in a text file +(e.g. `useful-packages.txt`) +with one on each line, + +``` +# Additional packages +requests + +pendulum # for easier datetimes +``` + +then the packages injected in one go. + +``` +> pipx inject ptpython -r useful-packages.txt +``` + +Note that these options can be repeated and used together, e.g. + +``` +> pipx inject ptpython package-1 -r extra-packages-1.txt -r extra-packages-2.txt package-2 +``` + ## `pipx list` example ``` From 6833592f1b92bc156f0b856881462b274a5b9608 Mon Sep 17 00:00:00 2001 From: James Myatt Date: Wed, 8 May 2024 10:50:48 +0100 Subject: [PATCH 26/51] Clarify changelog entry --- changelog.d/1252.feature.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog.d/1252.feature.md b/changelog.d/1252.feature.md index fba88bc27d..c1fc252ed3 100644 --- a/changelog.d/1252.feature.md +++ b/changelog.d/1252.feature.md @@ -4,10 +4,11 @@ This reads the list of packages to inject from a text file, can be used multiple times, and can be used in parallel with dependencies specified through the command line. +The option can be abbreviated to `-r`. Each line is a separate package specifier with the same syntax as the command line. Comments are supported with a `#` prefix. -This is a strict subset of the pip [requirements file format][pip-requirements] syntax. +Hence, the syntax is a strict subset of the pip [requirements file format][pip-requirements] syntax. If you require full pip functionality, then use the `runpip` command instead; however, the installed packages won't be recognised as "injected". From 0615b08152184d3e8b920bc0d3743b9827c38ced Mon Sep 17 00:00:00 2001 From: James Myatt Date: Wed, 8 May 2024 10:57:30 +0100 Subject: [PATCH 27/51] Mention in main README --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index b7316ff665..e75f428fb3 100644 --- a/README.md +++ b/README.md @@ -246,6 +246,17 @@ If an application installed by pipx requires additional packages, you can add th pipx inject ipython matplotlib ``` +You can inject multiple packages by adding them all to the command line, +or by listing them in a text file, with one package per line, +or a combination. +For example: + +``` +pipx inject ipython matplotlib pandas +# or: +pipx inject ipython -r useful-packages.txt +``` + ### Walkthrough: Running an Application in a Temporary Virtual Environment This is an alternative to `pipx install`. From 9967262d0d62f6a769e3f88115530b4ff95c4daf Mon Sep 17 00:00:00 2001 From: James Myatt Date: Wed, 8 May 2024 12:02:18 +0100 Subject: [PATCH 28/51] Check stdout and logs in test --- tests/test_inject.py | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/tests/test_inject.py b/tests/test_inject.py index a4187e2589..f4d3fe04b5 100644 --- a/tests/test_inject.py +++ b/tests/test_inject.py @@ -1,3 +1,4 @@ +import logging import textwrap import pytest # type: ignore @@ -57,7 +58,16 @@ def test_inject_include_apps(pipx_temp_env, capsys, with_suffix): assert not run_pipx_cli(["inject", f"pycowsay{suffix}", PKG["black"]["spec"], "--include-deps"]) -def test_inject_with_req_file(pipx_temp_env, capsys, tmp_path): +@pytest.mark.parametrize( + "with_packages,", [ + (), # no extra packages + ("black",), # duplicate from requirements file + ("isort",) # additional package + ] +) +def test_inject_with_req_file(pipx_temp_env, capsys, caplog, tmp_path, with_packages): + caplog.set_level(logging.INFO) + req_file = tmp_path / "inject-requirements.txt" req_file.write_text( textwrap.dedent( @@ -71,5 +81,23 @@ def test_inject_with_req_file(pipx_temp_env, capsys, tmp_path): ).strip() ) assert not run_pipx_cli(["install", "pycowsay"]) - assert not run_pipx_cli(["inject", "pycowsay", "--requirement", str(req_file)]) - assert not run_pipx_cli(["inject", "pycowsay", PKG["black"]["spec"], "--requirement", str(req_file)]) + + assert not run_pipx_cli( + ["inject", "pycowsay"] + + [PKG[pkg]["spec"] for pkg in with_packages] + + ["--requirement", str(req_file)] + ) + + packages = [ + ("black", PKG["black"]["spec"]), + ("nox", PKG["nox"]["spec"]), + ("pylint", PKG["pylint"]["spec"]), + ] + packages.extend((pkg, PKG[pkg]["spec"]) for pkg in with_packages) + packages = sorted(set(packages)) + + assert f"Injecting packages: {[p for _, p in packages]!r}" in caplog.text + + captured = capsys.readouterr() + for pkg, _ in packages: + assert f"injected package {pkg} into venv pycowsay" in captured.out From 48a88c795ee6c57d56c54498acc1275fa9913870 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 8 May 2024 11:03:13 +0000 Subject: [PATCH 29/51] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_inject.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/tests/test_inject.py b/tests/test_inject.py index f4d3fe04b5..4ea303ecdf 100644 --- a/tests/test_inject.py +++ b/tests/test_inject.py @@ -59,11 +59,12 @@ def test_inject_include_apps(pipx_temp_env, capsys, with_suffix): @pytest.mark.parametrize( - "with_packages,", [ - (), # no extra packages - ("black",), # duplicate from requirements file - ("isort",) # additional package - ] + "with_packages,", + [ + (), # no extra packages + ("black",), # duplicate from requirements file + ("isort",), # additional package + ], ) def test_inject_with_req_file(pipx_temp_env, capsys, caplog, tmp_path, with_packages): caplog.set_level(logging.INFO) @@ -83,9 +84,7 @@ def test_inject_with_req_file(pipx_temp_env, capsys, caplog, tmp_path, with_pack assert not run_pipx_cli(["install", "pycowsay"]) assert not run_pipx_cli( - ["inject", "pycowsay"] - + [PKG[pkg]["spec"] for pkg in with_packages] - + ["--requirement", str(req_file)] + ["inject", "pycowsay"] + [PKG[pkg]["spec"] for pkg in with_packages] + ["--requirement", str(req_file)] ) packages = [ From e078ed052551648fa1cad621379cb67efd5e29d0 Mon Sep 17 00:00:00 2001 From: James Myatt Date: Wed, 8 May 2024 12:34:55 +0100 Subject: [PATCH 30/51] Better test for injected packages --- tests/test_inject.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_inject.py b/tests/test_inject.py index 4ea303ecdf..cb57e6653c 100644 --- a/tests/test_inject.py +++ b/tests/test_inject.py @@ -1,4 +1,5 @@ import logging +import re import textwrap import pytest # type: ignore @@ -84,7 +85,7 @@ def test_inject_with_req_file(pipx_temp_env, capsys, caplog, tmp_path, with_pack assert not run_pipx_cli(["install", "pycowsay"]) assert not run_pipx_cli( - ["inject", "pycowsay"] + [PKG[pkg]["spec"] for pkg in with_packages] + ["--requirement", str(req_file)] + ["inject", "pycowsay", *(PKG[pkg]["spec"] for pkg in with_packages), "--requirement", str(req_file)] ) packages = [ @@ -98,5 +99,5 @@ def test_inject_with_req_file(pipx_temp_env, capsys, caplog, tmp_path, with_pack assert f"Injecting packages: {[p for _, p in packages]!r}" in caplog.text captured = capsys.readouterr() - for pkg, _ in packages: - assert f"injected package {pkg} into venv pycowsay" in captured.out + injected = re.findall(r"injected package (.+?) into venv pycowsay", captured.out) + assert set(injected) == {pkg for pkg, _ in packages} From 776bcac29d7026f48a1cb1e898bf1bcb386324b0 Mon Sep 17 00:00:00 2001 From: James Myatt Date: Wed, 8 May 2024 14:57:22 +0100 Subject: [PATCH 31/51] Clarify ignoring of comments in example Co-authored-by: chrysle <96722107+chrysle@users.noreply.github.com> --- docs/examples.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/examples.md b/docs/examples.md index ec29d44e21..4acd420bd6 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -105,9 +105,7 @@ One use of the inject command is setting up a REPL with some useful extra packag After running the above commands, you will be able to import and use the `requests` and `pendulum` packages inside a `ptpython` repl. -Equivalently, the extra packages can be listed in a text file -(e.g. `useful-packages.txt`) -with one on each line, +Equivalently, the extra packages can be listed in a text file (e.g. `useful-packages.txt`) with one on each line (comments ignored): ``` # Additional packages From e2f6d3a4f07b52a69fa6dab352402627be67cb16 Mon Sep 17 00:00:00 2001 From: James Myatt Date: Wed, 8 May 2024 14:57:40 +0100 Subject: [PATCH 32/51] Clarify use of "requirement" file Co-authored-by: chrysle <96722107+chrysle@users.noreply.github.com> --- docs/examples.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples.md b/docs/examples.md index 4acd420bd6..cfec78d790 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -114,7 +114,7 @@ requests pendulum # for easier datetimes ``` -then the packages injected in one go. +This file can then be given to `pipx inject` on the command line: ``` > pipx inject ptpython -r useful-packages.txt From 9792a21b401974d4829358a41c51fa1b29373907 Mon Sep 17 00:00:00 2001 From: James Myatt Date: Wed, 8 May 2024 14:57:46 +0100 Subject: [PATCH 33/51] Update README.md Co-authored-by: chrysle <96722107+chrysle@users.noreply.github.com> --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index e75f428fb3..39a86bc2bb 100644 --- a/README.md +++ b/README.md @@ -248,8 +248,7 @@ pipx inject ipython matplotlib You can inject multiple packages by adding them all to the command line, or by listing them in a text file, with one package per line, -or a combination. -For example: +or a combination. For example: ``` pipx inject ipython matplotlib pandas From 8f015c507662e4450b51b0d8787c9def667a6712 Mon Sep 17 00:00:00 2001 From: James Myatt Date: Wed, 8 May 2024 14:57:55 +0100 Subject: [PATCH 34/51] Update README.md Co-authored-by: chrysle <96722107+chrysle@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 39a86bc2bb..23900338f6 100644 --- a/README.md +++ b/README.md @@ -246,7 +246,7 @@ If an application installed by pipx requires additional packages, you can add th pipx inject ipython matplotlib ``` -You can inject multiple packages by adding them all to the command line, +You can inject multiple packages by specifying them all on the command line, or by listing them in a text file, with one package per line, or a combination. For example: From 8a0acbee4235befca2a8bf0ebd28107948c2d65a Mon Sep 17 00:00:00 2001 From: James Myatt Date: Wed, 8 May 2024 16:07:00 +0100 Subject: [PATCH 35/51] Check can inject each package independently --- tests/test_inject.py | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/tests/test_inject.py b/tests/test_inject.py index cb57e6653c..cd7b00ea7e 100644 --- a/tests/test_inject.py +++ b/tests/test_inject.py @@ -8,9 +8,29 @@ from package_info import PKG -def test_inject_simple(pipx_temp_env, capsys): +@pytest.mark.parametrize( + "pkg_spec,", + [ + PKG["black"]["spec"], # was test_inject_simple + "jaraco.clipboard==2.0.1", # was test_inject_tricky_character + "pylint==3.0.4", # was test_spec + PKG["nox"]["spec"], # used in test_inject_with_req_file + PKG["pylint"]["spec"], # used in test_inject_with_req_file + PKG["isort"]["spec"], # used in test_inject_with_req_file + ], +) +def test_inject_single_package(pipx_temp_env, capsys, caplog, pkg_spec): assert not run_pipx_cli(["install", "pycowsay"]) - assert not run_pipx_cli(["inject", "pycowsay", PKG["black"]["spec"]]) + assert not run_pipx_cli(["inject", "pycowsay", pkg_spec]) + + # Check arguments have been parsed correctly + assert f"Injecting packages: {[pkg_spec]!r}" in caplog.text + + # Check it's actually being installed and into correct venv + captured = capsys.readouterr() + injected = re.findall(r"injected package (.+?) into venv pycowsay", captured.out) + pkg_name, *_ = pkg_spec.split("=", 1) # assuming spec is always of the form == + assert set(injected) == {pkg_name} @skip_if_windows @@ -31,16 +51,6 @@ def test_inject_simple_legacy_venv(pipx_temp_env, capsys, metadata_version): assert "Please uninstall and install" in capsys.readouterr().err -def test_inject_tricky_character(pipx_temp_env, capsys): - assert not run_pipx_cli(["install", "pycowsay"]) - assert not run_pipx_cli(["inject", "pycowsay", "jaraco.clipboard==2.0.1"]) - - -def test_spec(pipx_temp_env, capsys): - assert not run_pipx_cli(["install", "pycowsay"]) - assert not run_pipx_cli(["inject", "pycowsay", "pylint==3.0.4"]) - - @pytest.mark.parametrize("with_suffix,", [(False,), (True,)]) def test_inject_include_apps(pipx_temp_env, capsys, with_suffix): install_args = [] @@ -96,8 +106,10 @@ def test_inject_with_req_file(pipx_temp_env, capsys, caplog, tmp_path, with_pack packages.extend((pkg, PKG[pkg]["spec"]) for pkg in with_packages) packages = sorted(set(packages)) + # Check arguments and files have been parsed correctly assert f"Injecting packages: {[p for _, p in packages]!r}" in caplog.text + # Check they're actually being installed and into correct venv captured = capsys.readouterr() injected = re.findall(r"injected package (.+?) into venv pycowsay", captured.out) assert set(injected) == {pkg for pkg, _ in packages} From 90394e40c6cf30a7a1c49febee737ef8073ef50d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 8 May 2024 15:07:13 +0000 Subject: [PATCH 36/51] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_inject.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_inject.py b/tests/test_inject.py index cd7b00ea7e..39f6dba7d2 100644 --- a/tests/test_inject.py +++ b/tests/test_inject.py @@ -13,7 +13,7 @@ [ PKG["black"]["spec"], # was test_inject_simple "jaraco.clipboard==2.0.1", # was test_inject_tricky_character - "pylint==3.0.4", # was test_spec + "pylint==3.0.4", # was test_spec PKG["nox"]["spec"], # used in test_inject_with_req_file PKG["pylint"]["spec"], # used in test_inject_with_req_file PKG["isort"]["spec"], # used in test_inject_with_req_file From 9d4a7e3204bd10d6ff2715ffc0e09dd6de007d1d Mon Sep 17 00:00:00 2001 From: James Myatt Date: Wed, 8 May 2024 16:22:36 +0100 Subject: [PATCH 37/51] Fix handling of tricky characters --- tests/test_inject.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_inject.py b/tests/test_inject.py index 39f6dba7d2..f5a5cd6974 100644 --- a/tests/test_inject.py +++ b/tests/test_inject.py @@ -29,7 +29,7 @@ def test_inject_single_package(pipx_temp_env, capsys, caplog, pkg_spec): # Check it's actually being installed and into correct venv captured = capsys.readouterr() injected = re.findall(r"injected package (.+?) into venv pycowsay", captured.out) - pkg_name, *_ = pkg_spec.split("=", 1) # assuming spec is always of the form == + pkg_name = pkg_spec.split("=", 1)[0].replace(".", "-") # assuming spec is always of the form == assert set(injected) == {pkg_name} From 3a58c79afcb9abc9ee5903b2aee15981dfaca2ed Mon Sep 17 00:00:00 2001 From: James Myatt Date: Wed, 8 May 2024 16:35:38 +0100 Subject: [PATCH 38/51] Use logger where possible --- src/pipx/venv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pipx/venv.py b/src/pipx/venv.py index ec6d8c6469..001fd9b459 100644 --- a/src/pipx/venv.py +++ b/src/pipx/venv.py @@ -217,7 +217,7 @@ def uninstall_package(self, package: str, was_injected: bool = False): cmd = ["uninstall", "-y"] + [package] self._run_pip(cmd) except PipxError as e: - logging.info(e) + logger.info(e) raise PipxError(f"Error uninstalling {package}.") from None if was_injected: From 80f6651304a15c6c59c188c8b737b96a6642577e Mon Sep 17 00:00:00 2001 From: James Myatt Date: Wed, 8 May 2024 16:45:53 +0100 Subject: [PATCH 39/51] More messages in logs --- src/pipx/venv.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/pipx/venv.py b/src/pipx/venv.py index 001fd9b459..9f87de72d1 100644 --- a/src/pipx/venv.py +++ b/src/pipx/venv.py @@ -159,6 +159,7 @@ def create_venv(self, venv_args: List[str], pip_args: List[str], override_shared """ override_shared -- Override installing shared libraries to the pipx shared directory (default False) """ + logger.info("Creating virtual environment") with animate("creating virtual environment", self.do_animation): cmd = [self.python, "-m", "venv"] if not override_shared: @@ -213,6 +214,7 @@ def upgrade_packaging_libraries(self, pip_args: List[str]) -> None: def uninstall_package(self, package: str, was_injected: bool = False): try: + logger.info("Uninstalling %s", package) with animate(f"uninstalling {package}", self.do_animation): cmd = ["uninstall", "-y"] + [package] self._run_pip(cmd) @@ -240,10 +242,8 @@ def install_package( # check syntax and clean up spec and pip_args (package_or_url, pip_args) = parse_specifier_for_install(package_or_url, pip_args) - with animate( - f"installing {full_package_description(package_name, package_or_url)}", - self.do_animation, - ): + logger.info("Installing %s", package_descr := full_package_description(package_name, package_or_url)) + with animate(f"installing {package_descr}", self.do_animation): # do not use -q with `pip install` so subprocess_post_check_pip_errors # has more information to analyze in case of failure. cmd = [ @@ -287,7 +287,8 @@ def install_unmanaged_packages(self, requirements: List[str], pip_args: List[str # Note: We want to install everything at once, as that lets # pip resolve conflicts correctly. - with animate(f"installing {', '.join(requirements)}", self.do_animation): + logger.info("Installing %s", package_descr := ", ".join(requirements)) + with animate(f"installing {package_descr}", self.do_animation): # do not use -q with `pip install` so subprocess_post_check_pip_errors # has more information to analyze in case of failure. cmd = [ @@ -428,10 +429,8 @@ def has_package(self, package_name: str) -> bool: return bool(list(Distribution.discover(name=package_name, path=[str(get_site_packages(self.python_path))]))) def upgrade_package_no_metadata(self, package_name: str, pip_args: List[str]) -> None: - with animate( - f"upgrading {full_package_description(package_name, package_name)}", - self.do_animation, - ): + logger.info("Upgrading %s", package_descr := full_package_description(package_name, package_name)) + with animate(f"upgrading {package_descr}", self.do_animation): pip_process = self._run_pip(["--no-input", "install"] + pip_args + ["--upgrade", package_name]) subprocess_post_check(pip_process) @@ -445,10 +444,8 @@ def upgrade_package( is_main_package: bool, suffix: str = "", ) -> None: - with animate( - f"upgrading {full_package_description(package_name, package_or_url)}", - self.do_animation, - ): + logger.info("Upgrading %s", package_descr := full_package_description(package_name, package_or_url)) + with animate(f"upgrading {package_descr}", self.do_animation): pip_process = self._run_pip(["--no-input", "install"] + pip_args + ["--upgrade", package_or_url]) subprocess_post_check(pip_process) From 1aa0b0b6b68fffc34ab13aa7449201ca6e4e3aa3 Mon Sep 17 00:00:00 2001 From: James Myatt Date: Thu, 9 May 2024 09:58:16 +0100 Subject: [PATCH 40/51] More debugging messages --- src/pipx/commands/inject.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/pipx/commands/inject.py b/src/pipx/commands/inject.py index e4a4b66eac..3ac757642e 100644 --- a/src/pipx/commands/inject.py +++ b/src/pipx/commands/inject.py @@ -30,6 +30,8 @@ def inject_dep( force: bool, suffix: bool = False, ) -> bool: + logger.debug("Injecting package %s", package_spec) + if not venv_dir.exists() or not next(venv_dir.iterdir()): raise PipxError( f""" @@ -63,6 +65,7 @@ def inject_dep( ) if not force and venv.has_package(package_name): + logger.info("Package %s has already been injected", package_name) print( pipx_wrap( f""" @@ -135,9 +138,9 @@ def inject( for dep in packages: all_success &= inject_dep( venv_dir, - None, - dep, - pip_args, + package_name=None, + package_spec=dep, + pip_args=pip_args, verbose=verbose, include_apps=include_apps, include_dependencies=include_dependencies, From 2c6201d1cb7c5386e6c7e87de66af73d5bf0e54b Mon Sep 17 00:00:00 2001 From: James Myatt Date: Thu, 9 May 2024 10:15:24 +0100 Subject: [PATCH 41/51] Inject additional package that isn't already installed --- tests/test_inject.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_inject.py b/tests/test_inject.py index f5a5cd6974..6355f4931c 100644 --- a/tests/test_inject.py +++ b/tests/test_inject.py @@ -16,7 +16,7 @@ "pylint==3.0.4", # was test_spec PKG["nox"]["spec"], # used in test_inject_with_req_file PKG["pylint"]["spec"], # used in test_inject_with_req_file - PKG["isort"]["spec"], # used in test_inject_with_req_file + PKG["ipython"]["spec"], # used in test_inject_with_req_file ], ) def test_inject_single_package(pipx_temp_env, capsys, caplog, pkg_spec): @@ -74,7 +74,7 @@ def test_inject_include_apps(pipx_temp_env, capsys, with_suffix): [ (), # no extra packages ("black",), # duplicate from requirements file - ("isort",), # additional package + ("ipython",), # additional package ], ) def test_inject_with_req_file(pipx_temp_env, capsys, caplog, tmp_path, with_packages): From 959323ae3fd3bd7ae8105926ad1ddadf89a2483a Mon Sep 17 00:00:00 2001 From: James Myatt Date: Thu, 9 May 2024 10:35:30 +0100 Subject: [PATCH 42/51] Make inject order deterministic --- src/pipx/commands/inject.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pipx/commands/inject.py b/src/pipx/commands/inject.py index 3ac757642e..41badc4c3f 100644 --- a/src/pipx/commands/inject.py +++ b/src/pipx/commands/inject.py @@ -126,10 +126,11 @@ def inject( packages = set(package_specs) for filename in requirement_files: packages.update(parse_requirements(filename)) + packages = sorted(packages) # make order deterministic if not packages: raise PipxError("No packages have been specified.") - logger.info("Injecting packages: %r", sorted(packages)) + logger.info("Injecting packages: %r", packages) # Inject packages if not include_apps and include_dependencies: From 3762d657dc2e3cf68aa785385f9ec52844b324e2 Mon Sep 17 00:00:00 2001 From: James Myatt Date: Thu, 9 May 2024 15:32:18 +0100 Subject: [PATCH 43/51] Fix mypy error --- src/pipx/commands/inject.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/pipx/commands/inject.py b/src/pipx/commands/inject.py index 41badc4c3f..b315b59c57 100644 --- a/src/pipx/commands/inject.py +++ b/src/pipx/commands/inject.py @@ -3,7 +3,7 @@ import re import sys from pathlib import Path -from typing import Generator, List, Optional, Union +from typing import Generator, Iterable, List, Optional, Union from pipx import paths from pipx.colors import bold @@ -111,8 +111,8 @@ def inject_dep( def inject( venv_dir: Path, package_name: Optional[str], - package_specs: List[str], - requirement_files: List[str], + package_specs: Iterable[str], + requirement_files: Iterable[str], pip_args: List[str], *, verbose: bool, @@ -123,10 +123,12 @@ def inject( ) -> ExitCode: """Returns pipx exit code.""" # Combined collection of package specifications - packages = set(package_specs) + packages = list(package_specs) for filename in requirement_files: - packages.update(parse_requirements(filename)) - packages = sorted(packages) # make order deterministic + packages.extend(parse_requirements(filename)) + + # Remove duplicates and order deterministically + packages = sorted(set(packages)) if not packages: raise PipxError("No packages have been specified.") From 6d62bcc4926a671d2be8fa837098f1a689462906 Mon Sep 17 00:00:00 2001 From: James Myatt Date: Fri, 10 May 2024 09:31:54 +0100 Subject: [PATCH 44/51] tidy test_inject_single_package cases --- tests/test_inject.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_inject.py b/tests/test_inject.py index 6355f4931c..5b3a4962cb 100644 --- a/tests/test_inject.py +++ b/tests/test_inject.py @@ -11,12 +11,11 @@ @pytest.mark.parametrize( "pkg_spec,", [ - PKG["black"]["spec"], # was test_inject_simple - "jaraco.clipboard==2.0.1", # was test_inject_tricky_character - "pylint==3.0.4", # was test_spec + PKG["black"]["spec"], # used in test_inject_with_req_file PKG["nox"]["spec"], # used in test_inject_with_req_file PKG["pylint"]["spec"], # used in test_inject_with_req_file PKG["ipython"]["spec"], # used in test_inject_with_req_file + "jaraco.clipboard==2.0.1", # tricky character ], ) def test_inject_single_package(pipx_temp_env, capsys, caplog, pkg_spec): From f8b97194e4ca9e81b6d1136a47de81a1ef0161ee Mon Sep 17 00:00:00 2001 From: James Myatt Date: Fri, 10 May 2024 09:35:40 +0100 Subject: [PATCH 45/51] Better comments on tests --- tests/test_inject.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/test_inject.py b/tests/test_inject.py index 5b3a4962cb..cebacd2665 100644 --- a/tests/test_inject.py +++ b/tests/test_inject.py @@ -8,13 +8,14 @@ from package_info import PKG +# Note that this also checks that packages used in other tests can be injected individually @pytest.mark.parametrize( "pkg_spec,", [ - PKG["black"]["spec"], # used in test_inject_with_req_file - PKG["nox"]["spec"], # used in test_inject_with_req_file - PKG["pylint"]["spec"], # used in test_inject_with_req_file - PKG["ipython"]["spec"], # used in test_inject_with_req_file + PKG["black"]["spec"], + PKG["nox"]["spec"], + PKG["pylint"]["spec"], + PKG["ipython"]["spec"], "jaraco.clipboard==2.0.1", # tricky character ], ) From c36056edab489af8dc39cc25533020510b4a74ce Mon Sep 17 00:00:00 2001 From: James Myatt Date: Tue, 14 May 2024 09:57:50 +0100 Subject: [PATCH 46/51] Update 1252.feature.md Simplify news fragment --- changelog.d/1252.feature.md | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/changelog.d/1252.feature.md b/changelog.d/1252.feature.md index c1fc252ed3..e9d0cd8b5b 100644 --- a/changelog.d/1252.feature.md +++ b/changelog.d/1252.feature.md @@ -1,16 +1,6 @@ -Add `--requirement` option to `inject` command. +Add `--requirement` option to `inject` command to read list of packages from a text file. -This reads the list of packages to inject from a text file, -can be used multiple times, -and can be used in parallel with dependencies specified -through the command line. -The option can be abbreviated to `-r`. - -Each line is a separate package specifier with the same syntax as the command line. -Comments are supported with a `#` prefix. -Hence, the syntax is a strict subset of the pip [requirements file format][pip-requirements] syntax. - -If you require full pip functionality, then use the `runpip` command instead; -however, the installed packages won't be recognised as "injected". +Hence, The syntax is a strict subset of the pip [requirements file format][pip-requirements] syntax: +one package specifier per line, comments with `#` prefix. [pip-requirements]: https://pip.pypa.io/en/stable/reference/requirements-file-format/ From fbee0e2dbc9a0af82bc0d2e0f16d32bd5bb659a5 Mon Sep 17 00:00:00 2001 From: James Myatt Date: Tue, 14 May 2024 09:58:06 +0100 Subject: [PATCH 47/51] Update 1252.feature.md Fix new fragement --- changelog.d/1252.feature.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/1252.feature.md b/changelog.d/1252.feature.md index e9d0cd8b5b..f46e558283 100644 --- a/changelog.d/1252.feature.md +++ b/changelog.d/1252.feature.md @@ -1,6 +1,6 @@ Add `--requirement` option to `inject` command to read list of packages from a text file. -Hence, The syntax is a strict subset of the pip [requirements file format][pip-requirements] syntax: +The syntax is a strict subset of the pip [requirements file format][pip-requirements] syntax: one package specifier per line, comments with `#` prefix. [pip-requirements]: https://pip.pypa.io/en/stable/reference/requirements-file-format/ From 49832dcbfaa0edbc5c5bd7252ee9334b5b664ebc Mon Sep 17 00:00:00 2001 From: James Myatt Date: Tue, 14 May 2024 10:02:07 +0100 Subject: [PATCH 48/51] Update examples.md Be more explicit about the syntax for the "inject -r" files. --- docs/examples.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/examples.md b/docs/examples.md index cfec78d790..a4f78111d5 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -105,7 +105,12 @@ One use of the inject command is setting up a REPL with some useful extra packag After running the above commands, you will be able to import and use the `requests` and `pendulum` packages inside a `ptpython` repl. -Equivalently, the extra packages can be listed in a text file (e.g. `useful-packages.txt`) with one on each line (comments ignored): +Equivalently, the extra packages can be listed in a text file (e.g. `useful-packages.txt`). +Each line is a separate package specifier with the same syntax as the command line. +Comments are supported with a `#` prefix. +Hence, the syntax is a strict subset of the pip [requirements file format][pip-requirements] syntax. + +[pip-requirements]: https://pip.pypa.io/en/stable/reference/requirements-file-format/ ``` # Additional packages @@ -117,6 +122,8 @@ pendulum # for easier datetimes This file can then be given to `pipx inject` on the command line: ``` +> pipx inject ptpython --requirement useful-packages.txt +or: > pipx inject ptpython -r useful-packages.txt ``` @@ -126,6 +133,9 @@ Note that these options can be repeated and used together, e.g. > pipx inject ptpython package-1 -r extra-packages-1.txt -r extra-packages-2.txt package-2 ``` +If you require full pip functionality, then use the `runpip` command instead; +however, the installed packages won't be recognised as "injected". + ## `pipx list` example ``` From 41ace416d0dc45b3ade7c1d3c5cc8473dee5171e Mon Sep 17 00:00:00 2001 From: James Myatt Date: Tue, 14 May 2024 10:03:00 +0100 Subject: [PATCH 49/51] Fix examples.md --- docs/examples.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/examples.md b/docs/examples.md index a4f78111d5..98471d8a30 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -121,9 +121,9 @@ pendulum # for easier datetimes This file can then be given to `pipx inject` on the command line: -``` +```shell > pipx inject ptpython --requirement useful-packages.txt -or: +# or: > pipx inject ptpython -r useful-packages.txt ``` From 9dc46f69a0801e6ccf8cc4dbbb5cd7ddfc8e7fa2 Mon Sep 17 00:00:00 2001 From: James Myatt Date: Tue, 14 May 2024 10:09:27 +0100 Subject: [PATCH 50/51] Update 1252.feature.md Fix markdown link --- changelog.d/1252.feature.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/changelog.d/1252.feature.md b/changelog.d/1252.feature.md index f46e558283..639bf93916 100644 --- a/changelog.d/1252.feature.md +++ b/changelog.d/1252.feature.md @@ -1,6 +1,4 @@ Add `--requirement` option to `inject` command to read list of packages from a text file. -The syntax is a strict subset of the pip [requirements file format][pip-requirements] syntax: +The syntax is a strict subset of the pip [requirements file format](https://pip.pypa.io/en/stable/reference/requirements-file-format/) syntax: one package specifier per line, comments with `#` prefix. - -[pip-requirements]: https://pip.pypa.io/en/stable/reference/requirements-file-format/ From 25adc77d48eb1fe151e8fd7b714f3ffc0630d363 Mon Sep 17 00:00:00 2001 From: James Myatt Date: Tue, 14 May 2024 10:12:19 +0100 Subject: [PATCH 51/51] Update 1252.feature.md --- changelog.d/1252.feature.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/changelog.d/1252.feature.md b/changelog.d/1252.feature.md index 639bf93916..41d11b7d0b 100644 --- a/changelog.d/1252.feature.md +++ b/changelog.d/1252.feature.md @@ -1,4 +1 @@ Add `--requirement` option to `inject` command to read list of packages from a text file. - -The syntax is a strict subset of the pip [requirements file format](https://pip.pypa.io/en/stable/reference/requirements-file-format/) syntax: -one package specifier per line, comments with `#` prefix.