From ac647404902c46019c4cf2da46dc225c5404d986 Mon Sep 17 00:00:00 2001 From: Jason Lam Date: Fri, 13 Oct 2023 18:06:49 +0800 Subject: [PATCH 01/21] Return error when directory can't be added to PATH (#1083) * Return error when directory can't be added to PATH * Update message --------- Co-authored-by: meowmeowcat <68463158+meowmeowmeowcat@users.noreply.github.com> --- CHANGELOG.md | 1 + src/pipx/commands/ensure_path.py | 22 +++++++++++++++------- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index da15e33efd..bf10d5052c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - Pass `--no-input` to pip when output is not piped to parent stdout - Fix program name in generated manual page - Print all environment variables in `pipx environment` +- Return an error message when directory can't be added to PATH successfully ## 1.2.0 diff --git a/src/pipx/commands/ensure_path.py b/src/pipx/commands/ensure_path.py index 8151177b51..6e3de6ffe0 100644 --- a/src/pipx/commands/ensure_path.py +++ b/src/pipx/commands/ensure_path.py @@ -61,14 +61,22 @@ def ensure_path(location: Path, *, force: bool) -> Tuple[bool, bool]: in_current_path = userpath.in_current_path(location_str) if force or (not in_current_path and not need_shell_restart): - userpath.append(location_str, "pipx") - print( - pipx_wrap( - f"Success! Added {location_str} to the PATH environment variable.", - subsequent_indent=" " * 4, + path_added = userpath.append(location_str, "pipx") + if not path_added: + print( + pipx_wrap( + f"{hazard} {location_str} is not added to the PATH environment variable successfully. " + f"You may need to add it to PATH manually.", + subsequent_indent=" " * 4, + ) + ) + else: + print( + pipx_wrap( + f"Success! Added {location_str} to the PATH environment variable.", + subsequent_indent=" " * 4, + ) ) - ) - path_added = True need_shell_restart = userpath.need_shell_restart(location_str) elif not in_current_path and need_shell_restart: print( From 1dfd29564d2e8bb68e81d1d4c8235d448a2609d7 Mon Sep 17 00:00:00 2001 From: Alain Anghelidi <38346044+aanghelidi@users.noreply.github.com> Date: Fri, 20 Oct 2023 13:04:49 +0200 Subject: [PATCH 02/21] Relax VenvInspectInformation attribute typing (#1087) Thanks! --- src/pipx/venv_inspect.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/pipx/venv_inspect.py b/src/pipx/venv_inspect.py index 193a09ece6..3b794cb95a 100644 --- a/src/pipx/venv_inspect.py +++ b/src/pipx/venv_inspect.py @@ -2,7 +2,7 @@ import logging import textwrap from pathlib import Path -from typing import Dict, List, NamedTuple, Optional, Set, Tuple +from typing import Collection, Dict, List, NamedTuple, Optional, Set, Tuple from packaging.requirements import Requirement from packaging.utils import canonicalize_name @@ -19,7 +19,7 @@ class VenvInspectInformation(NamedTuple): - distributions: List[metadata.Distribution] + distributions: Collection[metadata.Distribution] env: Dict[str, str] bin_path: Path @@ -34,7 +34,7 @@ class VenvMetadata(NamedTuple): def get_dist( - package: str, distributions: List[metadata.Distribution] + package: str, distributions: Collection[metadata.Distribution] ) -> Optional[metadata.Distribution]: """Find matching distribution in the canonicalized sense.""" for dist in distributions: @@ -237,10 +237,16 @@ def inspect_venv( venv_python_path ) + # Collect the generator created from metadata.distributions() + # (see `itertools.chain.from_iterable`) into a tuple because we + # need to iterate over it multiple times in `_dfs_package_apps`. + + # Tuple is chosen over a list because the program only iterate over + # the distributions and never modify it. + distributions = tuple(metadata.distributions(path=venv_sys_path)) + venv_inspect_info = VenvInspectInformation( - bin_path=venv_bin_path, - env=venv_env, - distributions=list(metadata.distributions(path=venv_sys_path)), + bin_path=venv_bin_path, env=venv_env, distributions=distributions ) root_dist = get_dist(root_req.name, venv_inspect_info.distributions) From ce27a75ce757e165bb76e3b71a8380f3b28375e9 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Mon, 23 Oct 2023 18:16:48 +0800 Subject: [PATCH 03/21] Merge out-of-main 1.2.1 release notes back to main --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf10d5052c..53827878e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,10 @@ - Print all environment variables in `pipx environment` - Return an error message when directory can't be added to PATH successfully +## 1.2.1 + +- Fix compatibility to packaging 23.2+ by removing reliance on packaging's requirement validation logic and detecting a URL-based requirement in pipx. (#1070) + ## 1.2.0 - Add test for pip module in `pipx reinstall` to fix an issue with `pipx reinstall-all` (#935) From a2d9e09322f3581a57af7c48e14759b17d6f2eec Mon Sep 17 00:00:00 2001 From: Carlos Pereira Atencio <4189262+carlosperate@users.noreply.github.com> Date: Sat, 11 Nov 2023 13:20:54 +0000 Subject: [PATCH 04/21] Remove unused statement (an accidental stand-alone string). (#1105) --- scripts/list_test_packages.py | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/list_test_packages.py b/scripts/list_test_packages.py index 0f701b3023..cae97e1322 100644 --- a/scripts/list_test_packages.py +++ b/scripts/list_test_packages.py @@ -146,7 +146,6 @@ def create_test_packages_list( all_packages.append(f"{package_name}=={package_version}") with platform_package_list_path.open("w") as package_list_fh: - "scripts/list_test_packages.py", for package in sorted(all_packages): print(package, file=package_list_fh) From b8efde1207731126313302ddf49b315b165760cf Mon Sep 17 00:00:00 2001 From: johnthagen Date: Tue, 14 Nov 2023 20:09:10 -0500 Subject: [PATCH 05/21] Normalize Windows install instructions for python.org installers (#1106) --- README.md | 6 +++--- docs/installation.md | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 060526ddf5..08f410fe07 100644 --- a/README.md +++ b/README.md @@ -47,8 +47,8 @@ Upgrade pipx with `python3 -m pip install --user --upgrade pipx`. ### On Windows, install via pip (requires pip 19.0 or later) ``` -# If you installed python using the app-store, replace `python` with `python3` in the next line. -python -m pip install --user pipx +# If you installed python using Microsoft Store, replace `py` with `python3` in the next line. +py -m pip install --user pipx ``` It is possible (even most likely) the above finishes with a WARNING looking similar to this: @@ -67,7 +67,7 @@ Enter the following line (even if you did not get the warning): This will add both the above mentioned path and the `%USERPROFILE%\.local\bin` folder to your search path. Restart your terminal session and verify `pipx` does run. -Upgrade pipx with `python3 -m pip install --user --upgrade pipx`. +Upgrade pipx with `py -m pip install --user --upgrade pipx`. ### Via zipapp diff --git a/docs/installation.md b/docs/installation.md index cfd21894fb..2174ecfa4e 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -18,8 +18,8 @@ pipx ensurepath On Windows (requires pip 19.0 or later): ``` -py -3 -m pip install --user pipx -py -3 -m pipx ensurepath +py -m pip install --user pipx +py -m pipx ensurepath ``` Otherwise, install via pip (requires pip 19.0 or later): From aa5e9405c927e9a3a2455b0deab2282f007e342b Mon Sep 17 00:00:00 2001 From: chrysle Date: Wed, 15 Nov 2023 15:57:39 +0100 Subject: [PATCH 06/21] Drop Python 3.7 (#1005) --- .github/workflows/create_tests_package_lists.yml | 2 +- .github/workflows/exhaustive_package_test.yml | 2 +- .github/workflows/tests.yml | 2 +- CHANGELOG.md | 1 + noxfile.py | 2 +- pyproject.toml | 4 +--- src/pipx/__init__.py | 4 ++-- tests/test_package_specifier.py | 4 ++-- 8 files changed, 10 insertions(+), 11 deletions(-) diff --git a/.github/workflows/create_tests_package_lists.yml b/.github/workflows/create_tests_package_lists.yml index 83c09244e6..27a9b52014 100644 --- a/.github/workflows/create_tests_package_lists.yml +++ b/.github/workflows/create_tests_package_lists.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/exhaustive_package_test.yml b/.github/workflows/exhaustive_package_test.yml index 4de52da3a9..09885bb089 100644 --- a/.github/workflows/exhaustive_package_test.yml +++ b/.github/workflows/exhaustive_package_test.yml @@ -10,7 +10,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d522dfb49b..26fc033d08 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -55,7 +55,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11"] include: - os: windows-latest python-version: "3.11" diff --git a/CHANGELOG.md b/CHANGELOG.md index 53827878e8..1288033a40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## dev +- Drop support for Python 3.7 - Make usage message in `pipx run` show `package_or_url`, so extra will be printed out as well - Add `--force-reinstall` to pip arguments when `--force` was passed - Use the py launcher, if available, to select Python version with the `--python` option diff --git a/noxfile.py b/noxfile.py index 55f94e5efb..b94bf658bf 100644 --- a/noxfile.py +++ b/noxfile.py @@ -4,7 +4,7 @@ import nox # type: ignore -PYTHON_ALL_VERSIONS = ["3.7", "3.8", "3.9", "3.10", "3.11"] +PYTHON_ALL_VERSIONS = ["3.8", "3.9", "3.10", "3.11"] PYTHON_DEFAULT_VERSION = "3.11" DOC_DEPENDENCIES = [".", "jinja2", "mkdocs", "mkdocs-material"] MAN_DEPENDENCIES = [".", "argparse-manpage[setuptools]"] diff --git a/pyproject.toml b/pyproject.toml index 599c6d926f..582fd38abc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "pipx" description = "Install and Run Python Applications in Isolated Environments" readme = "README.md" license = "MIT" -requires-python = ">=3.7" +requires-python = ">=3.8" keywords = ["pip", "install", "cli", "workflow", "Virtual Environment"] authors = [{ name = "Chad Smith", email = "chadsmith.software@gmail.com" }] classifiers = [ @@ -15,7 +15,6 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", @@ -24,7 +23,6 @@ classifiers = [ dependencies = [ "argcomplete>=1.9.4", "colorama>=0.4.4; sys_platform == 'win32'", - "importlib-metadata>=3.3.0; python_version < '3.8'", "packaging>=20.0", "platformdirs>=2.1.0", "userpath>=1.6.0,!=1.9.0", diff --git a/src/pipx/__init__.py b/src/pipx/__init__.py index ec98d9cf2c..c4155c40ab 100644 --- a/src/pipx/__init__.py +++ b/src/pipx/__init__.py @@ -1,8 +1,8 @@ import sys -if sys.version_info < (3, 6, 0): +if sys.version_info < (3, 8, 0): sys.exit( - "Python 3.6 or later is required. " + "Python 3.8 or later is required. " "See https://github.com/pypa/pipx " "for installation instructions." ) diff --git a/tests/test_package_specifier.py b/tests/test_package_specifier.py index 458ff7c5da..6351d207c9 100644 --- a/tests/test_package_specifier.py +++ b/tests/test_package_specifier.py @@ -96,7 +96,7 @@ def test_fix_package_name(package_spec_in, package_name, package_spec_out): True, ), ( - 'my-project[cli] @ git+ssh://git@bitbucket.org/my-company/myproject.git ; python_version<"3.7"', + 'my-project[cli] @ git+ssh://git@bitbucket.org/my-company/myproject.git ; python_version<"3.8"', "my-project[cli]@ git+ssh://git@bitbucket.org/my-company/myproject.git", True, ), @@ -165,7 +165,7 @@ def test_parse_specifier_for_metadata( True, ), ( - 'my-project[cli] @ git+ssh://git@bitbucket.org/my-company/myproject.git ; python_version<"3.7"', + 'my-project[cli] @ git+ssh://git@bitbucket.org/my-company/myproject.git ; python_version<"3.8"', "my-project[cli]@ git+ssh://git@bitbucket.org/my-company/myproject.git", True, ), From d83e7c285219695b7b8c18e5f7fd559517d5a1e8 Mon Sep 17 00:00:00 2001 From: chrysle Date: Thu, 30 Nov 2023 20:18:41 +0100 Subject: [PATCH 07/21] Add initial RTD configuration (#1060) Co-authored-by: Sviatoslav Sydorenko <578543+webknjaz@users.noreply.github.com> --- .github/workflows/publish_docs.yml | 32 ------------------------------ .readthedocs.yaml | 11 ++++++++++ noxfile.py | 11 ++-------- 3 files changed, 13 insertions(+), 41 deletions(-) delete mode 100644 .github/workflows/publish_docs.yml create mode 100644 .readthedocs.yaml diff --git a/.github/workflows/publish_docs.yml b/.github/workflows/publish_docs.yml deleted file mode 100644 index 478497052a..0000000000 --- a/.github/workflows/publish_docs.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Publish docs via GitHub Pages -on: - workflow_dispatch: - release: - types: [published] - -env: - default-python: "3.11" - -jobs: - build: - name: Deploy docs - runs-on: ubuntu-latest - steps: - - name: Checkout main - uses: actions/checkout@v4 - - name: Set up Python ${{ env.default-python }} - uses: actions/setup-python@v4 - with: - python-version: ${{ env.default-python }} - - name: Install nox - run: | - python -m pip install --upgrade pip - pip install nox - - name: Build Docs - run: | - nox --error-on-missing-interpreters --non-interactive --session build_docs - - name: Deploy docs - if: ${{ success() }} - uses: mhausenblas/mkdocs-deploy-gh-pages@1.26 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000000..3930655266 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,11 @@ +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.11" + commands: + - pip install nox + - nox --session build_docs -- "${READTHEDOCS_OUTPUT}"/html diff --git a/noxfile.py b/noxfile.py index b94bf658bf..fe9047a8a4 100644 --- a/noxfile.py +++ b/noxfile.py @@ -193,21 +193,14 @@ def publish(session): @nox.session(python=PYTHON_DEFAULT_VERSION) def build_docs(session): + site_dir = session.posargs or ["site/"] session.run("python", "-m", "pip", "install", "--upgrade", "pip") session.install(*DOC_DEPENDENCIES) session.env[ "PIPX__DOC_DEFAULT_PYTHON" ] = "typically the python used to execute pipx" session.run("python", "scripts/generate_docs.py") - session.run("mkdocs", "build", "--strict") - - -@nox.session(python=PYTHON_DEFAULT_VERSION) -def publish_docs(session): - session.run("python", "-m", "pip", "install", "--upgrade", "pip") - session.install(*DOC_DEPENDENCIES) - build_docs(session) - session.run("mkdocs", "gh-deploy", "--strict") + session.run("mkdocs", "build", "--strict", "--site-dir", *site_dir) @nox.session(python=PYTHON_DEFAULT_VERSION) From a129ac93f070246be7e66e05802da2c4ab3b1ad8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Nov 2023 11:18:59 -0800 Subject: [PATCH 08/21] Bump pypa/gh-action-pypi-publish from 1.8.7 to 1.8.11 (#1114) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/publish-testpypi.yml | 2 +- .github/workflows/tests.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish-testpypi.yml b/.github/workflows/publish-testpypi.yml index ba0f3d2e22..ea04d7ed6b 100644 --- a/.github/workflows/publish-testpypi.yml +++ b/.github/workflows/publish-testpypi.yml @@ -32,7 +32,7 @@ jobs: run: | nox --error-on-missing-interpreters --non-interactive --session build - name: Publish to Test PyPi - uses: pypa/gh-action-pypi-publish@v1.8.7 + uses: pypa/gh-action-pypi-publish@v1.8.11 with: user: __token__ password: ${{ secrets.test_pypi_password }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 26fc033d08..6a7b9e10d2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -110,7 +110,7 @@ jobs: run: | nox --error-on-missing-interpreters --non-interactive --session build - name: Publish to PyPi - uses: pypa/gh-action-pypi-publish@v1.8.7 + uses: pypa/gh-action-pypi-publish@v1.8.11 with: user: __token__ password: ${{ secrets.pypi_password }} From 038361ea5a1c05a3aa5657581a1ce902ad6068c1 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Thu, 30 Nov 2023 14:25:06 -0500 Subject: [PATCH 09/21] tests: fix regex to include build numbers (#1099) --- docs/installation.md | 4 ++-- docs/troubleshooting.md | 4 ++-- scripts/list_test_packages.py | 3 ++- testdata/tests_packages/macos21-python3.7.txt | 2 +- testdata/tests_packages/macos21-python3.8.txt | 2 +- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index 2174ecfa4e..e8da2573bc 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -82,8 +82,8 @@ sudo PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install PACKAGE | `~/.local/pipx/venvs` | `platformdirs.user_data_dir()/pipx/venv` | | `~/.local/pipx/.cache` | `platformdirs.user_cache_dir()/pipx` | | `~/.local/pipx/logs` | `platformdirs.user_log_dir()/pipx/log` | - - `user_data_dir()`, `user_cache_dir()` and `user_log_dir()` resolve to appropriate platform-specific user data, cache and log directories. + + `user_data_dir()`, `user_cache_dir()` and `user_log_dir()` resolve to appropriate platform-specific user data, cache and log directories. See the [platformdirs documentation](https://platformdirs.readthedocs.io/en/latest/api.html#platforms) for details. ## Upgrade pipx diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 2e18488316..4246a8e6d9 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -123,11 +123,11 @@ rm -rf test_venv ## Pipx files not in expected locations according to documentation The default PIPX_HOME is `~/.local/pipx`, prior to the adoption of the XDG base -directory specification after version 1.2.0. To maintain compatibility with older +directory specification after version 1.2.0. To maintain compatibility with older versions, pipx will automatically detect the old paths and use them accordingly. For a map of old and new paths, See [Installation](installation.md#installation-options) -To migrate from the old path to the new path, you can remove the `~/.local/pipx` directory and +To migrate from the old path to the new path, you can remove the `~/.local/pipx` directory and reinstall all packages. For example, on Linux systems, you could read out `pipx`'s package information in JSON via `jq` (which you might need to install first): diff --git a/scripts/list_test_packages.py b/scripts/list_test_packages.py index cae97e1322..0f0d9b58b2 100644 --- a/scripts/list_test_packages.py +++ b/scripts/list_test_packages.py @@ -130,7 +130,8 @@ def create_test_packages_list( all_packages = [] for downloaded_filename in downloaded_list: wheel_re = re.search( - r"(.+)\-([^-]+)\-([^-]+)\-([^-]+)\-([^-]+)\.whl$", downloaded_filename + r"([^-]+)\-([^-]+)\-([^-]+)\-([^-]+)\-([^-]+)(-[^-]+)?\.whl$", + downloaded_filename, ) src_re = re.search(r"(.+)\-([^-]+)\.(?:tar.gz|zip)$", downloaded_filename) if wheel_re: diff --git a/testdata/tests_packages/macos21-python3.7.txt b/testdata/tests_packages/macos21-python3.7.txt index 4bc92d6847..7a61081e16 100644 --- a/testdata/tests_packages/macos21-python3.7.txt +++ b/testdata/tests_packages/macos21-python3.7.txt @@ -20,7 +20,7 @@ attrs==22.1.0 awscli==1.18.168 backcall==0.2.0 beautifulsoup4==4.11.1 -black-22.10.0==1fixedarch +black==22.10.0 black==22.8.0 bleach==5.0.1 boto3==1.26.20 diff --git a/testdata/tests_packages/macos21-python3.8.txt b/testdata/tests_packages/macos21-python3.8.txt index 0ffdc9f5c2..3094f29d49 100644 --- a/testdata/tests_packages/macos21-python3.8.txt +++ b/testdata/tests_packages/macos21-python3.8.txt @@ -21,7 +21,7 @@ attrs==22.1.0 awscli==1.18.168 backcall==0.2.0 beautifulsoup4==4.11.1 -black-22.10.0==1fixedarch +black==22.10.0 black==22.8.0 bleach==5.0.1 boto3==1.26.20 From dd441b9ead7779e588f995b448a994cfba759c7f Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Thu, 30 Nov 2023 14:25:28 -0500 Subject: [PATCH 10/21] chore: update pre-commit and modernize Ruff config (#1098) --- .pre-commit-config.yaml | 8 ++++---- noxfile.py | 6 +++--- pyproject.toml | 16 +++++++++++----- scripts/list_test_packages.py | 2 +- scripts/migrate_pipsi_to_pipx.py | 4 ++-- scripts/pipx_release.py | 2 +- scripts/update_package_cache.py | 1 + src/pipx/interpreter.py | 3 ++- src/pipx/util.py | 2 ++ tests/conftest.py | 2 +- tests/test_install.py | 2 +- tests/test_install_all_packages.py | 2 +- tests/test_run.py | 2 ++ 13 files changed, 32 insertions(+), 20 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ca80856c0c..649d6a5f3c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,18 +6,18 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: end-of-file-fixer - id: check-added-large-files - id: trailing-whitespace - id: check-yaml - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.0.254 + rev: v0.1.3 hooks: - id: ruff - repo: https://github.com/psf/black - rev: 23.1.0 + rev: 23.10.1 hooks: - id: black # mypy args: @@ -27,7 +27,7 @@ repos: # cannot use --warn-unused-ignores because it conflicts with # --ignore-missing-imports - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.1.1 + rev: v1.6.1 hooks: - id: mypy args: ['--warn-unused-ignores', '--strict-equality','--no-implicit-optional'] diff --git a/noxfile.py b/noxfile.py index fe9047a8a4..6beb7bce9f 100644 --- a/noxfile.py +++ b/noxfile.py @@ -9,10 +9,10 @@ DOC_DEPENDENCIES = [".", "jinja2", "mkdocs", "mkdocs-material"] MAN_DEPENDENCIES = [".", "argparse-manpage[setuptools]"] LINT_DEPENDENCIES = [ - "black==22.8.0", - "mypy==1.1.1", + "black==23.10.1", + "mypy==1.6.1", "packaging>=20.0", - "ruff==0.0.254", + "ruff==0.1.3", "types-jinja2", ] # Packages whose dependencies need an intact system PATH to compile diff --git a/pyproject.toml b/pyproject.toml index 582fd38abc..4539d31f77 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,6 +48,9 @@ include = ["/src", "/logo.png", "/pipx_demo.gif", "/*.md"] skip-magic-trailing-comma = true [tool.ruff] +line-length = 121 + +[tool.ruff.lint] select = [ "A", "B", @@ -65,21 +68,24 @@ select = [ ignore = [ "B904", ] -line-length = 121 -show-source = true -[tool.ruff.isort] +[tool.ruff.lint.isort] known-first-party = ["helpers", "package_info", "pipx"] -[tool.ruff.mccabe] +[tool.ruff.lint.mccabe] max-complexity = 15 [tool.pytest.ini_options] markers = ["all_packages: test install with maximum number of packages"] +[tool.mypy] +show_error_codes = true + [[tool.mypy.overrides]] module = [ "packaging.*", - "platformdirs" + "platformdirs", + "pycowsay.*", + "jinja2", ] ignore_missing_imports = true diff --git a/scripts/list_test_packages.py b/scripts/list_test_packages.py index 0f0d9b58b2..139552d845 100644 --- a/scripts/list_test_packages.py +++ b/scripts/list_test_packages.py @@ -111,7 +111,7 @@ def create_test_packages_list( if verbose: print(f"CMD: {' '.join(cmd_list)}") pip_download_process = subprocess.run( - cmd_list, capture_output=True, text=True + cmd_list, capture_output=True, text=True, check=False ) if pip_download_process.returncode == 0: print(f"Examined {test_package['spec']}{test_package_option_string}") diff --git a/scripts/migrate_pipsi_to_pipx.py b/scripts/migrate_pipsi_to_pipx.py index bf81fa1a1c..74df56e250 100644 --- a/scripts/migrate_pipsi_to_pipx.py +++ b/scripts/migrate_pipsi_to_pipx.py @@ -38,7 +38,7 @@ def main(): error = False for package in packages: - ret = subprocess.run(["pipsi", "uninstall", "--yes", package]) + ret = subprocess.run(["pipsi", "uninstall", "--yes", package], check=False) if ret.returncode: error = True print( @@ -49,7 +49,7 @@ def main(): print( f"uninstalled {package!r} with pipsi. Now attempting to install with pipx." ) - ret = subprocess.run(["pipx", "install", package]) + ret = subprocess.run(["pipx", "install", package], check=False) if ret.returncode: error = True print(f"Failed to install {package!r} with pipx.") diff --git a/scripts/pipx_release.py b/scripts/pipx_release.py index 989c9b7652..90151a8df4 100644 --- a/scripts/pipx_release.py +++ b/scripts/pipx_release.py @@ -4,7 +4,7 @@ def python_mypy_ok(filepath: Path) -> bool: - mypy_proc = subprocess.run(["mypy", filepath]) + mypy_proc = subprocess.run(["mypy", filepath], check=False) return True if mypy_proc.returncode == 0 else False diff --git a/scripts/update_package_cache.py b/scripts/update_package_cache.py index 09cd7393e2..ee0ad07440 100644 --- a/scripts/update_package_cache.py +++ b/scripts/update_package_cache.py @@ -162,6 +162,7 @@ def update_test_packages_cache( ], capture_output=True, text=True, + check=False, ) if pip_download_process.returncode == 0: print(f"Successfully downloaded {package_spec}") diff --git a/src/pipx/interpreter.py b/src/pipx/interpreter.py index 6ec093c823..8eb0f727e3 100644 --- a/src/pipx/interpreter.py +++ b/src/pipx/interpreter.py @@ -34,6 +34,7 @@ def find_py_launcher_python(python_version: Optional[str] = None) -> Optional[st [py, f"-{python_version}", "-c", "import sys; print(sys.executable)"], capture_output=True, text=True, + check=True, ).stdout.strip() return py @@ -54,7 +55,7 @@ def _find_default_windows_python() -> str: # https://twitter.com/zooba/status/1212454929379581952 proc = subprocess.run( - [python, "-V"], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL + [python, "-V"], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, check=False ) if proc.returncode != 0: # Cover the 9009 return code pre-emptively. diff --git a/src/pipx/util.py b/src/pipx/util.py index b390fffe28..c7a76708fb 100644 --- a/src/pipx/util.py +++ b/src/pipx/util.py @@ -180,6 +180,7 @@ def run_subprocess( stderr=subprocess.PIPE if capture_stderr else None, encoding="utf-8", universal_newlines=True, + check=False, ) if capture_stdout and log_stdout: @@ -396,6 +397,7 @@ def exec_app( stderr=None, encoding="utf-8", universal_newlines=True, + check=False, ).returncode ) else: diff --git a/tests/conftest.py b/tests/conftest.py index e4eda1163b..a76542013e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -109,7 +109,7 @@ def pipx_local_pypiserver(request): str(PIPX_TESTS_PACKAGE_LIST_DIR), str(pipx_cache_dir), ] - check_test_packages_process = subprocess.run(check_test_packages_cmd) + check_test_packages_process = subprocess.run(check_test_packages_cmd, check=False) if check_test_packages_process.returncode != 0: raise Exception( f"Directory {str(pipx_cache_dir)} does not contain all " diff --git a/tests/test_install.py b/tests/test_install.py index 04e4423901..75702de02d 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -257,7 +257,7 @@ def test_install_pip_failure(pipx_temp_env, capsys): def test_install_local_archive(pipx_temp_env, monkeypatch, capsys): monkeypatch.chdir(Path(TEST_DATA_PATH) / "local_extras") - subprocess.run([sys.executable, "-m", "pip", "wheel", "."]) + subprocess.run([sys.executable, "-m", "pip", "wheel", "."], check=True) assert not run_pipx_cli(["install", "repeatme-0.1-py3-none-any.whl"]) captured = capsys.readouterr() assert f"- {app_name('repeatme')}\n" in captured.out diff --git a/tests/test_install_all_packages.py b/tests/test_install_all_packages.py index 626d86b314..9c84496796 100644 --- a/tests/test_install_all_packages.py +++ b/tests/test_install_all_packages.py @@ -163,7 +163,7 @@ def module_globals() -> ModuleGlobalsData: def pip_cache_purge() -> None: - subprocess.run([sys.executable, "-m", "pip", "cache", "purge"]) + subprocess.run([sys.executable, "-m", "pip", "cache", "purge"], check=True) def write_report_legend(report_legend_path: Path) -> None: diff --git a/tests/test_run.py b/tests/test_run.py index f70c140eac..f4199a0598 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -31,6 +31,7 @@ def execvpe_mock(cmd_path, cmd_args, env): capture_output=False, encoding="utf-8", text=True, + check=False, ).returncode sys.exit(return_code) @@ -145,6 +146,7 @@ def test_run_ensure_null_pythonpath(): env=env, capture_output=True, text=True, + check=True, ).stdout ) From 51f78b7d7c55fe54e39adeb355c941fad53060c9 Mon Sep 17 00:00:00 2001 From: Jason Lam Date: Fri, 1 Dec 2023 03:28:16 +0800 Subject: [PATCH 11/21] `pipx install`: emit a warning when `--force` and `--python` were passed at the same time (#899) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bernát Gábor --- CHANGELOG.md | 1 + src/pipx/commands/install.py | 17 ++++++++++++++++- src/pipx/commands/reinstall.py | 1 + src/pipx/main.py | 3 ++- tests/test_install.py | 11 +++++++++++ 5 files changed, 31 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1288033a40..e26252f048 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## dev +- `pipx install`: emit a warning when `--force` and `--python` were passed at the same time - Drop support for Python 3.7 - Make usage message in `pipx run` show `package_or_url`, so extra will be printed out as well - Add `--force-reinstall` to pip arguments when `--force` was passed diff --git a/src/pipx/commands/install.py b/src/pipx/commands/install.py index 8d6ae2b94a..218bf7d39e 100644 --- a/src/pipx/commands/install.py +++ b/src/pipx/commands/install.py @@ -4,6 +4,7 @@ from pipx import constants from pipx.commands.common import package_name_from_spec, run_post_install_actions from pipx.constants import EXIT_CODE_INSTALL_VENV_EXISTS, EXIT_CODE_OK, ExitCode +from pipx.interpreter import DEFAULT_PYTHON from pipx.util import pipx_wrap from pipx.venv import Venv, VenvContainer @@ -13,12 +14,13 @@ def install( package_name: Optional[str], package_spec: str, local_bin_dir: Path, - python: str, + python: Optional[str], pip_args: List[str], venv_args: List[str], verbose: bool, *, force: bool, + reinstall: bool, include_dependencies: bool, preinstall_packages: Optional[List[str]], suffix: str = "", @@ -26,6 +28,9 @@ def install( """Returns pipx exit code.""" # package_spec is anything pip-installable, including package_name, vcs spec, # zip file, or tar.gz file. + python_flag_was_passed = python is not None + + python = python or DEFAULT_PYTHON if package_name is None: package_name = package_name_from_spec( @@ -42,6 +47,16 @@ def install( venv = Venv(venv_dir, python=python, verbose=verbose) if exists: + if not reinstall and force and python_flag_was_passed: + print( + pipx_wrap( + f""" + --python is ignored when --force is passed. + If you want to reinstall {package_name} with {python}, + run `pipx reinstall {package_spec} --python {python}` instead. + """ + ) + ) if force: print(f"Installing to existing venv {venv.name!r}") pip_args += ["--force-reinstall"] diff --git a/src/pipx/commands/reinstall.py b/src/pipx/commands/reinstall.py index b8ab23fc97..a15bb292f5 100644 --- a/src/pipx/commands/reinstall.py +++ b/src/pipx/commands/reinstall.py @@ -69,6 +69,7 @@ def reinstall( venv.pipx_metadata.venv_args, verbose, force=True, + reinstall=True, include_dependencies=venv.pipx_metadata.main_package.include_dependencies, preinstall_packages=[], suffix=venv.pipx_metadata.main_package.suffix, diff --git a/src/pipx/main.py b/src/pipx/main.py index bb5750d164..fb59a8db8a 100644 --- a/src/pipx/main.py +++ b/src/pipx/main.py @@ -212,6 +212,7 @@ def run_pipx_command(args: argparse.Namespace) -> ExitCode: # noqa: C901 venv_args, verbose, force=args.force, + reinstall=False, include_dependencies=args.include_deps, preinstall_packages=args.preinstall, suffix=args.suffix, @@ -344,7 +345,7 @@ def _add_install(subparsers: argparse._SubParsersAction) -> None: ) p.add_argument( "--python", - default=DEFAULT_PYTHON, + # Don't pass a default Python here so we know whether --python flag was passed help=( "Python to install with. Possible values can be the executable name (python3.11), " "the version to pass to py launcher (3.11), or the full path to the executable." diff --git a/tests/test_install.py b/tests/test_install.py index 75702de02d..87fa2a8d1c 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -284,3 +284,14 @@ def test_preinstall(pipx_temp_env, caplog): def test_do_not_wait_for_input(pipx_temp_env, pipx_session_shared_dir, monkeypatch): monkeypatch.setenv("PIP_INDEX_URL", "http://127.0.0.1:8080/simple") run_pipx_cli(["install", "pycowsay"]) + + +def test_passed_python_and_force_flag_warning(pipx_temp_env, capsys): + assert not run_pipx_cli(["install", "black"]) + assert not run_pipx_cli(["install", "--python", sys.executable, "--force", "black"]) + captured = capsys.readouterr() + assert "--python is ignored when --force is passed." in captured.out + + assert not run_pipx_cli(["install", "pycowsay", "--force"]) + captured = capsys.readouterr() + assert "--python is ignored when --force is passed." not in captured.out From d4798ea9fdabcaac93fbc177e4a440173f88ad58 Mon Sep 17 00:00:00 2001 From: chrysle Date: Thu, 30 Nov 2023 20:32:04 +0100 Subject: [PATCH 12/21] Added `with-suffix` option for `pipx inject` command (#939) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bernát Gábor --- CHANGELOG.md | 1 + src/pipx/commands/common.py | 7 +++++++ src/pipx/commands/inject.py | 9 ++++++++- src/pipx/main.py | 6 ++++++ 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e26252f048..b70bc4d579 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## dev +- Add `--with-suffix` for `pipx inject` command - `pipx install`: emit a warning when `--force` and `--python` were passed at the same time - Drop support for Python 3.7 - Make usage message in `pipx run` show `package_or_url`, so extra will be printed out as well diff --git a/src/pipx/commands/common.py b/src/pipx/commands/common.py index 33ece9fb21..8666324806 100644 --- a/src/pipx/commands/common.py +++ b/src/pipx/commands/common.py @@ -350,6 +350,13 @@ def run_post_install_actions( display_name = f"{package_name}{package_metadata.suffix}" + if ( + not venv.main_package_name == package_name + and venv.package_metadata[venv.main_package_name].suffix + == package_metadata.suffix + ): + package_name = display_name + if not package_metadata.apps: if not package_metadata.apps_of_dependencies: if venv.safe_to_remove(): diff --git a/src/pipx/commands/inject.py b/src/pipx/commands/inject.py index 68a2243410..439766ef17 100644 --- a/src/pipx/commands/inject.py +++ b/src/pipx/commands/inject.py @@ -22,6 +22,7 @@ def inject_dep( include_apps: bool, include_dependencies: bool, force: bool, + suffix: bool = False, ) -> bool: if not venv_dir.exists() or not next(venv_dir.iterdir()): raise PipxError( @@ -53,7 +54,10 @@ def inject_dep( pip_args=pip_args, verbose=verbose, ) - + if suffix: + venv_suffix = venv.package_metadata[venv.main_package_name].suffix + else: + venv_suffix = "" venv.install_package( package_name=package_name, package_or_url=package_spec, @@ -61,6 +65,7 @@ def inject_dep( include_dependencies=include_dependencies, include_apps=include_apps, is_main_package=False, + suffix=venv_suffix, ) if include_apps: run_post_install_actions( @@ -89,6 +94,7 @@ def inject( include_apps: bool, include_dependencies: bool, force: bool, + suffix: bool = False, ) -> ExitCode: """Returns pipx exit code.""" if not include_apps and include_dependencies: @@ -106,6 +112,7 @@ def inject( include_apps=include_apps, include_dependencies=include_dependencies, force=force, + suffix=suffix, ) # Any failure to install will raise PipxError, otherwise success diff --git a/src/pipx/main.py b/src/pipx/main.py index fb59a8db8a..03cfe0e047 100644 --- a/src/pipx/main.py +++ b/src/pipx/main.py @@ -227,6 +227,7 @@ def run_pipx_command(args: argparse.Namespace) -> ExitCode: # noqa: C901 include_apps=args.include_apps, include_dependencies=args.include_deps, force=args.force, + suffix=args.with_suffix, ) elif args.command == "uninject": return commands.uninject( @@ -392,6 +393,11 @@ def _add_inject(subparsers, venv_completer: VenvCompleter) -> None: help="Modify existing virtual environment and files in PIPX_BIN_DIR", ) p.add_argument("--verbose", action="store_true") + p.add_argument( + "--with-suffix", + action="store_true", + help="Add the suffix (if given) of the Virtual Environment to the packages to inject", + ) def _add_uninject(subparsers, venv_completer: VenvCompleter): From 1508f189341176d0893a96f01a519116becb5a33 Mon Sep 17 00:00:00 2001 From: chrysle Date: Thu, 30 Nov 2023 20:32:38 +0100 Subject: [PATCH 13/21] `pipx inject`: Imply `--include-apps` if `--include-deps` is passed (#984) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bernát Gábor --- CHANGELOG.md | 1 + src/pipx/commands/inject.py | 4 +--- src/pipx/main.py | 6 +++++- tests/test_inject.py | 18 +++--------------- 4 files changed, 10 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b70bc4d579..b2bfd4d901 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## dev +- Imply `--include-apps` when running `pipx inject --include-deps` - Add `--with-suffix` for `pipx inject` command - `pipx install`: emit a warning when `--force` and `--python` were passed at the same time - Drop support for Python 3.7 diff --git a/src/pipx/commands/inject.py b/src/pipx/commands/inject.py index 439766ef17..f967e00ced 100644 --- a/src/pipx/commands/inject.py +++ b/src/pipx/commands/inject.py @@ -98,9 +98,7 @@ def inject( ) -> ExitCode: """Returns pipx exit code.""" if not include_apps and include_dependencies: - raise PipxError( - "Cannot pass --include-deps if --include-apps is not passed as well" - ) + include_apps = True all_success = True for dep in package_specs: all_success &= inject_dep( diff --git a/src/pipx/main.py b/src/pipx/main.py index 03cfe0e047..25f9ad08f8 100644 --- a/src/pipx/main.py +++ b/src/pipx/main.py @@ -384,7 +384,11 @@ def _add_inject(subparsers, venv_completer: VenvCompleter) -> None: action="store_true", help="Add apps from the injected packages onto your PATH", ) - add_include_dependencies(p) + p.add_argument( + "--include-deps", + help="Include apps of dependent packages. Implies --include-apps", + action="store_true", + ) add_pip_venv_args(p) p.add_argument( "--force", diff --git a/tests/test_inject.py b/tests/test_inject.py index 2e891db440..4cf262ef62 100644 --- a/tests/test_inject.py +++ b/tests/test_inject.py @@ -41,27 +41,15 @@ def test_inject_include_apps(pipx_temp_env, capsys, with_suffix): install_args = [f"--suffix={suffix}"] assert not run_pipx_cli(["install", "pycowsay", *install_args]) - assert run_pipx_cli( + assert not run_pipx_cli( ["inject", f"pycowsay{suffix}", PKG["black"]["spec"], "--include-deps"] ) if suffix: assert run_pipx_cli( - [ - "inject", - "pycowsay", - PKG["black"]["spec"], - "--include-deps", - "--include-apps", - ] + ["inject", "pycowsay", PKG["black"]["spec"], "--include-deps"] ) assert not run_pipx_cli( - [ - "inject", - f"pycowsay{suffix}", - PKG["black"]["spec"], - "--include-deps", - "--include-apps", - ] + ["inject", f"pycowsay{suffix}", PKG["black"]["spec"], "--include-deps"] ) From 61c8e920709a3e023ae9254c1ef113c3f60dd58b Mon Sep 17 00:00:00 2001 From: Ilya Grigoriev Date: Thu, 30 Nov 2023 11:35:35 -0800 Subject: [PATCH 14/21] install docs: reorganize, discourage installing `pipx` with `pipx` (#1109) --- README.md | 8 +++++++- docs/installation.md | 16 +++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 08f410fe07..f2a6aa23c3 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,12 @@ _For comparison to other tools including pipsi, see [Comparison to Other Tools]( ## Install pipx +> [!NOTE] +> It is not recommended to install `pipx` via `pipx`. If you'd like +> to do this anyway, take a look at the +> [`pipx-in-pipx`](https://github.com/mattsb42-meta/pipx-in-pipx) project and +> read about the limitations there. + ### On macOS ``` @@ -69,7 +75,7 @@ Restart your terminal session and verify `pipx` does run. Upgrade pipx with `py -m pip install --user --upgrade pipx`. -### Via zipapp +### Using pipx without installing (via zipapp) You can also use pipx without installing it. The zipapp can be downloaded from [Github releases](https://github.com/pypa/pipx/releases) and you can invoke it with a Python 3.7+ interpreter: diff --git a/docs/installation.md b/docs/installation.md index e8da2573bc..d559104955 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -6,7 +6,7 @@ You also need to have `pip` installed on your machine for `python3`. Installing pipx works on macOS, linux, and Windows. -## Install pipx +## Installing pipx On macOS: @@ -29,18 +29,24 @@ python3 -m pip install --user pipx python3 -m pipx ensurepath ``` -Or via zipapp: +!!!caution + It is not recommended to install `pipx` via `pipx`. If you'd like + to do this anyway, take a look at the + [`pipx-in-pipx`](https://github.com/mattsb42-meta/pipx-in-pipx) project and + read about the limitations there. -You can also use pipx without installing it. + +### Using pipx without installing (via zipapp) The zipapp can be downloaded from [Github releases](https://github.com/pypa/pipx/releases) and you can invoke it with a Python 3.7+ interpreter: ``` python pipx.pyz ensurepath ``` -Or use with pre-commit: +### Using pipx with pre-commit Pipx has [pre-commit](https://pre-commit.com/) support. This lets you run applications: + * That can be run using `pipx run` but don't have native pre-commit support. * Using its prebuilt wheel from pypi.org instead of building it from source. * Using pipx's `--spec` and `--index-url` flags. @@ -58,7 +64,7 @@ Example configuration for use of the code linter [yapf](https://github.com/googl types: ['python'] ``` -### Installation Options +## Installation Options The default binary location for pipx-installed apps is `~/.local/bin`. This can be overridden with the environment variable `PIPX_BIN_DIR`. From 60c5f24665bc4a906decb49d9af76a53838827d0 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Thu, 30 Nov 2023 14:36:26 -0500 Subject: [PATCH 15/21] feat: support PEP 723 run requirements (#1100) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bernát Gábor --- CHANGELOG.md | 1 + pyproject.toml | 1 + src/pipx/commands/run.py | 66 ++++++++++++++++++++++++---------------- tests/test_run.py | 19 +++++++----- 4 files changed, 52 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2bfd4d901..9ca9c78450 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## dev +- Support PEP 723 run requirements in `pipx run`. - Imply `--include-apps` when running `pipx inject --include-deps` - Add `--with-suffix` for `pipx inject` command - `pipx install`: emit a warning when `--force` and `--python` were passed at the same time diff --git a/pyproject.toml b/pyproject.toml index 4539d31f77..950f0f54d6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ dependencies = [ "colorama>=0.4.4; sys_platform == 'win32'", "packaging>=20.0", "platformdirs>=2.1.0", + "tomli; python_version < '3.11'", "userpath>=1.6.0,!=1.9.0", ] dynamic = ["version"] diff --git a/src/pipx/commands/run.py b/src/pipx/commands/run.py index 15428e081c..575565c129 100644 --- a/src/pipx/commands/run.py +++ b/src/pipx/commands/run.py @@ -1,6 +1,8 @@ import datetime import hashlib import logging +import re +import sys import time import urllib.parse import urllib.request @@ -24,6 +26,11 @@ ) from pipx.venv import Venv +if sys.version_info < (3, 11): + import tomli as tomllib +else: + import tomllib + logger = logging.getLogger(__name__) @@ -319,41 +326,46 @@ def _http_get_request(url: str) -> str: raise PipxError(str(e)) from e +# This regex comes from PEP 723 +PEP723 = re.compile( + r"(?m)^# /// (?P[a-zA-Z0-9-]+)$\s(?P(^#(| .*)$\s)+)^# ///$" +) + + def _get_requirements_from_script(content: str) -> Optional[List[str]]: - # An iterator over the lines in the script. We will - # read through this in sections, so it needs to be an - # iterator, not just a list. - lines = iter(content.splitlines()) - - for line in lines: - if not line.startswith("#"): - continue - line_content = line[1:].strip() - if line_content == "Requirements:": - break - else: - # No "Requirements:" line in the file + """ + Supports PEP 723. + """ + + name = "pyproject" + + # Windows is currently getting un-normalized line endings, so normalize + content = content.replace("\r\n", "\n") + + matches = [m for m in PEP723.finditer(content) if m.group("type") == name] + + if not matches: return None - # We are now at the first requirement - requirements = [] - for line in lines: - # Stop at the end of the comment block - if not line.startswith("#"): - break - line_content = line[1:].strip() - # Stop at a blank comment line - if not line_content: - break + if len(matches) > 1: + raise ValueError(f"Multiple {name} blocks found") + content = "".join( + line[2:] if line.startswith("# ") else line[1:] + for line in matches[0].group("content").splitlines(keepends=True) + ) + + pyproject = tomllib.loads(content) + + requirements = [] + for requirement in pyproject.get("run", {}).get("requirements", []): # Validate the requirement try: - req = Requirement(line_content) + req = Requirement(requirement) except InvalidRequirement as e: - raise PipxError(f"Invalid requirement {line_content}: {str(e)}") from e + raise PipxError(f"Invalid requirement {requirement}: {e}") from e - # Use the normalised form of the requirement, - # not the original line. + # Use the normalised form of the requirement requirements.append(str(req)) return requirements diff --git a/tests/test_run.py b/tests/test_run.py index f4199a0598..6a6708d50a 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -208,8 +208,9 @@ def test_run_with_requirements(caplog, pipx_temp_env, tmp_path): script.write_text( textwrap.dedent( f""" - # Requirements: - # requests==2.28.1 + # /// pyproject + # run.requirements = ["requests==2.28.1"] + # /// # Check requests can be imported import requests @@ -219,7 +220,8 @@ def test_run_with_requirements(caplog, pipx_temp_env, tmp_path): from pathlib import Path Path({repr(str(out))}).write_text(requests.__version__) """ - ).strip() + ).strip(), + encoding="utf-8", ) run_pipx_cli_exit(["run", script.as_uri()]) assert out.read_text() == "2.28.1" @@ -249,9 +251,9 @@ def test_run_with_requirements_and_args(caplog, pipx_temp_env, tmp_path): script.write_text( textwrap.dedent( f""" - # Requirements: - # packaging - + # /// pyproject + # run.requirements = ["packaging"] + # /// import packaging import sys from pathlib import Path @@ -269,8 +271,9 @@ def test_run_with_invalid_requirement(capsys, pipx_temp_env, tmp_path): script.write_text( textwrap.dedent( """ - # Requirements: - # this is an invalid requirement + # /// pyproject + # run.requirements = ["this is an invalid requirement"] + # /// print() """ ).strip() From ef9434feea5021e10ce7263aaab3dc1ec2714f93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Feitosa=20da=20Silva?= Date: Thu, 30 Nov 2023 16:44:58 -0300 Subject: [PATCH 16/21] =?UTF-8?q?=F0=9F=93=9D=20Update=20installation=20me?= =?UTF-8?q?thod=20for=20linux=20users=20(#972)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jason Lam --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f2a6aa23c3..cd28d0257b 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,17 @@ pipx ensurepath Upgrade pipx with `brew update && brew upgrade pipx`. -### On Linux, install via pip (requires pip 19.0 or later) +### On Linux + +- Ubuntu 23.04 or above + +``` +sudo apt update +sudo apt install pipx +pipx ensurepath +``` + +- Ubuntu 22.04 or below ``` python3 -m pip install --user pipx From 18d6722fa6e998a9c0aadee829a82452ede90dcc Mon Sep 17 00:00:00 2001 From: Mark Blakeney Date: Fri, 1 Dec 2023 08:32:50 +1000 Subject: [PATCH 17/21] Fix PIPX_HOME move instruction and improve text (#1063) * Fix PIPX_HOME move instruction and improve text * Adopt chrysle suggested word changes --- docs/troubleshooting.md | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 4246a8e6d9..add5c50510 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -122,18 +122,23 @@ rm -rf test_venv ## Pipx files not in expected locations according to documentation -The default PIPX_HOME is `~/.local/pipx`, prior to the adoption of the XDG base -directory specification after version 1.2.0. To maintain compatibility with older -versions, pipx will automatically detect the old paths and use them accordingly. -For a map of old and new paths, See [Installation](installation.md#installation-options) - -To migrate from the old path to the new path, you can remove the `~/.local/pipx` directory and -reinstall all packages. - -For example, on Linux systems, you could read out `pipx`'s package information in JSON via `jq` (which you might need to install first): - -``` -packages=($(pipx list --json | jq '.venvs | keys[]' -r)) -rm -rf ~/.local/pipx -for p in ${packages[@]}; do pipx install "$p"; done +Pipx versions after 1.2.0 adopt the XDG base directory specification for +the location of `PIPX_HOME` and the data, cache, and log directories. +Version 1.2.0 and earlier use `~/.local/pipx` as the default `PIPX_HOME` +and install the data, cache, and log directories under it. To maintain +compatibility with older versions, pipx will automatically use this old +`PIPX_HOME` path if it exists. For a map of old and new paths, see +[Installation](installation.md#installation-options). + +If you have a `pipx` version later than 1.2.0 and want to migrate from +the old path to the new paths, you can move the `~/.local/pipx` +directory to the new location (after removing cache, log, and trash +directories which will get recreated automatically) and then reinstall +all packages. For example, on Linux systems, `PIPX_HOME` moves from +`~/.local/pipx` to `~/.local/share/pipx` so you can do this: + +``` +rm -rf ~/.local/pipx/{.cache,logs,trash} +mkdir -p ~/.local/share && mv ~/.local/pipx ~/.local/share/ +pipx reinstall-all ``` From 64b445e02a58baa83a1fe3b876f89a65af69ec59 Mon Sep 17 00:00:00 2001 From: chrysle Date: Sun, 8 Oct 2023 15:27:26 +0200 Subject: [PATCH 18/21] Drop `wheel` and `setuptools` from shared libs And enable running the remaining shared library `pip` using `pipx run`. --- CHANGELOG.md | 2 ++ docs/how-pipx-works.md | 8 ++++---- src/pipx/commands/install.py | 7 ++++++- src/pipx/commands/run.py | 7 ++++++- src/pipx/shared_libs.py | 6 ++---- src/pipx/venv.py | 34 +++++++++++++++++++++------------- tests/test_run.py | 7 +++++++ 7 files changed, 48 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ca9c78450..fb80dcb285 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## dev +- Drop `setuptools` and `wheel` from the shared libraries +- Allow running `pip` with `pipx run` - Support PEP 723 run requirements in `pipx run`. - Imply `--include-apps` when running `pipx inject --include-deps` - Add `--with-suffix` for `pipx inject` command diff --git a/docs/how-pipx-works.md b/docs/how-pipx-works.md index 622543d47a..17a75efac5 100644 --- a/docs/how-pipx-works.md +++ b/docs/how-pipx-works.md @@ -3,8 +3,8 @@ When installing a package and its binaries on linux (`pipx install package`) pipx will - create directory `~/.local/share/pipx/venvs/PACKAGE` -- create or re-use a shared virtual environment that contains shared packaging libraries `pip`, `setuptools` and `wheel` in `~/.local/share/pipx/shared/` -- ensure all packaging libraries are updated to their latest versions +- create or re-use a shared virtual environment that contains shared packaging library `pip` in `~/.local/share/pipx/shared/` +- ensure the library is updated to its latest version - create a Virtual Environment in `~/.local/share/pipx/venvs/PACKAGE` that uses the shared pip mentioned above but otherwise is isolated (pipx uses a [.pth file]( https://docs.python.org/3/library/site.html) to do this) - install the desired package in the Virtual Environment - expose binaries at `~/.local/bin` that point to new binaries in `~/.local/share/pipx/venvs/PACKAGE/bin` (such as `~/.local/bin/black` -> `~/.local/share/pipx/venvs/black/bin/black`) @@ -12,8 +12,8 @@ When installing a package and its binaries on linux (`pipx install package`) pip When running a binary (`pipx run BINARY`), pipx will -- create or re-use a shared virtual environment that contains shared packaging libraries `pip`, `setuptools` and `wheel` in `~/.local/share/pipx/shared/` -- ensure all packaging libraries are updated to their latest versions +- create or re-use a shared virtual environment that contains the shared packaging library `pip` +- ensure the library is updated to its latest version - create a temporary directory (or reuse a cached virtual environment for this package) with a name based on a hash of the attributes that make the run reproducible. This includes things like the package name, spec, python version, and pip arguments. - create a Virtual Environment inside it with `python -m venv` - install the desired package in the Virtual Environment diff --git a/src/pipx/commands/install.py b/src/pipx/commands/install.py index 218bf7d39e..d0f1de38c4 100644 --- a/src/pipx/commands/install.py +++ b/src/pipx/commands/install.py @@ -73,7 +73,12 @@ def install( return EXIT_CODE_INSTALL_VENV_EXISTS try: - venv.create_venv(venv_args, pip_args) + # Enable installing shared library `pip` with `pipx` + override_shared = False + + if package_name == "pip": + override_shared = True + venv.create_venv(venv_args, pip_args, override_shared) for dep in preinstall_packages or []: dep_name = package_name_from_spec( dep, python, pip_args=pip_args, verbose=verbose diff --git a/src/pipx/commands/run.py b/src/pipx/commands/run.py index 575565c129..c57fabf83e 100644 --- a/src/pipx/commands/run.py +++ b/src/pipx/commands/run.py @@ -225,7 +225,6 @@ def _download_and_run( verbose: bool, ) -> NoReturn: venv = Venv(venv_dir, python=python, verbose=verbose) - venv.create_venv(venv_args, pip_args) if venv.pipx_metadata.main_package.package is not None: package_name = venv.pipx_metadata.main_package.package @@ -234,6 +233,12 @@ def _download_and_run( package_or_url, python, pip_args=pip_args, verbose=verbose ) + override_shared = False + + if package_name == "pip": + override_shared = True + + venv.create_venv(venv_args, pip_args, override_shared) venv.install_package( package_name=package_name, package_or_url=package_or_url, diff --git a/src/pipx/shared_libs.py b/src/pipx/shared_libs.py index 5a752c9bd9..625ad98f06 100644 --- a/src/pipx/shared_libs.py +++ b/src/pipx/shared_libs.py @@ -26,8 +26,8 @@ def __init__(self) -> None: self.root = constants.PIPX_SHARED_LIBS self.bin_path, self.python_path = get_venv_paths(self.root) self.pip_path = self.bin_path / ("pip" if not WINDOWS else "pip.exe") - # i.e. bin_path is ~/.local/pipx/shared/bin - # i.e. python_path is ~/.local/pipx/shared/python + # i.e. bin_path is ~/.local/share/pipx/shared/bin + # i.e. python_path is ~/.local/share/pipx/shared/python self._site_packages: Optional[Path] = None self.has_been_updated_this_run = False self.has_been_logged_this_run = False @@ -107,8 +107,6 @@ def upgrade( *_pip_args, "--upgrade", "pip", - "setuptools", - "wheel", ] ) subprocess_post_check(upgrade_process) diff --git a/src/pipx/venv.py b/src/pipx/venv.py index 28189cc510..70921e86dc 100644 --- a/src/pipx/venv.py +++ b/src/pipx/venv.py @@ -156,24 +156,32 @@ def main_package_name(self) -> str: else: return self.pipx_metadata.main_package.package - def create_venv(self, venv_args: List[str], pip_args: List[str]) -> None: + def create_venv( + self, venv_args: List[str], pip_args: List[str], override_shared: bool = False + ) -> None: + """ + override_shared -- Override installing shared libraries to the pipx shared directory (default False) + """ with animate("creating virtual environment", self.do_animation): - cmd = [self.python, "-m", "venv", "--without-pip"] + cmd = [self.python, "-m", "venv"] + if not override_shared: + cmd.append("--without-pip") venv_process = run_subprocess(cmd + venv_args + [str(self.root)]) subprocess_post_check(venv_process) shared_libs.create(self.verbose) - pipx_pth = get_site_packages(self.python_path) / PIPX_SHARED_PTH - # write path pointing to the shared libs site-packages directory - # example pipx_pth location: - # ~/.local/pipx/venvs/black/lib/python3.8/site-packages/pipx_shared.pth - # example shared_libs.site_packages location: - # ~/.local/pipx/shared/lib/python3.6/site-packages - # - # https://docs.python.org/3/library/site.html - # A path configuration file is a file whose name has the form 'name.pth'. - # its contents are additional items (one per line) to be added to sys.path - pipx_pth.write_text(f"{shared_libs.site_packages}\n", encoding="utf-8") + if not override_shared: + pipx_pth = get_site_packages(self.python_path) / PIPX_SHARED_PTH + # write path pointing to the shared libs site-packages directory + # example pipx_pth location: + # ~/.local/share/pipx/venvs/black/lib/python3.8/site-packages/pipx_shared.pth + # example shared_libs.site_packages location: + # ~/.local/share/pipx/shared/lib/python3.6/site-packages + # + # https://docs.python.org/3/library/site.html + # A path configuration file is a file whose name has the form 'name.pth'. + # its contents are additional items (one per line) to be added to sys.path + pipx_pth.write_text(f"{shared_libs.site_packages}\n", encoding="utf-8") self.pipx_metadata.venv_args = venv_args self.pipx_metadata.python_version = self.get_python_version() diff --git a/tests/test_run.py b/tests/test_run.py index 6a6708d50a..7835eebe81 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -339,3 +339,10 @@ def test_run_with_windows_python_version(caplog, pipx_temp_env, tmp_path): ) run_pipx_cli_exit(["run", script.as_uri(), "--python", "3.11"]) assert "3.11" in out.read_text() + + +@mock.patch("os.execvpe", new=execvpe_mock) +def test_run_shared_lib_as_app(pipx_temp_env, monkeypatch, capfd): + run_pipx_cli_exit(["run", "pip", "--help"]) + captured = capfd.readouterr() + assert "pip [options]\n" in captured.out From fd4205c7f772995ebdcca8345dced196fcd24d80 Mon Sep 17 00:00:00 2001 From: chrysle Date: Tue, 24 Oct 2023 21:15:09 +0200 Subject: [PATCH 19/21] Update changelog and remove line ending from test assert --- CHANGELOG.md | 3 ++- tests/test_run.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb80dcb285..5371db415a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## dev -- Drop `setuptools` and `wheel` from the shared libraries +- Drop `setuptools` and `wheel` from the shared libraries. This results in less time consumption when the libraries are +automatically upgraded. - Allow running `pip` with `pipx run` - Support PEP 723 run requirements in `pipx run`. - Imply `--include-apps` when running `pipx inject --include-deps` diff --git a/tests/test_run.py b/tests/test_run.py index 7835eebe81..60a24c40a8 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -345,4 +345,4 @@ def test_run_with_windows_python_version(caplog, pipx_temp_env, tmp_path): def test_run_shared_lib_as_app(pipx_temp_env, monkeypatch, capfd): run_pipx_cli_exit(["run", "pip", "--help"]) captured = capfd.readouterr() - assert "pip [options]\n" in captured.out + assert "pip [options]" in captured.out From 913e823f10b14e9f6f3be5e03825e819c9336274 Mon Sep 17 00:00:00 2001 From: chrysle Date: Fri, 1 Dec 2023 06:55:14 +0100 Subject: [PATCH 20/21] Simplify test for `pip` and set minimum version Co-authored-by: Sviatoslav Sydorenko --- src/pipx/commands/install.py | 5 +---- src/pipx/commands/run.py | 5 +---- src/pipx/shared_libs.py | 2 +- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/pipx/commands/install.py b/src/pipx/commands/install.py index d0f1de38c4..ab76778551 100644 --- a/src/pipx/commands/install.py +++ b/src/pipx/commands/install.py @@ -74,10 +74,7 @@ def install( try: # Enable installing shared library `pip` with `pipx` - override_shared = False - - if package_name == "pip": - override_shared = True + override_shared = package_name == "pip" venv.create_venv(venv_args, pip_args, override_shared) for dep in preinstall_packages or []: dep_name = package_name_from_spec( diff --git a/src/pipx/commands/run.py b/src/pipx/commands/run.py index c57fabf83e..ce84efc8b6 100644 --- a/src/pipx/commands/run.py +++ b/src/pipx/commands/run.py @@ -233,10 +233,7 @@ def _download_and_run( package_or_url, python, pip_args=pip_args, verbose=verbose ) - override_shared = False - - if package_name == "pip": - override_shared = True + override_shared = package_name == "pip" venv.create_venv(venv_args, pip_args, override_shared) venv.install_package( diff --git a/src/pipx/shared_libs.py b/src/pipx/shared_libs.py index 625ad98f06..cfe938dbcb 100644 --- a/src/pipx/shared_libs.py +++ b/src/pipx/shared_libs.py @@ -106,7 +106,7 @@ def upgrade( "install", *_pip_args, "--upgrade", - "pip", + "pip >= 23.1", ] ) subprocess_post_check(upgrade_process) From dea8c7a1e78caa7f9d4936e0ffd987e71064472e Mon Sep 17 00:00:00 2001 From: Jason Lam Date: Fri, 1 Dec 2023 21:18:52 +0800 Subject: [PATCH 21/21] Fix TypeError in tests (#1117) --- src/pipx/main.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/pipx/main.py b/src/pipx/main.py index 25f9ad08f8..d829d1a227 100644 --- a/src/pipx/main.py +++ b/src/pipx/main.py @@ -181,10 +181,12 @@ def run_pipx_command(args: argparse.Namespace) -> ExitCode: # noqa: C901 logger.info(f"Virtual Environment location is {venv_dir}") if "skip" in args: skip_list = [canonicalize_name(x) for x in args.skip] - if "python" in args and not Path(args.python).is_file(): - py_launcher_python = find_py_launcher_python(args.python) - if py_launcher_python: - args.python = py_launcher_python + + if "python" in args: + if args.python is not None and not Path(args.python).is_file(): + py_launcher_python = find_py_launcher_python(args.python) + if py_launcher_python: + args.python = py_launcher_python if args.command == "run": commands.run(