Skip to content

Commit 708ce02

Browse files
committed
Drop pip from critical packages in project venv
An initial attempt at using the embedded pip wheel from virtualenv package directly. This avoids the need for pip to be a critical package in the project's virtual environment. Setuptools is still maintained as this is required form edgeases where we still do `setuptools` editable installs (git, path dependencies).
1 parent 0d48fb6 commit 708ce02

File tree

8 files changed

+52
-39
lines changed

8 files changed

+52
-39
lines changed

poetry/inspection/info.py

+2-5
Original file line numberDiff line numberDiff line change
@@ -435,17 +435,14 @@ def _pep517_metadata(cls, path): # type (Path) -> PackageInfo
435435
with temporary_directory() as tmp_dir:
436436
# TODO: cache PEP 517 build environment corresponding to each project venv
437437
venv_dir = Path(tmp_dir) / ".venv"
438-
EnvManager.build_venv(venv_dir.as_posix())
438+
EnvManager.build_venv(venv_dir.as_posix(), with_pip=True)
439439
venv = VirtualEnv(venv_dir, venv_dir)
440440

441441
dest_dir = Path(tmp_dir) / "dist"
442442
dest_dir.mkdir()
443443

444444
try:
445-
venv.run(
446-
"python",
447-
"-m",
448-
"pip",
445+
venv.run_pip(
449446
"install",
450447
"--disable-pip-version-check",
451448
"--ignore-installed",

poetry/puzzle/provider.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ def _formatter_elapsed(self):
5252

5353
class Provider:
5454

55-
UNSAFE_PACKAGES = {"setuptools", "distribute", "pip"}
55+
UNSAFE_PACKAGES = {"setuptools"}
5656

5757
def __init__(
5858
self, package, pool, io, env=None

poetry/utils/env.py

+32-16
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
from poetry.utils._compat import list_to_shell_command
4141
from poetry.utils._compat import subprocess
4242
from poetry.utils.toml_file import TomlFile
43+
from virtualenv.seed.wheels.embed import get_embed_wheel
4344

4445

4546
GET_ENVIRONMENT_INFO = """\
@@ -679,19 +680,25 @@ def create_venv(
679680

680681
@classmethod
681682
def build_venv(
682-
cls, path, executable=None
683-
): # type: (Union[Path,str], Optional[Union[str, Path]]) -> virtualenv.run.session.Session
683+
cls, path, executable=None, with_pip=False
684+
): # type: (Union[Path,str], Optional[Union[str, Path]], bool) -> virtualenv.run.session.Session
684685
if isinstance(executable, Path):
685686
executable = executable.resolve().as_posix()
686-
return virtualenv.cli_run(
687-
[
688-
"--no-download",
689-
"--no-periodic-update",
690-
"--python",
691-
executable or sys.executable,
692-
str(path),
693-
]
694-
)
687+
688+
opts = [
689+
"--no-download",
690+
"--no-periodic-update",
691+
"--python",
692+
executable or sys.executable,
693+
]
694+
695+
if not with_pip:
696+
# we cannot drop setuptools yet because we do editable installs (git, path) in project envs
697+
opts.extend(["--no-pip", "--no-wheel"])
698+
699+
opts.append(str(path))
700+
701+
return virtualenv.cli_run(opts)
695702

696703
@classmethod
697704
def remove_venv(cls, path): # type: (Union[Path,str]) -> None
@@ -787,12 +794,21 @@ def marker_env(self):
787794

788795
return self._marker_env
789796

797+
def get_embedded_wheel(self, distribution):
798+
return get_embed_wheel(
799+
distribution, "{}.{}".format(self.version_info[0], self.version_info[1])
800+
).path
801+
790802
@property
791803
def pip(self): # type: () -> str
792804
"""
793805
Path to current pip executable
794806
"""
795-
return self._bin("pip")
807+
# we do not use as_posix() here due to issues with windows pathlib2 implementation
808+
path = self._bin("pip")
809+
if not Path(path).exists():
810+
return str(self.get_embedded_wheel("pip").joinpath("pip"))
811+
return path
796812

797813
@property
798814
def platform(self): # type: () -> str
@@ -1010,7 +1026,7 @@ def get_python_implementation(self): # type: () -> str
10101026
def get_pip_command(self): # type: () -> List[str]
10111027
# If we're not in a venv, assume the interpreter we're running on
10121028
# has a pip and use that
1013-
return [sys.executable, "-m", "pip"]
1029+
return [sys.executable, self.pip]
10141030

10151031
def get_paths(self): # type: () -> Dict[str, str]
10161032
# We can't use sysconfig.get_paths() because
@@ -1112,7 +1128,7 @@ def get_python_implementation(self): # type: () -> str
11121128
def get_pip_command(self): # type: () -> List[str]
11131129
# We're in a virtualenv that is known to be sane,
11141130
# so assume that we have a functional pip
1115-
return [self._bin("pip")]
1131+
return [self._bin("python"), self.pip]
11161132

11171133
def get_supported_tags(self): # type: () -> List[Tag]
11181134
file_path = Path(packaging.tags.__file__)
@@ -1167,7 +1183,7 @@ def is_venv(self): # type: () -> bool
11671183

11681184
def is_sane(self):
11691185
# A virtualenv is considered sane if both "python" and "pip" exist.
1170-
return os.path.exists(self._bin("python")) and os.path.exists(self._bin("pip"))
1186+
return os.path.exists(self._bin("python"))
11711187

11721188
def _run(self, cmd, **kwargs):
11731189
with self.temp_environ():
@@ -1217,7 +1233,7 @@ def __init__(self, path=None, base=None, execute=False):
12171233
self.executed = []
12181234

12191235
def get_pip_command(self): # type: () -> List[str]
1220-
return [self._bin("python"), "-m", "pip"]
1236+
return [self._bin("python"), self.pip]
12211237

12221238
def _run(self, cmd, **kwargs):
12231239
self.executed.append(cmd)

tests/inspection/test_info.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ def test_info_setup_simple(mocker, demo_setup):
168168
def test_info_setup_simple_py2(mocker, demo_setup):
169169
spy = mocker.spy(VirtualEnv, "run")
170170
info = PackageInfo.from_directory(demo_setup, allow_build=True)
171-
assert spy.call_count == 2
171+
assert spy.call_count == 1
172172
demo_check_info(info, requires_dist={"package"})
173173

174174

tests/installation/test_installer.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -362,15 +362,19 @@ def test_run_install_remove_untracked(installer, locker, repo, package, installe
362362
package_b = get_package("b", "1.1")
363363
package_c = get_package("c", "1.2")
364364
package_pip = get_package("pip", "20.0.0")
365+
package_setuptools = get_package("setuptools", "20.0.0")
366+
365367
repo.add_package(package_a)
366368
repo.add_package(package_b)
367369
repo.add_package(package_c)
368370
repo.add_package(package_pip)
371+
repo.add_package(package_setuptools)
369372

370373
installed.add_package(package_a)
371374
installed.add_package(package_b)
372375
installed.add_package(package_c)
373-
installed.add_package(package_pip) # Always required and never removed.
376+
installed.add_package(package_pip)
377+
installed.add_package(package_setuptools) # Always required and never removed.
374378
installed.add_package(package) # Root package never removed.
375379

376380
package.add_dependency("A", "~1.0")
@@ -380,8 +384,8 @@ def test_run_install_remove_untracked(installer, locker, repo, package, installe
380384

381385
assert 0 == installer.executor.installations_count
382386
assert 0 == installer.executor.updates_count
383-
assert 2 == installer.executor.removals_count
384-
assert {"b", "c"} == set(r.name for r in installer.executor.removals)
387+
assert 3 == installer.executor.removals_count
388+
assert {"b", "c", "pip"} == set(r.name for r in installer.executor.removals)
385389

386390

387391
def test_run_whitelist_add(installer, locker, repo, package):

tests/installation/test_installer_old.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -323,15 +323,19 @@ def test_run_install_remove_untracked(installer, locker, repo, package, installe
323323
package_b = get_package("b", "1.1")
324324
package_c = get_package("c", "1.2")
325325
package_pip = get_package("pip", "20.0.0")
326+
package_setuptools = get_package("setuptools", "20.0.0")
327+
326328
repo.add_package(package_a)
327329
repo.add_package(package_b)
328330
repo.add_package(package_c)
329331
repo.add_package(package_pip)
332+
repo.add_package(package_setuptools)
330333

331334
installed.add_package(package_a)
332335
installed.add_package(package_b)
333336
installed.add_package(package_c)
334-
installed.add_package(package_pip) # Always required and never removed.
337+
installed.add_package(package_pip)
338+
installed.add_package(package_setuptools) # Always required and never removed.
335339
installed.add_package(package) # Root package never removed.
336340

337341
package.add_dependency("A", "~1.0")
@@ -346,7 +350,7 @@ def test_run_install_remove_untracked(installer, locker, repo, package, installe
346350
assert len(updates) == 0
347351

348352
removals = installer.installer.removals
349-
assert set(r.name for r in removals) == {"b", "c"}
353+
assert set(r.name for r in removals) == {"b", "c", "pip"}
350354

351355

352356
def test_run_whitelist_add(installer, locker, repo, package):

tests/masonry/builders/test_editable_builder.py

+2-10
Original file line numberDiff line numberDiff line change
@@ -170,17 +170,9 @@ def test_builder_falls_back_on_setup_and_pip_for_packages_with_build_scripts(
170170
builder = EditableBuilder(extended_poetry, env, NullIO())
171171

172172
builder.build()
173-
174173
assert [
175-
[
176-
"python",
177-
"-m",
178-
"pip",
179-
"install",
180-
"-e",
181-
str(extended_poetry.file.parent),
182-
"--no-deps",
183-
]
174+
env.get_pip_command()
175+
+ ["install", "-e", str(extended_poetry.file.parent), "--no-deps"]
184176
] == env.executed
185177

186178

tests/puzzle/test_solver.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1894,7 +1894,7 @@ def test_solver_remove_untracked_keeps_critical_package(
18941894
package, pool, installed, locked, io
18951895
):
18961896
solver = Solver(package, pool, installed, locked, io, remove_untracked=True)
1897-
package_pip = get_package("pip", "1.0")
1897+
package_pip = get_package("setuptools", "1.0")
18981898
installed.add_package(package_pip)
18991899

19001900
ops = solver.solve()

0 commit comments

Comments
 (0)