Skip to content

Commit b2a819b

Browse files
committed
fix: address PR #460 round-2 review feedback
- get_default_virtual_dir(): no longer requires the path to exist on disk at construction time. The long-lived locator graph is built once at server startup; users may install Hatch (or create the first env) later in the same process. find() now re-checks existence at call time so newly-created envs are discovered without a restart. - try_from(): do the cheap path-shape classification (default storage / configured workspace dirs) BEFORE reading pyvenv.cfg, so non-Hatch venvs flowing through the locator chain do not pay an extra filesystem read. - try_from(): inspect the workspace cache under the lock and capture the match instead of cloning the entire cache on every identification attempt.
1 parent ec40512 commit b2a819b

1 file changed

Lines changed: 73 additions & 60 deletions

File tree

crates/pet-hatch/src/lib.rs

Lines changed: 73 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -128,61 +128,76 @@ impl Locator for Hatch {
128128
.map(Path::to_path_buf)
129129
})?;
130130

131-
// A pyvenv.cfg must be present — Hatch envs are always venvs.
132-
let cfg = PyVenvCfg::find(&prefix)?;
133-
131+
// Do the cheap path-shape classification *first* so we don't pay for
132+
// a `pyvenv.cfg` filesystem read on every non-Hatch venv that flows
133+
// through the locator chain.
134+
//
134135
// Case 1: prefix lives in the default `<data_dir>/env/virtual` storage,
135136
// exactly three components deep:
136137
// <storage>/<project_name>/<project_id>/<venv_name>
138+
let mut classification: Option<(String, Option<PathBuf>)> = None;
137139
if let Some(storage) = self.default_virtual_dir.as_deref() {
138140
if let Some(env_name) = match_default_storage_layout(&prefix, storage) {
139-
trace!(
140-
"Hatch env (default storage) {} found at {}",
141-
env_name,
142-
env.executable.display()
143-
);
144-
return Some(build_env(&prefix, &cfg, env_name, None, &env.executable));
141+
classification = Some((env_name, None));
145142
}
146143
}
147144

