Skip to content

Commit 95d03d6

Browse files
authored
Fix Python package version detection after upgrade in conda history (#276)
Fixes #239
1 parent 7b6a321 commit 95d03d6

File tree

4 files changed

+130
-50
lines changed

4 files changed

+130
-50
lines changed

crates/pet-conda/src/package.rs

Lines changed: 66 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -93,59 +93,75 @@ fn get_conda_package_info_from_history(path: &Path, name: &Package) -> Option<Co
9393
let package_entry = format!(":{}-", name.to_name());
9494

9595
let history_contents = fs::read_to_string(history).ok()?;
96-
for line in history_contents
96+
97+
// Filter to only include lines that:
98+
// 1. Start with '+' (installed packages, not '-' for removed packages)
99+
// 2. Contain the package entry (e.g., ":python-")
100+
//
101+
// We need the LAST matching entry because conda appends to history chronologically.
102+
// When a package is upgraded, the old version is removed (-) and new version installed (+).
103+
// The last '+' entry represents the currently installed version.
104+
//
105+
// Sample history for Python upgrade from 3.9.18 to 3.9.21:
106+
// +defaults::python-3.9.18-h123456_0 <- initial install
107+
// ...
108+
// -defaults::python-3.9.18-h123456_0 <- removed during upgrade
109+
// +defaults::python-3.9.21-h789abc_0 <- current version (we want this)
110+
let matching_lines: Vec<&str> = history_contents
97111
.lines()
98-
.filter(|l| l.contains(&package_entry))
99-
{
100-
// Sample entry in the history file
101-
// +conda-forge/osx-arm64::psutil-5.9.8-py312he37b823_0
102-
// +conda-forge/osx-arm64::python-3.12.2-hdf0ec26_0_cpython
103-
// +conda-forge/osx-arm64::python_abi-3.12-4_cp312
104-
let regex = get_package_version_history_regex(name);
105-
if let Some(captures) = regex.captures(line) {
106-
if let Some(version) = captures.get(1) {
107-
if let Some(hash) = captures.get(2) {
108-
let package_path = format!(
109-
"{}-{}-{}.json",
110-
name.to_name(),
111-
version.as_str(),
112-
hash.as_str()
113-
);
114-
let package_path = path.join(package_path);
115-
let mut arch: Option<Architecture> = None;
116-
// Sample contents
117-
// {
118-
// "build": "h966fe2a_2",
119-
// "build_number": 2,
120-
// "channel": "https://repo.anaconda.com/pkgs/main/win-64",
121-
// "constrains": [],
122-
// }
123-
// 32bit channel is https://repo.anaconda.com/pkgs/main/win-32/
124-
// 64bit channel is "channel": "https://repo.anaconda.com/pkgs/main/osx-arm64",
125-
if let Ok(contents) = read_to_string(&package_path) {
126-
if let Ok(js) = serde_json::from_str::<CondaMetaPackageStructure>(&contents)
127-
{
128-
if let Some(channel) = js.channel {
129-
if channel.ends_with("64") {
130-
arch = Some(Architecture::X64);
131-
} else if channel.ends_with("32") {
132-
arch = Some(Architecture::X86);
133-
}
134-
}
135-
if let Some(version) = js.version {
136-
return Some(CondaPackageInfo {
137-
package: name.clone(),
138-
path: package_path,
139-
version,
140-
arch,
141-
});
142-
} else {
143-
warn!(
144-
"Unable to find version for package {} in {:?}",
145-
name, package_path
146-
);
112+
.filter(|l| l.starts_with('+') && l.contains(&package_entry))
113+
.collect();
114+
115+
// Get the last matching line (most recent installation)
116+
let line = matching_lines.last()?;
117+
118+
// Sample entry in the history file
119+
// +conda-forge/osx-arm64::psutil-5.9.8-py312he37b823_0
120+
// +conda-forge/osx-arm64::python-3.12.2-hdf0ec26_0_cpython
121+
// +conda-forge/osx-arm64::python_abi-3.12-4_cp312
122+
let regex = get_package_version_history_regex(name);
123+
if let Some(captures) = regex.captures(line) {
124+
if let Some(version) = captures.get(1) {
125+
if let Some(hash) = captures.get(2) {
126+
let package_path = format!(
127+
"{}-{}-{}.json",
128+
name.to_name(),
129+
version.as_str(),
130+
hash.as_str()
131+
);
132+
let package_path = path.join(package_path);
133+
let mut arch: Option<Architecture> = None;
134+
// Sample contents
135+
// {
136+
// "build": "h966fe2a_2",
137+
// "build_number": 2,
138+
// "channel": "https://repo.anaconda.com/pkgs/main/win-64",
139+
// "constrains": [],
140+
// }
141+
// 32bit channel is https://repo.anaconda.com/pkgs/main/win-32/
142+
// 64bit channel is "channel": "https://repo.anaconda.com/pkgs/main/osx-arm64",
143+
if let Ok(contents) = read_to_string(&package_path) {
144+
if let Ok(js) = serde_json::from_str::<CondaMetaPackageStructure>(&contents) {
145+
if let Some(channel) = js.channel {
146+
if channel.ends_with("64") {
147+
arch = Some(Architecture::X64);
148+
} else if channel.ends_with("32") {
149+
arch = Some(Architecture::X86);
147150
}
148151
}
152+
if let Some(version) = js.version {
153+
return Some(CondaPackageInfo {
154+
package: name.clone(),
155+
path: package_path,
156+
version,
157+
arch,
158+
});
159+
} else {
160+
warn!(
161+
"Unable to find version for package {} in {:?}",
162+
name, package_path
163+
);
164+
}
149165
}
150166
}
151167
}

crates/pet-conda/tests/package_test.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,33 @@ fn get_python_package_info_without_history() {
9292
])
9393
);
9494
}
95+
96+
/// Test that when Python is upgraded, we get the current (last installed) version,
97+
/// not the original (first installed) version.
98+
/// This is a regression test for https://github.com/microsoft/python-environment-tools/issues/239
99+
///
100+
/// The history file contains:
101+
/// +defaults::python-3.9.18-h1a28f6b_0 (initial install)
102+
/// -defaults::python-3.9.18-h1a28f6b_0 (removed during upgrade)
103+
/// +defaults::python-3.9.21-h789abc_0 (current version)
104+
///
105+
/// We should detect version 3.9.21, not 3.9.18.
106+
#[cfg(unix)]
107+
#[test]
108+
fn get_python_package_info_after_upgrade() {
109+
let path: PathBuf = resolve_test_path(&["unix", "conda_env_with_python_upgrade"]);
110+
let pkg = CondaPackageInfo::from(&path, &package::Package::Python).unwrap();
111+
112+
assert_eq!(pkg.package, package::Package::Python);
113+
// Should be 3.9.21 (current version), NOT 3.9.18 (original version)
114+
assert_eq!(pkg.version, "3.9.21".to_string());
115+
assert_eq!(
116+
pkg.path,
117+
resolve_test_path(&[
118+
"unix",
119+
"conda_env_with_python_upgrade",
120+
"conda-meta",
121+
"python-3.9.21-h789abc_0.json"
122+
])
123+
);
124+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
==> 2024-01-15 10:00:00 <==
2+
# cmd: /home/user/miniforge3/bin/conda create -n waa python=3.9.18
3+
+defaults::ca-certificates-2023.01.10-hca03da5_0
4+
+defaults::openssl-1.1.1t-h1a28f6b_0
5+
+defaults::python-3.9.18-h1a28f6b_0
6+
+defaults::pip-22.3.1-py39hca03da5_0
7+
+defaults::setuptools-65.6.3-py39hca03da5_0
8+
+defaults::wheel-0.38.4-py39hca03da5_0
9+
# update specs: ['python=3.9.18']
10+
11+
==> 2024-06-20 14:30:00 <==
12+
# cmd: /home/user/miniforge3/bin/conda update python
13+
-defaults::python-3.9.18-h1a28f6b_0
14+
-defaults::pip-22.3.1-py39hca03da5_0
15+
-defaults::setuptools-65.6.3-py39hca03da5_0
16+
-defaults::wheel-0.38.4-py39hca03da5_0
17+
+defaults::python-3.9.21-h789abc_0
18+
+defaults::pip-23.3.1-py39hca03da5_0
19+
+defaults::setuptools-68.0.0-py39hca03da5_0
20+
+defaults::wheel-0.41.2-py39hca03da5_0
21+
# update specs: ['python']
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"build": "h789abc_0",
3+
"build_number": 0,
4+
"channel": "https://repo.anaconda.com/pkgs/main/linux-64",
5+
"constrains": [],
6+
"depends": [],
7+
"files": [],
8+
"license": "PSF",
9+
"name": "python",
10+
"noarch": null,
11+
"package_type": "conda",
12+
"version": "3.9.21"
13+
}

0 commit comments

Comments
 (0)