Skip to content

Commit 57fa499

Browse files
radoeringabn
authored andcommitted
install: make --sync compatible with virtualenvs.options.system-site-packages = true
Without this fix, we try to uninstall untracked system site packages.
1 parent b7cd0b0 commit 57fa499

File tree

7 files changed

+157
-20
lines changed

7 files changed

+157
-20
lines changed

src/poetry/installation/installer.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def __init__(
3939
locker: Locker,
4040
pool: RepositoryPool,
4141
config: Config,
42-
installed: Repository | None = None,
42+
installed: InstalledRepository | None = None,
4343
executor: Executor | None = None,
4444
disable_cache: bool = False,
4545
) -> None:
@@ -332,6 +332,9 @@ def _do_install(self) -> int:
332332
synchronize=self._requires_synchronization,
333333
skip_directory=self._skip_directory,
334334
extras=set(self._extras),
335+
system_site_packages={
336+
p.name for p in self._installed_repository.system_site_packages
337+
},
335338
)
336339

337340
# Validate the dependencies

src/poetry/plugins/plugin_manager.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
from poetry.packages import Locker
2020
from poetry.plugins.application_plugin import ApplicationPlugin
2121
from poetry.plugins.plugin import Plugin
22-
from poetry.repositories import Repository
2322
from poetry.repositories.installed_repository import InstalledRepository
2423
from poetry.toml import TOMLFile
2524
from poetry.utils._compat import metadata
@@ -288,7 +287,7 @@ def _install(
288287
self._poetry.config,
289288
# Build installed repository from locked packages so that plugins
290289
# that may be overwritten are not included.
291-
Repository("poetry-repo", locked_packages),
290+
InstalledRepository(locked_packages),
292291
)
293292
installer.update(True)
294293

src/poetry/puzzle/transaction.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,20 @@ def get_solved_packages(self) -> dict[Package, TransitivePackageInfo]:
4242

4343
def calculate_operations(
4444
self,
45+
*,
4546
with_uninstalls: bool = True,
4647
synchronize: bool = False,
47-
*,
4848
skip_directory: bool = False,
4949
extras: set[NormalizedName] | None = None,
50+
system_site_packages: set[NormalizedName] | None = None,
5051
) -> list[Operation]:
5152
from poetry.installation.operations import Install
5253
from poetry.installation.operations import Uninstall
5354
from poetry.installation.operations import Update
5455

56+
if not system_site_packages:
57+
system_site_packages = set()
58+
5559
operations: list[Operation] = []
5660

