Skip to content

Commit

Permalink
Support environments where pip is not installed
Browse files Browse the repository at this point in the history
  • Loading branch information
sbidoul committed Feb 18, 2024
1 parent 3978528 commit 5076cc4
Show file tree
Hide file tree
Showing 7 changed files with 65 additions and 29 deletions.
6 changes: 1 addition & 5 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -341,11 +341,7 @@ pip-df sync
dependencies of the project. If not
specified, ask confirmation.
--installer [default|env-pip|uv]
The installer to use. For now the default is
to use 'pip' that is installed in the target
environment. To use 'uv', pip-deepfreeze must be
installed with the 'uv' extra.
--installer [pip|uv]
--help Show this message and exit.
Expand Down
1 change: 1 addition & 0 deletions news/98.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Support environments where pip is not installed.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ requires-python = ">=3.8"
dependencies=[
"httpx",
"packaging>=23",
"pip>=22.2",
"tomli ; python_version<'3.11'",
"typer[all]>=0.3.2",
]
Expand Down
2 changes: 1 addition & 1 deletion src/pip_deepfreeze/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def sync(
),
),
installer: Installer = typer.Option(
"default",
"pip",
),
) -> None:
"""Install/update the environment to match the project requirements.
Expand Down
23 changes: 10 additions & 13 deletions src/pip_deepfreeze/pip.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
parse as parse_req_file,
)
from .req_parser import get_req_name
from .sanity import _get_env_info, get_pip_version
from .sanity import _get_env_info, get_pip_command, get_pip_version
from .utils import (
check_call,
check_output,
Expand All @@ -49,8 +49,7 @@ class PipInspectReport(TypedDict, total=False):


class Installer(str, Enum):
default = "default"
envpip = "env-pip"
pip = "pip"
uv = "uv"


Expand All @@ -59,8 +58,8 @@ def _has_uv() -> bool:
return bool(importlib.util.find_spec("uv"))


def _env_pip_install_cmd_and_env(python: str) -> Tuple[List[str], Dict[str, str]]:
return [python, "-m", "pip", "install"], {}
def _pip_install_cmd_and_env(python: str) -> Tuple[List[str], Dict[str, str]]:
return [*get_pip_command(python), "install"], {}


def _uv_install_cmd_and_env(python: str) -> Tuple[List[str], Dict[str, str]]:
Expand All @@ -74,10 +73,8 @@ def _uv_install_cmd_and_env(python: str) -> Tuple[List[str], Dict[str, str]]:
def _install_cmd_and_env(
installer: Installer, python: str
) -> Tuple[List[str], Dict[str, str]]:
if installer == Installer.default:
installer = Installer.envpip
if installer == Installer.envpip:
return _env_pip_install_cmd_and_env(python)
if installer == Installer.pip:
return _pip_install_cmd_and_env(python)
elif installer == Installer.uv:
if not _has_uv():
log_error(
Expand All @@ -94,7 +91,7 @@ def pip_upgrade_project(
constraints_filename: Path,
project_root: Path,
extras: Optional[Sequence[NormalizedName]] = None,
installer: Installer = Installer.envpip,
installer: Installer = Installer.pip,
) -> None:
"""Upgrade a project.
Expand Down Expand Up @@ -202,7 +199,7 @@ def _pip_list__env_info_json(python: str) -> InstalledDistributions:
def _pip_inspect(python: str) -> PipInspectReport:
return cast(
PipInspectReport,
json.loads(check_output([python, "-m", "pip", "--quiet", "inspect"])),
json.loads(check_output([*get_pip_command(python), "--quiet", "inspect"])),
)


Expand Down Expand Up @@ -230,7 +227,7 @@ def pip_list(python: str) -> InstalledDistributions:

def pip_freeze(python: str) -> Iterable[str]:
"""Run pip freeze."""
cmd = [python, "-m", "pip", "freeze", "--all"]
cmd = [*get_pip_command(python), "freeze", "--all"]
return check_output(cmd).splitlines()


Expand Down Expand Up @@ -312,7 +309,7 @@ def pip_uninstall(python: str, requirements: Iterable[str]) -> None:
reqs = list(requirements)
if not reqs:
return
cmd = [python, "-m", "pip", "uninstall", "--yes", *reqs]
cmd = [*get_pip_command(), "uninstall", "--yes", *reqs]
check_call(cmd)


