Skip to content

Commit 3f11e55

Browse files
committed
fix: formalize refresh locator state (Fixes #387)
1 parent bf64216 commit 3f11e55

File tree

11 files changed

+622
-56
lines changed

11 files changed

+622
-56
lines changed

crates/pet-conda/src/lib.rs

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use pet_core::{
1616
os_environment::Environment,
1717
python_environment::{PythonEnvironment, PythonEnvironmentKind},
1818
reporter::Reporter,
19-
Locator, LocatorKind,
19+
Locator, LocatorKind, RefreshStatePersistence, RefreshStateSyncScope,
2020
};
2121
use pet_fs::path::norm_case;
2222
use rayon::prelude::*;
@@ -216,11 +216,39 @@ impl Locator for Conda {
216216
fn get_kind(&self) -> LocatorKind {
217217
LocatorKind::Conda
218218
}
219-
fn configure(&self, config: &pet_core::Configuration) {
220-
if let Some(ref conda_exe) = config.conda_executable {
221-
let mut conda_executable = self.conda_executable.write().unwrap();
222-
conda_executable.replace(conda_exe.clone());
219+
fn refresh_state(&self) -> RefreshStatePersistence {
220+
RefreshStatePersistence::SyncedDiscoveryState
221+
}
222+
fn sync_refresh_state_from(&self, source: &dyn Locator, scope: &RefreshStateSyncScope) {
223+
let source = source.as_any().downcast_ref::<Conda>().unwrap_or_else(|| {
224+
panic!("attempted to sync Conda state from {:?}", source.get_kind())
225+
});
226+
227+
match scope {
228+
RefreshStateSyncScope::Full => {}
229+
RefreshStateSyncScope::GlobalFiltered(kind)
230+
if self.supported_categories().contains(kind) => {}
231+
RefreshStateSyncScope::GlobalFiltered(_) | RefreshStateSyncScope::Workspace => {
232+
return;
233+
}
223234
}
235+
236+
self.environments.clear();
237+
self.environments
238+
.insert_many(source.environments.clone_map());
239+
240+
self.managers.clear();
241+
self.managers.insert_many(source.managers.clone_map());
242+
243+
self.mamba_managers.clear();
244+
self.mamba_managers
245+
.insert_many(source.mamba_managers.clone_map());
246+
}
247+
fn configure(&self, config: &pet_core::Configuration) {
248+
self.conda_executable
249+
.write()
250+
.unwrap()
251+
.clone_from(&config.conda_executable);
224252
}
225253
fn supported_categories(&self) -> Vec<PythonEnvironmentKind> {
226254
vec![PythonEnvironmentKind::Conda]

crates/pet-core/src/lib.rs

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4-
use std::path::PathBuf;
4+
use std::{any::Any, path::PathBuf};
55

66
use env::PythonEnv;
77
use manager::EnvManager;
@@ -61,7 +61,26 @@ pub enum LocatorKind {
6161
WindowsStore,
6262
}
6363

64-
pub trait Locator: Send + Sync {
64+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65+
pub enum RefreshStatePersistence {
66+
/// The locator keeps no mutable state across requests.
67+
Stateless,
68+
/// The locator keeps configured inputs only; refresh must not copy them back.
69+
ConfiguredOnly,
70+
/// The locator keeps cache-like state, but later requests can repopulate it on demand.
71+
SelfHydratingCache,
72+
/// The locator keeps refresh-discovered state that later requests depend on for correctness.
73+
SyncedDiscoveryState,
74+
}
75+
76+
#[derive(Debug, Clone, PartialEq, Eq)]
77+
pub enum RefreshStateSyncScope {
78+
Full,
79+
GlobalFiltered(PythonEnvironmentKind),
80+
Workspace,
81+
}
82+
83+
pub trait Locator: Any + Send + Sync {
6584
/// Returns the name of the locator.
6685
fn get_kind(&self) -> LocatorKind;
6786
/// Configures the locator with the given configuration.
@@ -100,6 +119,21 @@ pub trait Locator: Send + Sync {
100119
fn configure(&self, _config: &Configuration) {
101120
//
102121
}
122+
/// Describes what mutable state, if any, must survive a refresh boundary.
123+
///
124+
/// Refresh runs execute against transient locator graphs and then invoke
125+
/// `sync_refresh_state_from()` on the long-lived shared locators.
126+
fn refresh_state(&self) -> RefreshStatePersistence {
127+
RefreshStatePersistence::Stateless
128+
}
129+
/// Copies correctness-critical post-refresh state from a transient locator into this
130+
/// long-lived shared locator.
131+
///
132+
/// Override this only when `refresh_state()` returns
133+
/// `RefreshStatePersistence::SyncedDiscoveryState`.
134+
fn sync_refresh_state_from(&self, _source: &dyn Locator, _scope: &RefreshStateSyncScope) {
135+
//
136+
}
103137
/// Returns a list of supported categories for this locator.
104138
fn supported_categories(&self) -> Vec<PythonEnvironmentKind>;
105139
/// Given a Python executable, and some optional data like prefix,
@@ -112,3 +146,9 @@ pub trait Locator: Send + Sync {
112146
/// Finds all environments specific to this locator.
113147
fn find(&self, reporter: &dyn Reporter);
114148
}
149+
150+
impl dyn Locator {
151+
pub fn as_any(&self) -> &dyn Any {
152+
self
153+
}
154+
}

crates/pet-linux-global-python/src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use pet_core::{
1515
env::PythonEnv,
1616
python_environment::{PythonEnvironment, PythonEnvironmentBuilder, PythonEnvironmentKind},
1717
reporter::Reporter,
18-
Locator, LocatorKind,
18+
Locator, LocatorKind, RefreshStatePersistence,
1919
};
2020
use pet_fs::path::resolve_symlink;
2121
use pet_python_utils::{env::ResolvedPythonEnv, executable::find_executables};
@@ -62,6 +62,9 @@ impl Locator for LinuxGlobalPython {
6262
fn get_kind(&self) -> LocatorKind {
6363
LocatorKind::LinuxGlobal
6464
}
65+
fn refresh_state(&self) -> RefreshStatePersistence {
66+
RefreshStatePersistence::SelfHydratingCache
67+
}
6568
fn supported_categories(&self) -> Vec<PythonEnvironmentKind> {
6669
vec![PythonEnvironmentKind::LinuxGlobal]
6770
}

crates/pet-pipenv/src/lib.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use pet_core::LocatorKind;
1111
use pet_core::{
1212
python_environment::{PythonEnvironment, PythonEnvironmentBuilder, PythonEnvironmentKind},
1313
reporter::Reporter,
14-
Configuration, Locator,
14+
Configuration, Locator, RefreshStatePersistence,
1515
};
1616
use pet_fs::path::norm_case;
1717
use pet_python_utils::executable::find_executables;
@@ -418,10 +418,15 @@ impl Locator for PipEnv {
418418
LocatorKind::PipEnv
419419
}
420420

421+
fn refresh_state(&self) -> RefreshStatePersistence {
422+
RefreshStatePersistence::ConfiguredOnly
423+
}
424+
421425
fn configure(&self, config: &Configuration) {
422-
if let Some(exe) = &config.pipenv_executable {
423-
self.pipenv_executable.write().unwrap().replace(exe.clone());
424-
}
426+
self.pipenv_executable
427+
.write()
428+
.unwrap()
429+
.clone_from(&config.pipenv_executable);
425430
}
426431

427432
fn supported_categories(&self) -> Vec<PythonEnvironmentKind> {

crates/pet-poetry/src/lib.rs

Lines changed: 144 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ use pet_core::{
1111
os_environment::Environment,
1212
python_environment::{PythonEnvironment, PythonEnvironmentKind},
1313
reporter::Reporter,
14-
Configuration, Locator, LocatorKind, LocatorResult,
14+
Configuration, Locator, LocatorKind, LocatorResult, RefreshStatePersistence,
15+
RefreshStateSyncScope,
1516
};
1617
use pet_virtualenv::is_virtualenv;
1718
use regex::Regex;
@@ -137,7 +138,6 @@ impl Poetry {
137138
}
138139
}
139140
fn clear(&self) {
140-
self.poetry_executable.write().unwrap().take();
141141
self.search_result.write().unwrap().take();
142142
}
143143
pub fn from(environment: &dyn Environment) -> Poetry {
@@ -152,6 +152,34 @@ impl Poetry {
152152
.clone_from(&search_result);
153153
}
154154

155+
pub fn merge_search_result_from(&self, source: &Poetry) {
156+
let source_search_result = source.search_result.read().unwrap().clone();
157+
let Some(source_search_result) = source_search_result else {
158+
return;
159+
};
160+
161+
let mut merged = self
162+
.search_result
163+
.read()
164+
.unwrap()
165+
.clone()
166+
.unwrap_or(LocatorResult {
167+
managers: vec![],
168+
environments: vec![],
169+
});
170+
merged.managers.extend(source_search_result.managers);
171+
merged.managers.sort();
172+
merged.managers.dedup();
173+
174+
merged
175+
.environments
176+
.extend(source_search_result.environments);
177+
merged.environments.sort();
178+
merged.environments.dedup();
179+
180+
self.search_result.write().unwrap().replace(merged);
181+
}
182+
155183
fn find_with_cache(&self) -> Option<LocatorResult> {
156184
// First check if we have cached results
157185
{
@@ -226,17 +254,39 @@ impl Locator for Poetry {
226254
fn get_kind(&self) -> LocatorKind {
227255
LocatorKind::Poetry
228256
}
257+
fn refresh_state(&self) -> RefreshStatePersistence {
258+
RefreshStatePersistence::SyncedDiscoveryState
259+
}
260+
fn sync_refresh_state_from(&self, source: &dyn Locator, scope: &RefreshStateSyncScope) {
261+
let source = source.as_any().downcast_ref::<Poetry>().unwrap_or_else(|| {
262+
panic!(
263+
"attempted to sync Poetry state from {:?}",
264+
source.get_kind()
265+
)
266+
});
267+
match scope {
268+
RefreshStateSyncScope::Full => self.sync_search_result_from(source),
269+
RefreshStateSyncScope::GlobalFiltered(kind)
270+
if self.supported_categories().contains(kind) =>
271+
{
272+
self.sync_search_result_from(source)
273+
}
274+
RefreshStateSyncScope::Workspace => self.merge_search_result_from(source),
275+
RefreshStateSyncScope::GlobalFiltered(_) => {}
276+
}
277+
}
229278
fn configure(&self, config: &Configuration) {
279+
let mut ws_dirs = self.workspace_directories.write().unwrap();
280+
ws_dirs.clear();
230281
if let Some(workspace_directories) = &config.workspace_directories {
231-
let mut ws_dirs = self.workspace_directories.write().unwrap();
232-
ws_dirs.clear();
233282
if !workspace_directories.is_empty() {
234283
ws_dirs.extend(workspace_directories.clone());
235284
}
236285
}
237-
if let Some(exe) = &config.poetry_executable {
238-
self.poetry_executable.write().unwrap().replace(exe.clone());
239-
}
286+
self.poetry_executable
287+
.write()
288+
.unwrap()
289+
.clone_from(&config.poetry_executable);
240290
}
241291

242292
fn supported_categories(&self) -> Vec<PythonEnvironmentKind> {
@@ -349,4 +399,91 @@ mod tests {
349399
Some("fresh")
350400
);
351401
}
402+
403+
#[test]
404+
fn test_workspace_scope_merges_search_results() {
405+
let environment = EnvironmentApi::new();
406+
let target = Poetry::from(&environment);
407+
let source = Poetry::from(&environment);
408+
409+
target
410+
.search_result
411+
.write()
412+
.unwrap()
413+
.replace(LocatorResult {
414+
managers: vec![],
415+
environments: vec![PythonEnvironment {
416+
name: Some("existing".to_string()),
417+
kind: Some(PythonEnvironmentKind::Poetry),
418+
..Default::default()
419+
}],
420+
});
421+
422+
source
423+
.search_result
424+
.write()
425+
.unwrap()
426+
.replace(LocatorResult {
427+
managers: vec![],
428+
environments: vec![PythonEnvironment {
429+
name: Some("workspace".to_string()),
430+
kind: Some(PythonEnvironmentKind::Poetry),
431+
..Default::default()
432+
}],
433+
});
434+
435+
target.sync_refresh_state_from(&source, &RefreshStateSyncScope::Workspace);
436+
437+
let result = target.search_result.read().unwrap().clone().unwrap();
438+
let mut names = result
439+
.environments
440+
.iter()
441+
.map(|environment| environment.name.clone().unwrap())
442+
.collect::<Vec<String>>();
443+
names.sort();
444+
445+
assert_eq!(names, vec!["existing".to_string(), "workspace".to_string()]);
446+
}
447+
448+
#[test]
449+
fn test_clear_preserves_configured_poetry_executable() {
450+
let environment = EnvironmentApi::new();
451+
let poetry = Poetry::from(&environment);
452+
let configured = PathBuf::from("/configured/poetry");
453+
454+
poetry.configure(&Configuration {
455+
poetry_executable: Some(configured.clone()),
456+
..Default::default()
457+
});
458+
poetry
459+
.search_result
460+
.write()
461+
.unwrap()
462+
.replace(LocatorResult {
463+
managers: vec![],
464+
environments: vec![],
465+
});
466+
467+
poetry.clear();
468+
469+
assert_eq!(
470+
poetry.poetry_executable.read().unwrap().clone(),
471+
Some(configured)
472+
);
473+
assert!(poetry.search_result.read().unwrap().is_none());
474+
}
475+
476+
#[test]
477+
fn test_configure_clears_poetry_executable_when_unset() {
478+
let environment = EnvironmentApi::new();
479+
let poetry = Poetry::from(&environment);
480+
481+
poetry.configure(&Configuration {
482+
poetry_executable: Some(PathBuf::from("/configured/poetry")),
483+
..Default::default()
484+
});
485+
poetry.configure(&Configuration::default());
486+
487+
assert!(poetry.poetry_executable.read().unwrap().is_none());
488+
}
352489
}

crates/pet-pyenv/src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use pet_core::{
1919
os_environment::Environment,
2020
python_environment::{PythonEnvironment, PythonEnvironmentKind},
2121
reporter::Reporter,
22-
Locator, LocatorKind,
22+
Locator, LocatorKind, RefreshStatePersistence,
2323
};
2424
use pet_python_utils::executable::find_executable;
2525

@@ -84,6 +84,9 @@ impl Locator for PyEnv {
8484
fn get_kind(&self) -> LocatorKind {
8585
LocatorKind::PyEnv
8686
}
87+
fn refresh_state(&self) -> RefreshStatePersistence {
88+
RefreshStatePersistence::SelfHydratingCache
89+
}
8790
fn supported_categories(&self) -> Vec<PythonEnvironmentKind> {
8891
vec![
8992
PythonEnvironmentKind::Pyenv,

0 commit comments

Comments
 (0)