Skip to content

Commit 98015ce

Browse files
authored
Merge pull request #137 from sbidoul/support-pip-less-venv
Support environments where pip is not installed
2 parents 3300585 + 7b25c9a commit 98015ce

File tree

8 files changed

+72
-29
lines changed

8 files changed

+72
-29
lines changed

README.rst

+1-5
Original file line numberDiff line numberDiff line change
@@ -341,11 +341,7 @@ pip-df sync
341341
dependencies of the project. If not
342342
specified, ask confirmation.
343343
344-
--installer [default|env-pip|uv]
345-
The installer to use. For now the default is
346-
to use 'pip' that is installed in the target
347-
environment. To use 'uv', pip-deepfreeze must be
348-
installed with the 'uv' extra.
344+
--installer [pip|uv]
349345
350346
--help Show this message and exit.
351347

news/98.feature

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Support environments where pip is not installed.

pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ requires-python = ">=3.8"
2727
dependencies=[
2828
"httpx",
2929
"packaging>=23",
30+
"pip>=22.2",
3031
"tomli ; python_version<'3.11'",
3132
"typer[all]>=0.3.2",
3233
]

src/pip_deepfreeze/__main__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def sync(
7070
),
7171
),
7272
installer: Installer = typer.Option(
73-
"default",
73+
"pip",
7474
),
7575
) -> None:
7676
"""Install/update the environment to match the project requirements.

src/pip_deepfreeze/pip.py

+10-13
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
parse as parse_req_file,
3030
)
3131
from .req_parser import get_req_name
32-
from .sanity import _get_env_info, get_pip_version
32+
from .sanity import _get_env_info, get_pip_command, get_pip_version
3333
from .utils import (
3434
check_call,
3535
check_output,
@@ -49,8 +49,7 @@ class PipInspectReport(TypedDict, total=False):
4949

5050

5151
class Installer(str, Enum):
52-
default = "default"
53-
envpip = "env-pip"
52+
pip = "pip"
5453
uv = "uv"
5554

5655

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

6160

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

6564

6665
def _uv_install_cmd_and_env(python: str) -> Tuple[List[str], Dict[str, str]]:
@@ -74,10 +73,8 @@ def _uv_install_cmd_and_env(python: str) -> Tuple[List[str], Dict[str, str]]:
7473
def _install_cmd_and_env(
7574
installer: Installer, python: str
7675
) -> Tuple[List[str], Dict[str, str]]:
77-
if installer == Installer.default:
78-
installer = Installer.envpip
79-
if installer == Installer.envpip:
80-
return _env_pip_install_cmd_and_env(python)
76+
if installer == Installer.pip:
77+
return _pip_install_cmd_and_env(python)
8178
elif installer == Installer.uv:
8279
if not _has_uv():
8380
log_error(
@@ -94,7 +91,7 @@ def pip_upgrade_project(
9491
constraints_filename: Path,
9592
project_root: Path,
9693
extras: Optional[Sequence[NormalizedName]] = None,
97-
installer: Installer = Installer.envpip,
94+
installer: Installer = Installer.pip,
9895
) -> None:
9996
"""Upgrade a project.
10097
@@ -202,7 +199,7 @@ def _pip_list__env_info_json(python: str) -> InstalledDistributions:
202199
def _pip_inspect(python: str) -> PipInspectReport:
203200
return cast(
204201
PipInspectReport,
205-
json.loads(check_output([python, "-m", "pip", "--quiet", "inspect"])),
202+
json.loads(check_output([*get_pip_command(python), "--quiet", "inspect"])),
206203
)
207204

208205

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

231228
def pip_freeze(python: str) -> Iterable[str]:
232229
"""Run pip freeze."""
233-
cmd = [python, "-m", "pip", "freeze", "--all"]
230+
cmd = [*get_pip_command(python), "freeze", "--all"]
234231
return check_output(cmd).splitlines()
235232

236233

@@ -312,7 +309,7 @@ def pip_uninstall(python: str, requirements: Iterable[str]) -> None:
312309
reqs = list(requirements)
313310
if not reqs:
314311
return
315-
cmd = [python, "-m", "pip", "uninstall", "--yes", *reqs]
312+
cmd = [*get_pip_command(python), "uninstall", "--yes", *reqs]
316313
check_call(cmd)
317314

318315

src/pip_deepfreeze/sanity.py

+50-9
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import json
22
import shlex
33
import subprocess
4+
import sys
45
from functools import lru_cache
6+
from importlib.metadata import version
57
from importlib.resources import path as resource_path
68
from typing import Optional, Tuple, TypedDict, cast
79

10+
import typer
811
from packaging.version import Version
912

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

3639

