Skip to content

Commit 0be3e1c

Browse files
authored
Protect conda plugins from being deleted during reset (#61)
* Protect conda plugins from being deleted * let subprocess print to stdout/err by default * make PackageInfo.from_record() compatible with both PrefixRecord and PackageCacheRecord * merge the two class methods * debug * do not return early * save iterator as list to avoid exhausting it * verbose pytest * dedent
1 parent 4ca30fa commit 0be3e1c

File tree

6 files changed

+60
-25
lines changed

6 files changed

+60
-25
lines changed

.github/workflows/tests.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,12 @@ jobs:
5353
echo "channels: [${{ matrix.channel }}]" > .pixi/envs/test-py${{ matrix.python-version }}/.condarc
5454
pixi run --environment test-py${{ matrix.python-version }} python -m conda info
5555
- name: Run tests
56-
run: pixi run --environment test-py${{ matrix.python-version }} test --basetemp=${{ runner.os == 'Windows' && 'D:\temp' || runner.temp }}
56+
run: >
57+
pixi run
58+
--environment test-py${{ matrix.python-version }}
59+
test
60+
-vv
61+
--basetemp=${{ runner.os == 'Windows' && 'D:\temp' || runner.temp }}
5762
5863
build-conda:
5964
name: Build conda package (${{ matrix.os }})

conda_self/exceptions.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,7 @@ def __init__(self, specs: list[str]):
1919

2020

2121
class NoDistInfoDirFound(CondaError):
22-
def __init__(self, path: str | Path):
23-
super().__init__(f"No *.dist-info directories found in {path}.")
22+
def __init__(self, package_name: str, path: str | Path):
23+
super().__init__(
24+
f"No *.dist-info directories found for '{package_name}' in '{path}'."
25+
)

conda_self/package_info.py

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
from __future__ import annotations
22

33
import configparser
4+
import os
5+
import sys
46
from pathlib import Path
57
from typing import TYPE_CHECKING
68

9+
from conda.models.records import PrefixRecord
10+
711
from .exceptions import NoDistInfoDirFound
812

913
if TYPE_CHECKING:
@@ -18,23 +22,34 @@ class CaseSensitiveConfigParser(configparser.ConfigParser):
1822

1923
class PackageInfo:
2024
def __init__(self, dist_info_path: Path):
21-
"""Describe the dist-info for a conda package"""
25+
"""Describe the dist-info for a Python package installed as a conda package"""
2226
self.dist_info_path = dist_info_path
2327

2428
@classmethod
25-
def from_record(cls, record: PackageCacheRecord) -> list[PackageInfo]:
26-
return cls.from_conda_extracted_package_path(record.extracted_package_dir)
27-
28-
@classmethod
29-
def from_conda_extracted_package_path(cls, path: str | Path) -> list[PackageInfo]:
30-
"""Create a PackageInfo object given the path to an extracted conda package"""
31-
path = Path(path)
32-
matching_paths = [
33-
p for p in path.rglob("**/*site-packages/*dist-info*") if p.is_dir()
34-
]
35-
if len(matching_paths) == 0:
36-
raise NoDistInfoDirFound(path)
37-
return [cls(matching_path) for matching_path in matching_paths]
29+
def from_record(
30+
cls, record: PrefixRecord | PackageCacheRecord
31+
) -> list[PackageInfo]:
32+
if not (paths := getattr(record, "files", None)):
33+
try:
34+
paths = (
35+
Path(record.extracted_package_dir, "info/files")
36+
.read_text()
37+
.splitlines()
38+
)
39+
except FileNotFoundError:
40+
# missing info/files -> empty package
41+
paths = []
42+
dist_infos = set()
43+
for path in paths:
44+
if (maybe_dist_info := os.path.dirname(path)).endswith(".dist-info"):
45+
dist_infos.add(maybe_dist_info)
46+
if isinstance(record, PrefixRecord):
47+
basedir = sys.prefix
48+
else:
49+
basedir = record.extracted_package_dir
50+
if not dist_infos:
51+
raise NoDistInfoDirFound(record.name, basedir)
52+
return [cls(Path(basedir, p)) for p in dist_infos]
3853

3954
def entry_points(self) -> dict[str, dict[str, str]]:
4055
"""Get the entry points for a package.

conda_self/query.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import sys
6+
from contextlib import suppress
67
from typing import TYPE_CHECKING
78

89
from conda.base.context import context
@@ -17,6 +18,8 @@
1718
from conda.models.version import VersionOrder
1819

1920
from .constants import PERMANENT_PACKAGES
21+
from .exceptions import NoDistInfoDirFound
22+
from .package_info import PackageInfo
2023

2124
if TYPE_CHECKING:
2225
from collections.abc import Iterable
@@ -66,11 +69,19 @@ def permanent_dependencies() -> set[str]:
6669
# In some dev environments, conda-self is installed as a PyPI package
6770
# and does not have its conda-meta/conda-self-*.json entry, which makes it
6871
# invisible to PrefixData()... unless we enable interoperability.
69-
installed = PrefixData(sys.prefix, interoperability=True)
70-
prefix_graph = PrefixGraph(installed.iter_records())
72+
installed = list(PrefixData(sys.prefix, interoperability=True).iter_records())
73+
prefix_graph = PrefixGraph(installed)
74+
75+
protect = [*PERMANENT_PACKAGES]
76+
for record in installed:
77+
with suppress(NoDistInfoDirFound):
78+
for pkg_info in PackageInfo.from_record(record):
79+
if "conda" in pkg_info.entry_points():
80+
protect.append(record.name)
81+
break
7182

7283
packages = []
73-
for pkg in PERMANENT_PACKAGES:
84+
for pkg in dict.fromkeys(protect):
7485
node = next((rec for rec in prefix_graph.records if rec.name == pkg), None)
7586
if node:
7687
packages.append(node.name)

conda_self/testing.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from conda.models.records import PackageRecord
1515

1616

17-
def conda_cli_subprocess(prefix: str | Path, *args) -> CompletedProcess:
17+
def conda_cli_subprocess(prefix: str | Path, *args, **kwargs) -> CompletedProcess:
1818
prefix = Path(prefix)
1919
if os.name == "nt":
2020
python = prefix / "python.exe"
@@ -23,9 +23,8 @@ def conda_cli_subprocess(prefix: str | Path, *args) -> CompletedProcess:
2323

2424
return run(
2525
[python, "-m", "conda", *args],
26-
check=True,
27-
capture_output=True,
28-
text=True,
26+
check=kwargs.pop("check", True),
27+
**kwargs,
2928
)
3029

3130

tests/test_cli_reset.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ def test_help(conda_cli):
99

1010

1111
def test_reset(conda_cli, tmp_env: TmpEnvFixture):
12-
with tmp_env("conda", "conda-self") as prefix:
12+
# Adding conda-index too to test that non-default plugins are kept
13+
with tmp_env("conda", "conda-self", "conda-index") as prefix:
1314
assert not is_installed(prefix, "numpy")
1415

1516
conda_cli("install", "numpy", "--yes", "--prefix", prefix)
@@ -20,5 +21,7 @@ def test_reset(conda_cli, tmp_env: TmpEnvFixture):
2021
assert is_installed(prefix, "conda")
2122
# make sure conda-self didn't remove itself
2223
assert is_installed(prefix, "conda-self")
24+
# make sure conda-self didn't remove a non-default conda plugin
25+
assert is_installed(prefix, "conda-index")
2326
# but numpy should be gone
2427
assert not is_installed(prefix, "numpy")

0 commit comments

Comments
 (0)