# Copyright Kevin Deldycke <kevin@deldycke.com> and contributors.
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
from __future__ import annotations
from operator import attrgetter
from extra_platforms import ALL_PLATFORMS
from ..capabilities import version_not_implemented
from ..manager import PackageManager
from ..version import VersionRange
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Iterator
from ..package import Package
[docs]
class Pipx(PackageManager):
"""pipx installs Python CLI applications, each in its own isolated venv.
Installed applications come from ``pipx list --json``; only each venv's main
package is tracked, never the packages injected beside it. There is no
``search`` operation: the request was closed as not planned, since PyPI
exposes no search API and custom search is out of pipx's scope (see `pipx
issue 777 <https://github.com/pypa/pipx/issues/777#issuecomment-990919047>`__).
.. note::
The outdated query prefers pipx ``1.16.0``'s native
``pipx list --outdated``, which checks every venv in one call, each
against its own backend (pip or uv). An older pipx falls back to probing
each venv with its embedded pip, one call per application. The version
floor stays at ``1.0.0`` so an older pipx remains fully usable: only the
outdated path degrades.
.. note::
The supply-chain cooldown rides on the underlying pip and needs that pip
to be at least ``26.1``, the first release to honor
``--uploaded-prior-to``; older pip silently ignores the release-age gate.
"""
name = "Python pipx"
homepage_url = "https://pipx.pypa.io"
platforms = ALL_PLATFORMS
requirement = ">=1.0.0"
"""
.. code-block:: shell-session
$ pipx --version
1.0.0
"""
cooldown_env_var = "PIP_UPLOADED_PRIOR_TO"
"""pipx defers resolution to pip, so it honors pip's ``--uploaded-prior-to``
gate through the same environment variable.
Setting ``PIP_UPLOADED_PRIOR_TO`` on a pipx invocation propagates to the pip
subprocess pipx spawns to install the application and its dependencies, so the
cutoff applies to the whole resolution. mpm injects the RFC 3339 timestamp from
the default :py:meth:`meta_package_manager.execution.CLIExecutor.cooldown_env_value`.
.. caution::
Same caveat as :py:class:`meta_package_manager.managers.pip.Pip`: the
underlying pip must be at least ``26.1`` for the gate to take effect. Older
pip releases silently ignore the env var.
See https://github.com/pypa/pipx/issues/1811.
"""
outdated_requirement = ">=1.16.0"
"""Minimum pipx version providing the native outdated query.
`1.16.0 <https://github.com/pypa/pipx/releases/tag/1.16.0>`_ introduced
``pipx list --outdated`` (see https://github.com/pypa/pipx/issues/149). Kept
apart from :py:attr:`requirement` (``>=1.0.0``) so an older pipx stays fully
usable, :py:attr:`outdated` falling back to one pip probe per venv.
"""
@property
def installed(self) -> Iterator[Package]:
"""Fetch installed packages.
.. code-block:: shell-session
$ pipx list --json
{
"pipx_spec_version": "0.1",
"venvs": {
"pycowsay": {
"metadata": {
"injected_packages": {},
"main_package": {
"app_paths": [
{
"__Path__": "~/.local/pipx/venvs/pycowsay/bin/pycowsay",
"__type__": "Path"
}
],
"app_paths_of_dependencies": {},
"apps": [
"pycowsay"
],
"apps_of_dependencies": [],
"include_apps": true,
"include_dependencies": false,
"package": "pycowsay",
"package_or_url": "pycowsay",
"package_version": "0.0.0.1",
"pip_args": [],
"suffix": ""
},
"pipx_metadata_version": "0.2",
"python_version": "Python 3.10.4",
"venv_args": []
}
}
}
}
"""
output = self.run_cli("list", "--json", must_succeed=True)
data = self.parse_json(output)
if data:
for package_id, package_info in data["venvs"].items():
yield self.package(
id=package_id,
installed_version=package_info["metadata"]["main_package"][
"package_version"
],
)
@property
def outdated(self) -> Iterator[Package]:
"""Fetch outdated packages.
pipx ``1.16.0`` introduced a native outdated query, which checks all
venvs in a single call, each against its own backend (pip or uv).
Injected packages are excluded by default, so only each venv's main
package is reported. Pinned packages are kept: a newer release exists,
even if ``pipx upgrade`` skips them.
.. code-block:: shell-session
$ pipx list --outdated --output=json
{
"command": ["list"],
"data": {
"packages_checked": 1,
"packages": [
{
"environment": "pycowsay",
"package": "pycowsay",
"version": "0.0.0.1",
"latest_version": "0.0.0.2",
"injected": false,
"pinned": false
}
],
"skipped": []
},
"errors": [],
"exit_code": 0,
"pipx_result_version": "1",
"status": "success"
}
A pipx older than :py:attr:`outdated_requirement` falls back to probing
each venv with its embedded pip, one call per package. Only the venv's
main package is reported; its dependencies also show up in
``pip list --outdated`` but are silenced:
.. code-block:: shell-session
$ pipx runpip pycowsay list --no-color --format=json --outdated \
> --verbose --quiet
[
{
"name": "pycowsay",
"version": "0.0.0.1",
"location": "~/.local/pipx/venvs/pycowsay/lib/python3.10/site-packages",
"installer": "pip",
"latest_version": "0.0.0.2",
"latest_filetype": "wheel"
}
]
.. todo::
Drop the fallback, which mimics ``Pip.outdated()``, once pipx
``1.16.0`` is old enough to be required outright.
"""
if self.version is not None and self.version in VersionRange(
self.outdated_requirement
):
output = self.run_cli(
"list", "--outdated", "--output=json", must_succeed=True
)
data = self.parse_json(output)
if data:
for package_info in data["data"]["packages"]:
yield self.package(
id=package_info["package"],
installed_version=package_info["version"],
latest_version=package_info["latest_version"],
)
return
for main_package_id in map(attrgetter("id"), self.installed):
# --quiet is required here to silence warning and error messages
# mangling the JSON content.
output = self.run_cli(
"runpip",
main_package_id,
"list",
"--no-color",
"--format=json",
"--outdated",
"--verbose",
"--quiet",
must_succeed=True,
)
data = self.parse_json(output)
if data:
for sub_package in data:
# Only report the main package as outdated, silencing its
# dependencies.
sub_package_id = sub_package["name"]
if sub_package_id == main_package_id:
yield self.package(
id=sub_package_id,
installed_version=sub_package["version"],
latest_version=sub_package["latest_version"],
)
# No search operation: the feature request was closed as not planned, as PyPI
# exposes no search API and custom search logic is out of pipx's design scope.
# See https://github.com/pypa/pipx/issues/777#issuecomment-990919047.
[docs]
@version_not_implemented
def install(self, package_id: str, version: str | None = None) -> str:
"""Install one package.
.. code-block:: shell-session
$ pipx install pycowsay
installed package pycowsay 0.0.0.1, installed using Python 3.10.4
These apps are now globally available
- pycowsay
done! β¨ π β¨
"""
return self.run_cli("install", package_id)
[docs]
def upgrade_all_cli(self) -> tuple[str, ...]:
"""Upgrade all packages."""
return self.build_cli("upgrade-all")
[docs]
@version_not_implemented
def upgrade_one_cli(
self,
package_id: str,
version: str | None = None,
) -> tuple[str, ...]:
"""Upgrade the package provided as parameter."""
return self.build_cli("upgrade", package_id)
[docs]
def remove(self, package_id: str) -> str:
"""Remove one package.
.. code-block:: shell-session
$ pipx uninstall pycowsay
uninstalled pycowsay! β¨ π β¨
"""
return self.run_cli("uninstall", package_id)