Source code for meta_package_manager.managers.pip

# 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
import re
from functools import cached_property
from typing import TYPE_CHECKING, Iterator

from extra_platforms import ALL_PLATFORMS

from meta_package_manager.base import Package, PackageManager
from meta_package_manager.capabilities import (
    search_capabilities,
    version_not_implemented,
)

if TYPE_CHECKING:
    from meta_package_manager.version import TokenizedString


[docs] class Pip(PackageManager): """We will use the Python binary to call out ``pip`` as a module instead of a CLI. This is a more robust way of managing packages: "if you're on Windows there is an added benefit to using `python -m pip` as it lets `pip` update itself." Source: https://snarky.ca/why-you-should-use-python-m-pip/ """ homepage_url = "https://pip.pypa.io" platforms = ALL_PLATFORMS requirement = "10.0.0" # Targets `python3` CLI first to allow for some systems (like macOS) to keep the # default `python` CLI tied to the Python 2.x ecosystem. cli_names = ("python3", "python") pre_args = ( "-m", "pip", # Canonical call to Python's pip module. "--no-color", # Suppress colored output. ) version_cli_options = (*pre_args, "--version") version_regex = r"pip\s+(?P<version>\S+)" """ .. code-block:: shell-session ► python -m pip --no-color --version pip 2.0.2 from /usr/local/lib/python/site-packages/pip (python 3.7) """ @cached_property def version(self) -> TokenizedString | None: """Print Python's own version before Pip's. This gives much more context to the user about the environment when a Python executable is found but Pip is not. Runs: .. code-block:: shell-session ► python --version --version Python 3.10.10 (Feb 8 2023, 05:34) [Clang 14.0.0 (clang-1400.0.29.202)] """ if self.executable: self.run_cli( ("--version", "--version"), auto_pre_cmds=False, auto_pre_args=False, auto_post_args=False, force_exec=True, ) # XXX The sentence below gets modernized with `super().version` by ruff. # See: https://beta.ruff.rs/docs/rules/#pyupgrade-up # But we're explicitly using the old syntax to bypass `cached_property`. return super(Pip, self).version # noqa: UP008 @property def installed(self) -> Iterator[Package]: """Fetch installed packages. .. code-block:: shell-session ► python -m pip --no-color list --format=json --verbose --quiet \ > | jq [ { "version": "1.3", "name": "backports.functools-lru-cache", "location": "/usr/local/lib/python3.7/site-packages", "installer": "pip" }, { "version": "0.9999999", "name": "html5lib", "location": "/usr/local/lib/python3.7/site-packages", "installer": "pip" }, { "name": "setuptools", "version": "46.0.0", "location": "/usr/local/lib/python3.7/site-packages", "installer": "" }, { "version": "2.8", "name": "Jinja2", "location": "/usr/local/lib/python3.7/site-packages", "installer": "" }, (...) ] """ # --quiet is required here to silence warning and error messages # mangling the JSON content. output = self.run_cli("list", "--format=json", "--verbose", "--quiet") if output: for package in json.loads(output): yield self.package( id=package["name"], installed_version=package["version"], ) @property def outdated(self) -> Iterator[Package]: """Fetch outdated packages. .. code-block:: shell-session ► python -m pip --no-color list --format=json --outdated \ > --verbose --quiet | jq [ { "latest_filetype": "wheel", "version": "0.7.9", "name": "alabaster", "latest_version": "0.7.10", "location": "/usr/local/lib/python3.7/site-packages", "installer": "pip" }, { "latest_filetype": "wheel", "version": "0.9999999", "name": "html5lib", "latest_version": "0.999999999", "location": "/usr/local/lib/python3.7/site-packages", "installer": "pip" }, { "latest_filetype": "wheel", "version": "2.8", "name": "Jinja2", "latest_version": "2.9.5", "location": "/usr/local/lib/python3.7/site-packages", "installer": "pip" }, { "latest_filetype": "wheel", "version": "0.5.3", "name": "mccabe", "latest_version": "0.6.1", "location": "/usr/local/lib/python3.7/site-packages", "installer": "pip" }, { "latest_filetype": "wheel", "version": "2.2.0", "name": "pycodestyle", "latest_version": "2.3.1", "location": "/usr/local/lib/python3.7/site-packages", "installer": "pip" }, { "latest_filetype": "wheel", "version": "2.1.3", "name": "Pygments", "latest_version": "2.2.0", "location": "/usr/local/lib/python3.7/site-packages", "installer": "" } ] """ # --quiet is required here to silence warning and error messages # mangling the JSON content. output = self.run_cli( "list", "--format=json", "--outdated", "--verbose", "--quiet", ) if output: for package in json.loads(output): yield self.package( id=package["name"], installed_version=package["version"], latest_version=package["latest_version"], )
[docs] @search_capabilities(extended_support=False, exact_support=False) def search_xxx_disabled( self, query: str, extended: bool, exact: bool, ) -> Iterator[Package]: """Fetch matching packages. .. warning:: That function was previously named ``search`` but has been renamed to make it invisible from the ``mpm`` framework, disabling search feature altogether for ``pip``. This had to be done has Pip's maintainers disabled the server-side API because of unmanageable high-load. See: https://github.com/pypa/pip/issues/5216#issuecomment-744605466 .. caution:: Search is extended by default. So we returns the best subset of results and let :py:meth:`meta_package_manager.base.PackageManager.refiltered_search` refine them .. code-block:: shell-session ► python -m pip --no-color search abc ABC (0.0.0) - UNKNOWN micropython-abc (0.0.1) - Dummy abc module for MicroPython abc1 (1.2.0) - a list about my think abcd (0.3.0) - AeroGear Build Cli for Digger abcyui (1.0.0) - Sorry ,This is practice! astroabc (1.4.2) - A Python implementation of an Approximate Bayesian Computation Sequential Monte Carlo (ABC SMC) sampler for parameter estimation. collective.js.abcjs (1.10) - UNKNOWN cosmo (1.0.5) - Python ABC sampler """ output = self.run_cli("search", query) regexp = re.compile( r""" ^(?P<package_id>\S+) # A string with a char at least. \ # A space. \((?P<version>.*?)\) # Content between parenthesis. [ ]+- # A space or more, then a dash. (?P<description> # Start of the multi-line desc group. (?:[ ]+.*\s)+ # Lines of strings prefixed by spaces. ) """, re.MULTILINE | re.VERBOSE, ) for package_id, version, description in regexp.findall(output): yield self.package( id=package_id, description=description, latest_version=version, )
@version_not_implemented def install(self, package_id: str, version: str | None = None) -> str: """Install one package. .. code-block:: shell-session ► python -m pip --no-color install arrow Collecting arrow Using cached arrow-1.1.1-py3-none-any.whl (60 kB) Collecting python-dateutil>=2.7.0 Using cached python_dateutil-2.8.2-py2.py3-none-any.whl (247 kB) Requirement already satisfied: six>=1.5 in python3.9/site-packages (1.16.0) Installing collected packages: python-dateutil, arrow Successfully installed arrow-1.1.1 python-dateutil-2.8.2 """ return self.run_cli("install", package_id) @version_not_implemented def upgrade_one_cli( self, package_id: str, version: str | None = None, ) -> tuple[str, ...]: """Generates the CLI to upgrade the package provided as parameter. .. code-block:: shell-session ► python -m pip --no-color install --user --upgrade six Collecting six Using cached six-1.15.0-py2.py3-none-any.whl (10 kB) Installing collected packages: six Attempting uninstall: six Found existing installation: six 1.14.0 Uninstalling six-1.14.0: Successfully uninstalled six-1.14.0 Successfully installed six-1.15.0 .. note:: Pip lacks support of a proper full upgrade command. Raising an error let the parent class upgrade packages one by one. See: https://github.com/pypa/pip/issues/59 """ return self.build_cli("install", "--user", "--upgrade", package_id)
[docs] def remove(self, package_id: str) -> str: """Remove one package. .. code-block:: shell-session ► python -m pip --no-color uninstall --yes arrow """ return self.run_cli("uninstall", "--yes", package_id)