148145
// Case 2: prefix lives one level under a workspace's configured
149-
// `dirs.env.virtual` directory (flat layout).
150-
let workspaces = self
151-
.workspace_virtual_dirs
152-
.lock()
153-
.expect("workspace_virtual_dirs mutex poisoned")
154-
.clone();
155-
for (workspace, virtual_dirs) in &workspaces {
156-
for virtual_dir in virtual_dirs {
157-
if prefix_is_directly_under(&prefix, virtual_dir) {
158-
let env_name = prefix
159-
.file_name()
160-
.map(|n| n.to_string_lossy().to_string())
161-
.unwrap_or_default();
162-
trace!(
163-
"Hatch env (project-local) {} found at {}",
164-
env_name,
165-
env.executable.display()
166-
);
167-
return Some(build_env(
168-
&prefix,
169-
&cfg,
170-
env_name,
171-
Some(workspace.clone()),
172-
&env.executable,
173-
));
146+
// `dirs.env.virtual` directory (flat layout). Inspect the cached
147+
// workspaces under the lock and capture the match instead of cloning
148+
// the entire cache.
149+
if classification.is_none() {
150+
let cache = self
151+
.workspace_virtual_dirs
152+
.lock()
153+
.expect("workspace_virtual_dirs mutex poisoned");
154+
'workspaces: for (workspace, virtual_dirs) in cache.iter() {
155+
for virtual_dir in virtual_dirs {
156+
if prefix_is_directly_under(&prefix, virtual_dir) {
157+
let env_name = prefix
158+
.file_name()
159+
.map(|n| n.to_string_lossy().to_string())
160+
.unwrap_or_default();
161+
classification = Some((env_name, Some(workspace.clone())));
162+
break 'workspaces;
163+
}
174164
}
175165
}
176166
}
177167

178-
None
168+
let (env_name, project_path) = classification?;
169+
170+
// Now that we know this is (likely) a Hatch env, read pyvenv.cfg.
171+
// Hatch always writes one; if it's missing this isn't actually a
172+
// Hatch-managed env.
173+
let cfg = PyVenvCfg::find(&prefix)?;
174+
175+
trace!(
176+
"Hatch env {} found at {}",
177+
env_name,
178+
env.executable.display()
179+
);
180+
Some(build_env(
181+
&prefix,
182+
&cfg,
183+
env_name,
184+
project_path,
185+
&env.executable,
186+
))
179187
}
180188

181189
fn find(&self, reporter: &dyn Reporter) {
182-
// 1. Walk the default storage directory.
190+
// 1. Walk the default storage directory if it currently exists. We
191+
// re-check existence here (rather than caching the result of the
192+
// check at construction) because the long-lived locator graph is
193+
// built once at server startup; the user may install Hatch or
194+
// create their first env after that point and we still want to
195+
// discover it without a restart.
183196
if let Some(storage) = self.default_virtual_dir.as_deref() {
184-
for env in find_envs_in_default_storage(storage) {
185-
reporter.report_environment(&env);
197+
if storage.is_dir() {
198+
for env in find_envs_in_default_storage(storage) {
199+
reporter.report_environment(&env);
200+
}
186201
}
187202
}
188203

@@ -214,29 +229,23 @@ impl Locator for Hatch {
214229
/// 2. Platform default for `platformdirs.user_data_dir("hatch", appauthor=False)`
215230
/// (then append `env/virtual`).
216231
///
217-
/// Returns `None` if the resulting directory does not exist on disk.
232+
/// The returned path may not exist on disk yet; callers must check existence
233+
/// at use time. This lets us correctly identify Hatch envs created later in
234+
/// the same long-lived PET process without a restart.
218235
fn get_default_virtual_dir(environment: &dyn Environment) -> Option<PathBuf> {
219-
// If HATCH_DATA_DIR is set and non-empty, Hatch *only* uses that location
220-
// it never falls back to the platform default. Mirror that behaviour: return
221-
// the env/virtual subdir when it exists on disk, otherwise None. Do not
222-
// fall through to platform defaults, or we'd risk attributing platform-
223-
// default envs to Hatch when the user has redirected Hatch elsewhere.
236+
// If HATCH_DATA_DIR is set and non-empty, Hatch *only* uses that location
237+
// it never falls back to the platform default. Mirror that behaviour.
238+
// Do not fall through to platform defaults, or we'd risk attributing
239+
// platform-default envs to Hatch when the user has redirected Hatch
240+
// elsewhere.
224241
if let Some(custom) = environment.get_env_var("HATCH_DATA_DIR".to_string()) {
225242
if !custom.is_empty() {
226-
let path = append_virtual_subdir(PathBuf::from(custom));
227-
return if path.is_dir() {
228-
Some(norm_case(path))
229-
} else {
230-
None
231-
};
243+
return Some(norm_case(append_virtual_subdir(PathBuf::from(custom))));
232244
}
233245
}
234-
let path = append_virtual_subdir(platform_default_data_dir(environment)?);
235-
if path.is_dir() {
236-
Some(norm_case(path))
237-
} else {
238-
None
239-
}
246+
Some(norm_case(append_virtual_subdir(platform_default_data_dir(
247+
environment,
248+
)?)))
240249
}
241250

242251
fn append_virtual_subdir(data_dir: PathBuf) -> PathBuf {
@@ -962,18 +971,22 @@ mod tests {
962971
// If HATCH_DATA_DIR is set, Hatch only uses that location. We must
963972
// never silently fall through to the platform default — that could
964973
// misattribute platform-default envs to Hatch when the user has
965-
// redirected Hatch elsewhere.
974+
// redirected Hatch elsewhere. The path itself does not need to
975+
// exist at construction time (it may be created later in the
976+
// process lifetime); we only require that the returned value
977+
// points at HATCH_DATA_DIR/env/virtual, not the platform default.
966978
let temp = TempDir::new().unwrap();
967-
// Set HATCH_DATA_DIR to a directory whose env/virtual subdir does not exist.
979+
let custom = temp.path().join("does-not-exist-yet");
968980
let mut vars = HashMap::new();
969981
vars.insert(
970982
"HATCH_DATA_DIR".to_string(),
971-
temp.path().to_string_lossy().to_string(),
983+
custom.to_string_lossy().to_string(),
972984
);
973985
let env = TestEnv {
974986
home: Some(temp.path().to_path_buf()),
975987
vars,
976988
};
977-
assert_eq!(get_default_virtual_dir(&env), None);
989+
let expected = norm_case(custom.join("env").join("virtual"));
990+
assert_eq!(get_default_virtual_dir(&env), Some(expected));
978991
}
979992
}

0 commit comments

Comments
 (0)