Expand Down
59 changes: 50 additions & 9 deletions src/pip_deepfreeze/sanity.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import json
import shlex
import subprocess
import sys
from functools import lru_cache
from importlib.metadata import version
from importlib.resources import path as resource_path
from typing import Optional, Tuple, TypedDict, cast

import typer
from packaging.version import Version

from .utils import log_error, log_warning
Expand Down Expand Up @@ -34,19 +37,43 @@ def _get_env_info(python: str) -> EnvInfo:
return cast(EnvInfo, json.loads(env_info_json))


@lru_cache
def get_pip_version(python: str) -> Version:
pip_version = _get_env_info(python).get("pip_version")
# assert because we have checked the pip availability before
assert pip_version, "pip is not available"
return Version(pip_version)
if pip_version:
return Version(pip_version)
return Version(version("pip"))


@lru_cache
def get_pip_command(python: str) -> Tuple[str, ...]:
env_pip_version = _get_env_info(python).get("pip_version")
if env_pip_version:
# pip is installed in the target environment, let's use it
return (python, "-m", "pip")
if not local_pip_compatible(python):
log_error(
f"pip is not available to {python}, and the pip version. "
f"installed the pip-deepfreeze environment is not compatible with it. "
f"Please install pip in the target environment."
)
raise typer.Exit(1)
return (sys.executable, "-m", "pip", "--python", python)


@lru_cache
def get_python_version_info(python: str) -> Tuple[int, ...]:
python_version = _get_env_info(python).get("python_version")
assert python_version
return tuple(map(int, python_version.split(".", 1)))


@lru_cache
def local_pip_compatible(python: str) -> bool:
cmd = [sys.executable, "-m", "pip", "--python", python, "--version"]
return subprocess.call(cmd) == 0


def check_env(python: str) -> bool:
_get_env_info.cache_clear()
env_info = _get_env_info(python)
Expand All @@ -65,9 +92,19 @@ def check_env(python: str) -> bool:
return False
pip_version = env_info.get("pip_version")
if not env_info.get("has_pkg_resources") and (
# pip_version is None if pkg_resources is not installed for python<3.8
not pip_version
or Version(pip_version) < Version("22.2")
(
# target pip does not have pip inspect: we need pkg_resources to
# inspect with env-info-json.py
pip_version
and Version(pip_version) < Version("22.2")
)
or (
# pip not installed in target python env and local pip is not compatible
# with target python, so we'll need pkg_resources to inspect with
# env-info-json.py
not pip_version
and not local_pip_compatible(python)
)
):
setuptools_install_cmd = shlex.join(
[python, "-m", "pip", "install", "setuptools"]
Expand All @@ -84,17 +121,21 @@ def check_env(python: str) -> bool:
return False
# Testing for pip must be done after testing for pkg_resources, because
# pkg_resources is needed to obtain the pip version for python < 3.8.
if not pip_version:
if not pip_version and not local_pip_compatible(python):
log_error(f"pip is not available to {python}. Please install it.")
return False
if Version(pip_version) < Version("20.1"):
if pip_version and Version(pip_version) < Version("20.1"):
pip_install_cmd = shlex.join([python, "-m", "pip", "install", "pip>=20.1"])
log_warning(
f"pip-deepfreeze works best with pip>=20.1, "
f"in particular if you use direct URL references. "
f"You can upgrade pip it with '{pip_install_cmd}'."
)
if not env_info.get("wheel_version") and Version(pip_version) < Version("23.1"):
if (
not env_info.get("wheel_version")
and pip_version
and Version(pip_version) < Version("23.1")
):
wheel_install_cmd = shlex.join([python, "-m", "pip", "install", "wheel"])
log_warning(
f"wheel is not available to {python}. "
Expand Down
2 changes: 1 addition & 1 deletion src/pip_deepfreeze/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def sync(
uninstall_unneeded: Optional[bool],
project_root: Path,
post_sync_commands: Sequence[str] = (),
installer: Installer = Installer.envpip,
installer: Installer = Installer.pip,
) -> None:
project_name = get_project_name(python, project_root)
project_name_with_extras = make_project_name_with_extras(project_name, extras)
Expand Down

0 comments on commit 5076cc4

Please sign in to comment.