5761
extra_packages: set[NormalizedName] = set()
@@ -105,7 +109,8 @@ def calculate_operations(
105109
# Extras that were not requested are always uninstalled.
106110
if is_unsolicited_extra:
107111
uninstalls.add(installed_package.name)
108-
operations.append(Uninstall(installed_package))
112+
if installed_package.name not in system_site_packages:
113+
operations.append(Uninstall(installed_package))
109114

110115
# We have to perform an update if the version or another
111116
# attribute of the package has changed (source type, url, ref, ...).
@@ -156,7 +161,8 @@ def calculate_operations(
156161
for installed_package in self._installed_packages:
157162
if installed_package.name == current_package.name:
158163
uninstalls.add(installed_package.name)
159-
operations.append(Uninstall(installed_package))
164+
if installed_package.name not in system_site_packages:
165+
operations.append(Uninstall(installed_package))
160166

161167
if synchronize:
162168
# We preserve pip when not managed by poetry, this is done to avoid
@@ -178,7 +184,8 @@ def calculate_operations(
178184

179185
if installed_package.name not in relevant_result_packages:
180186
uninstalls.add(installed_package.name)
181-
operations.append(Uninstall(installed_package))
187+
if installed_package.name not in system_site_packages:
188+
operations.append(Uninstall(installed_package))
182189

183190
return sorted(
184191
operations,

src/poetry/repositories/installed_repository.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,26 @@
1616
from poetry.repositories.repository import Repository
1717
from poetry.utils._compat import getencoding
1818
from poetry.utils._compat import metadata
19+
from poetry.utils.env import VirtualEnv
1920

2021

2122
if TYPE_CHECKING:
22-
from poetry.utils.env import Env
23+
from collections.abc import Sequence
2324

25+
from poetry.utils.env import Env
2426

2527
logger = logging.getLogger(__name__)
2628

2729

2830
class InstalledRepository(Repository):
29-
def __init__(self) -> None:
30-
super().__init__("poetry-installed")
31+
def __init__(self, packages: Sequence[Package] | None = None) -> None:
32+
super().__init__("poetry-installed", packages)
33+
self.system_site_packages: list[Package] = []
34+
35+
def add_package(self, package: Package, *, is_system_site: bool = False) -> None:
36+
super().add_package(package)
37+
if is_system_site:
38+
self.system_site_packages.append(package)
3139

3240
@classmethod
3341
def get_package_paths(cls, env: Env, name: str) -> set[Path]:
@@ -242,6 +250,12 @@ def load(cls, env: Env, with_dependencies: bool = False) -> InstalledRepository:
242250
seen = set()
243251
skipped = set()
244252

253+
base_env = (
254+
env.parent_env
255+
if isinstance(env, VirtualEnv) and env.includes_system_site_packages
256+
else None
257+
)
258+
245259
for entry in env.sys_path:
246260
if not entry.strip():
247261
logger.debug(
@@ -283,6 +297,14 @@ def load(cls, env: Env, with_dependencies: bool = False) -> InstalledRepository:
283297
package.add_dependency(dep)
284298

285299
seen.add(package.name)
286-
repo.add_package(package)
300+
repo.add_package(
301+
package,
302+
is_system_site=bool(
303+
base_env
304+
and base_env.is_path_relative_to_lib(
305+
Path(str(distribution._path)) # type: ignore[attr-defined]
306+
)
307+
),
308+
)
287309

288310
return repo

tests/conftest.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@
2929
from poetry.factory import Factory
3030
from poetry.layouts import layout
3131
from poetry.packages.direct_origin import _get_package_from_git
32-
from poetry.repositories import Repository
3332
from poetry.repositories import RepositoryPool
33+
from poetry.repositories.installed_repository import InstalledRepository
3434
from poetry.utils.cache import ArtifactCache
3535
from poetry.utils.env import EnvManager
3636
from poetry.utils.env import SystemEnv
@@ -379,8 +379,8 @@ def tmp_venv(tmp_path: Path) -> Iterator[VirtualEnv]:
379379

380380

381381
@pytest.fixture
382-
def installed() -> Repository:
383-
return Repository("installed")
382+
def installed() -> InstalledRepository:
383+
return InstalledRepository()
384384

385385

386386
@pytest.fixture(scope="session")
@@ -412,7 +412,7 @@ def project_factory(
412412
tmp_path: Path,
413413
config: Config,
414414
repo: TestRepository,
415-
installed: Repository,
415+
installed: InstalledRepository,
416416
default_python: str,
417417
load_required_fixtures: None,
418418
) -> ProjectFactory:

tests/puzzle/test_transaction.py

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,59 @@ def test_it_should_remove_installed_packages_if_required() -> None:
135135
)
136136

137137

138+
def test_it_should_not_remove_system_site_packages() -> None:
139+
"""
140+
Different types of uninstalls:
141+
- c: tracked but not required
142+
- e: not tracked
143+
- f: root extra that is not requested
144+
"""
145+
extra_name = canonicalize_name("foo")
146+
package = ProjectPackage("root", "1.0")
147+
dep_f = Dependency("f", "1", optional=True)
148+
dep_f._in_extras = [extra_name]
149+
package.add_dependency(dep_f)
150+
package.extras = {extra_name: [dep_f]}
151+
opt_f = Package("f", "6.0.0")
152+
opt_f.optional = True
153+
transaction = Transaction(
154+
[Package("a", "1.0.0"), Package("b", "2.0.0"), Package("c", "3.0.0")],
155+
{
156+
Package("a", "1.0.0"): get_transitive_info(1),
157+
Package("b", "2.1.0"): get_transitive_info(2),
158+
Package("d", "4.0.0"): get_transitive_info(0),
159+
opt_f: get_transitive_info(0),
160+
},
161+
installed_packages=[
162+
Package("a", "1.0.0"),
163+
Package("b", "2.0.0"),
164+
Package("c", "3.0.0"),
165+
Package("e", "5.0.0"),
166+
Package("f", "6.0.0"),
167+
],
168+
root_package=package,
169+
)
170+
171+
check_operations(
172+
transaction.calculate_operations(
173+
synchronize=True,
174+
extras=set(),
175+
system_site_packages={
176+
canonicalize_name(name) for name in ("a", "b", "c", "e", "f")
177+
},
178+
),
179+
[
180+
{
181+
"job": "update",
182+
"from": Package("b", "2.0.0"),
183+
"to": Package("b", "2.1.0"),
184+
},
185+
{"job": "install", "package": Package("a", "1.0.0"), "skipped": True},
186+
{"job": "install", "package": Package("d", "4.0.0")},
187+
],
188+
)
189+
190+
138191
def test_it_should_not_remove_installed_packages_that_are_in_result() -> None:
139192
transaction = Transaction(
140193
[],
@@ -236,7 +289,9 @@ def test_calculate_operations_with_groups(
236289
for name in sorted({"a", "b", "c"}.difference(expected), reverse=True):
237290
expected_ops.insert(0, {"job": "remove", "package": Package(name, "1")})
238291

239-
check_operations(transaction.calculate_operations(sync), expected_ops)
292+
check_operations(
293+
transaction.calculate_operations(with_uninstalls=sync), expected_ops
294+
)
240295

241296

242297
@pytest.mark.parametrize(
@@ -273,7 +328,9 @@ def test_calculate_operations_with_markers(
273328
for name in sorted({"a", "b"}.difference(expected), reverse=True):
274329
expected_ops.insert(0, {"job": "remove", "package": Package(name, "1")})
275330

276-
check_operations(transaction.calculate_operations(sync), expected_ops)
331+
check_operations(
332+
transaction.calculate_operations(with_uninstalls=sync), expected_ops
333+
)
277334

278335

279336
@pytest.mark.parametrize(
@@ -366,8 +423,8 @@ def test_calculate_operations_extras(
366423

367424
check_operations(
368425
transaction.calculate_operations(
369-
with_uninstalls,
370-
sync,
426+
with_uninstalls=with_uninstalls,
427+
synchronize=sync,
371428
extras={extra_name} if extras else set(),
372429
),
373430
ops,

tests/repositories/test_installed_repository.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import os
4+
import platform
45
import shutil
56
import zipfile
67

@@ -415,6 +416,48 @@ def test_load_pep_610_compliant_editable_directory_packages(
415416
assert package.develop
416417

417418

419+
@pytest.mark.parametrize("with_system_site_packages", [False, True])
420+
def test_system_site_packages(
421+
tmp_path: Path,
422+
mocker: MockerFixture,
423+
poetry: Poetry,
424+
site_purelib: Path,
425+
with_system_site_packages: bool,
426+
) -> None:
427+
venv_path = tmp_path / "venv"
428+
site_path = tmp_path / "site"
429+
cleo_dist_info = "cleo-0.7.6.dist-info"
430+
shutil.copytree(site_purelib / cleo_dist_info, site_path / cleo_dist_info)
431+
432+
EnvManager(poetry).build_venv(
433+
path=venv_path, flags={"system-site-packages": with_system_site_packages}
434+
)
435+
env = VirtualEnv(venv_path)
436+
standard_dist_info = "standard-1.2.3.dist-info"
437+
shutil.copytree(site_purelib / standard_dist_info, env.purelib / standard_dist_info)
438+
orig_sys_path = env.sys_path
439+
if with_system_site_packages:
440+
mocker.patch(
441+
"poetry.utils.env.virtual_env.VirtualEnv.sys_path",
442+
orig_sys_path[:-1] + [str(site_path)],
443+
)
444+
mocker.patch(
445+
"poetry.utils.env.generic_env.GenericEnv.get_paths",
446+
return_value={"purelib": str(site_path)},
447+
)
448+
449+
installed_repository = InstalledRepository.load(env)
450+
451+
expected_system_site_packages = {"cleo"} if with_system_site_packages else set()
452+
expected_packages = {"standard"} | expected_system_site_packages
453+
if platform.system() == "FreeBSD":
454+
expected_packages.add("sqlite3")
455+
assert {p.name for p in installed_repository.packages} == expected_packages
456+
assert {
457+
p.name for p in installed_repository.system_site_packages
458+
} == expected_system_site_packages
459+
460+
418461
def test_system_site_packages_source_type(
419462
tmp_path: Path, mocker: MockerFixture, poetry: Poetry, site_purelib: Path
420463
) -> None:
@@ -427,12 +470,17 @@ def test_system_site_packages_source_type(
427470
for dist_info in {"cleo-0.7.6.dist-info", "directory_pep_610-1.2.3.dist-info"}:
428471
shutil.copytree(site_purelib / dist_info, site_path / dist_info)
429472
mocker.patch("poetry.utils.env.virtual_env.VirtualEnv.sys_path", [str(site_path)])
430-
mocker.patch("site.getsitepackages", return_value=[str(site_path)])
473+
mocker.patch(
474+
"poetry.utils.env.generic_env.GenericEnv.get_paths",
475+
return_value={"purelib": str(site_path)},
476+
)
431477

432478
EnvManager(poetry).build_venv(path=venv_path, flags={"system-site-packages": True})
433479
env = VirtualEnv(venv_path)
480+
434481
installed_repository = InstalledRepository.load(env)
435482

483+
assert installed_repository.packages == installed_repository.system_site_packages
436484
source_types = {
437485
package.name: package.source_type for package in installed_repository.packages
438486
}
@@ -458,6 +506,7 @@ def test_pipx_shared_lib_site_packages(
458506
installed_repository = InstalledRepository.load(env)
459507

460508
assert len(installed_repository.packages) == 1
509+
assert installed_repository.system_site_packages == []
461510
cleo_package = installed_repository.packages[0]
462511
cleo_package.to_dependency()
463512
# There must not be a warning

0 commit comments

Comments
 (0)