# 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
import json
from operator import attrgetter
from extra_platforms import ALL_PLATFORMS
from ..capabilities import version_not_implemented
from ..manager import PackageManager
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Iterator
from ..package import Package
[docs]
class Pipx(PackageManager):
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:`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.
"""
@property
def installed(self) -> Iterator[Package]:
"""Fetch installed packages.
.. code-block:: shell-session
$ pipx list --json | jq
{
"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)
if output:
for package_id, package_info in json.loads(output)["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.
.. todo::
Mimics ``Pip.outdated()`` operation. There probably is a way to factorize
it.
.. code-block:: shell-session
$ pipx runpip poetry list --no-color --format=json --outdated \
> --verbose --quiet | jq
[
{
"name": "charset-normalizer",
"version": "2.0.12",
"location": "~/.local/pipx/venvs/poetry/lib/python3.10/site-packages",
"installer": "pip",
"latest_version": "2.1.0",
"latest_filetype": "wheel"
},
{
"name": "packaging",
"version": "20.9",
"location": "~/.local/pipx/venvs/poetry/lib/python3.10/site-packages",
"installer": "pip",
"latest_version": "21.3",
"latest_filetype": "wheel"
},
{
"name": "virtualenv",
"version": "20.14.1",
"location": "~/.local/pipx/venvs/poetry/lib/python3.10/site-packages",
"installer": "pip",
"latest_version": "20.15.0",
"latest_filetype": "wheel"
}
]
"""
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,
)
if output:
for sub_package in json.loads(output):
# 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"],
)
[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)