40+
@lru_cache
3741
def get_pip_version(python: str) -> Version:
3842
pip_version = _get_env_info(python).get("pip_version")
39-
# assert because we have checked the pip availability before
40-
assert pip_version, "pip is not available"
41-
return Version(pip_version)
43+
if pip_version:
44+
return Version(pip_version)
45+
return Version(version("pip"))
4246

4347

48+
@lru_cache
49+
def get_pip_command(python: str) -> Tuple[str, ...]:
50+
env_pip_version = _get_env_info(python).get("pip_version")
51+
if env_pip_version:
52+
# pip is installed in the target environment, let's use it
53+
return (python, "-m", "pip")
54+
if not local_pip_compatible(python):
55+
log_error(
56+
f"pip is not available to {python}, and the pip version. "
57+
f"installed the pip-deepfreeze environment is not compatible with it. "
58+
f"Please install pip in the target environment."
59+
)
60+
raise typer.Exit(1)
61+
return (sys.executable, "-m", "pip", "--python", python)
62+
63+
64+
@lru_cache
4465
def get_python_version_info(python: str) -> Tuple[int, ...]:
4566
python_version = _get_env_info(python).get("python_version")
4667
assert python_version
4768
return tuple(map(int, python_version.split(".", 1)))
4869

4970

71+
@lru_cache
72+
def local_pip_compatible(python: str) -> bool:
73+
cmd = [sys.executable, "-m", "pip", "--python", python, "--version"]
74+
return subprocess.call(cmd) == 0
75+
76+
5077
def check_env(python: str) -> bool:
5178
_get_env_info.cache_clear()
5279
env_info = _get_env_info(python)
@@ -65,9 +92,19 @@ def check_env(python: str) -> bool:
6592
return False
6693
pip_version = env_info.get("pip_version")
6794
if not env_info.get("has_pkg_resources") and (
68-
# pip_version is None if pkg_resources is not installed for python<3.8
69-
not pip_version
70-
or Version(pip_version) < Version("22.2")
95+
(
96+
# target pip does not have pip inspect: we need pkg_resources to
97+
# inspect with env-info-json.py
98+
pip_version
99+
and Version(pip_version) < Version("22.2")
100+
)
101+
or (
102+
# pip not installed in target python env and local pip is not compatible
103+
# with target python, so we'll need pkg_resources to inspect with
104+
# env-info-json.py
105+
not pip_version
106+
and not local_pip_compatible(python)
107+
)
71108
):
72109
setuptools_install_cmd = shlex.join(
73110
[python, "-m", "pip", "install", "setuptools"]
@@ -84,17 +121,21 @@ def check_env(python: str) -> bool:
84121
return False
85122
# Testing for pip must be done after testing for pkg_resources, because
86123
# pkg_resources is needed to obtain the pip version for python < 3.8.
87-
if not pip_version:
124+
if not pip_version and not local_pip_compatible(python):
88125
log_error(f"pip is not available to {python}. Please install it.")
89126
return False
90-
if Version(pip_version) < Version("20.1"):
127+
if pip_version and Version(pip_version) < Version("20.1"):
91128
pip_install_cmd = shlex.join([python, "-m", "pip", "install", "pip>=20.1"])
92129
log_warning(
93130
f"pip-deepfreeze works best with pip>=20.1, "
94131
f"in particular if you use direct URL references. "
95132
f"You can upgrade pip it with '{pip_install_cmd}'."
96133
)
97-
if not env_info.get("wheel_version") and Version(pip_version) < Version("23.1"):
134+
if (
135+
not env_info.get("wheel_version")
136+
and pip_version
137+
and Version(pip_version) < Version("23.1")
138+
):
98139
wheel_install_cmd = shlex.join([python, "-m", "pip", "install", "wheel"])
99140
log_warning(
100141
f"wheel is not available to {python}. "

src/pip_deepfreeze/sync.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def sync(
4444
uninstall_unneeded: Optional[bool],
4545
project_root: Path,
4646
post_sync_commands: Sequence[str] = (),
47-
installer: Installer = Installer.envpip,
47+
installer: Installer = Installer.pip,
4848
) -> None:
4949
project_name = get_project_name(python, project_root)
5050
project_name_with_extras = make_project_name_with_extras(project_name, extras)

tests/test_sanity.py

+7
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,19 @@
22
import sys
33
from pathlib import Path
44

5+
import pytest
56
from typer.testing import CliRunner
67

78
from pip_deepfreeze.__main__ import app
89
from pip_deepfreeze.sanity import check_env, get_python_version_info
910

1011

12+
@pytest.mark.xfail(
13+
reason=(
14+
"check_env succeeds because the pip inversion installed with pip-deepfreeze "
15+
"is compatible with the target environment."
16+
)
17+
)
1118
def test_sanity_pip(virtualenv_python, capsys):
1219
assert check_env(virtualenv_python)
1320
subprocess.check_call([virtualenv_python, "-m", "pip", "uninstall", "-y", "pip"])

0 commit comments

Comments
 (0)