feat: add pet-hatch locator for Hatch environments (Fixes #450)#460
feat: add pet-hatch locator for Hatch environments (Fixes #450)#460karthiknadig wants to merge 15 commits into
Conversation
Adds a new pet-hatch crate that detects Hatch-managed virtual environments so they are no longer misclassified as plain Venv by downstream consumers. Implementation matches Hatch's actual storage layout from src/hatch/env/virtual.py: - Default storage: <data_dir>/env/virtual/<project_name>/<project_id>/<venv_name> (3 levels deep) - HATCH_DATA_DIR env var honoured; never silently falls back to platform default when set \ example from the issue) - Locator inserted before Venv so Hatch claims its envs first - 15 unit tests covering layout depth (rejects 2 / 4 levels), HATCH_DATA_DIR semantics, project-local config, and platform defaults
Performance Report (Linux) ➖
Legend
|
Test Coverage Report (Linux)
Coverage increased! Great work! |
Performance Report (Windows) ✅
Legend
|
Test Coverage Report (Windows)⏳ Coverage analysis in progress... This comment will be updated with results when the analysis completes. |
Performance Report (macOS)
Legend
|
There was a problem hiding this comment.
Pull request overview
This PR adds first-class Hatch environment detection to PET by introducing a new pet-hatch locator and wiring it into the existing locator chain so Hatch-managed venvs are classified as Hatch rather than the generic Venv.
Changes:
- Add new
pet-hatchcrate implementing aHatchlocator with default-storage and workspace-configured discovery/identification. - Register
LocatorKind::HatchandPythonEnvironmentKind::Hatch, and insert Hatch into the locator chain beforeVenv. - Update refresh-state contract expectations to mark Hatch as
RefreshStatePersistence::ConfiguredOnly.
Show a summary per file
| File | Description |
|---|---|
| crates/pet/src/locators.rs | Adds the Hatch locator into the ordered locator chain before Venv. |
| crates/pet/src/jsonrpc.rs | Updates the refresh-state contract test to include LocatorKind::Hatch. |
| crates/pet/Cargo.toml | Adds pet-hatch as a dependency of the main pet crate. |
| crates/pet-hatch/src/lib.rs | Implements Hatch detection (default storage + workspace-configured) and adds unit tests. |
| crates/pet-hatch/Cargo.toml | Defines the new pet-hatch crate and its dependencies. |
| crates/pet-core/src/python_environment.rs | Adds PythonEnvironmentKind::Hatch. |
| crates/pet-core/src/lib.rs | Adds LocatorKind::Hatch. |
| Cargo.lock | Records the new pet-hatch crate and dependency graph updates. |
Copilot's findings
- Files reviewed: 7/8 changed files
- Comments generated: 2
- Cache resolved virtual dirs in configure() so try_from() does not re-parse pyproject.toml / hatch.toml on every executable identification attempt. - Expand ~ (and \C:\Users\kanadig/\) in configured dirs.env.virtual values via pet_fs::path::expand_path so values like '~/.virtualenvs' resolve against the user home. - Build the new cache outside the workspace_virtual_dirs lock to keep disk I/O out of the critical section. - Serialize env-var-mutating tests via a per-binary mutex so cargo's default multi-threaded harness cannot race.
A Hatch project can configure dirs.env.virtual = '~/.virtualenvs' (or any path that overlaps with WORKON_HOME). With Hatch placed after VirtualEnvWrapper, those envs would be claimed as VirtualEnvWrapper before Hatch ever saw them. Move Hatch ahead so it gets first claim on workspace-configured envs.
- 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.
- resolve_project_virtual_dirs(): cache configured paths regardless of whether the directory exists on disk yet. A user may configure 'virtual = .hatch' and create the env later in this process lifetime; we now recognise it without requiring the client to re-send 'configure'. find_envs_in_flat_dir() already handles missing dirs by returning empty. - default_virtual_dir field doc: corrected to reflect that the path may not exist on disk; existence is rechecked at find() time. - Module docs: aligned with implementation. All workspace-configured 'dirs.env.virtual' paths use the flat layout, regardless of whether they are relative, absolute, or use ~ expansion.
…irtual dirs (PR #460) Also fix CI test for tilde expansion to compare paths component-wise (Windows mixed-separator).
Address Copilot review: configure() now parses pyproject.toml and hatch.toml once each per workspace via read_workspace_hatch_sections(), and the env-name allowlist is precomputed into EnvNameMatcher so the try_from() hot path avoids per-call to_lowercase()/format!() allocations.
There was a problem hiding this comment.
Copilot's findings
Comments suppressed due to low confidence (2)
crates/pet-hatch/src/lib.rs:187
try_from()recovers from a poisonedworkspace_virtual_dirsmutex viainto_inner(). If a previous panic occurred while mutating the cache, continuing here can misclassify environments and makes the underlying issue harder to detect. Consider using.expect("workspace_virtual_dirs mutex poisoned")(consistent with other crates) unless there’s a documented reason to continue after poisoning.
let cache = self
.workspace_virtual_dirs
.lock()
.unwrap_or_else(|p| p.into_inner());
'workspaces: for (workspace, virtual_dirs, matcher) in cache.iter() {
crates/pet-hatch/src/lib.rs:247
find()clones the workspace cache after recovering from a poisoned mutex viainto_inner(). Similar toconfigure()/try_from(), this can hide panics and proceed with corrupted state. Align with the rest of the repo by using.expect("workspace_virtual_dirs mutex poisoned")(or add a comment explaining why poison recovery is safe here).
let workspaces = self
.workspace_virtual_dirs
.lock()
.unwrap_or_else(|p| p.into_inner())
.clone();
- Files reviewed: 7/8 changed files
- Comments generated: 2
| pub enum PythonEnvironmentKind { | ||
| Conda, | ||
| Pixi, | ||
| Homebrew, | ||
| Pyenv, | ||
| GlobalPaths, // Python found in global locations like PATH, /usr/bin etc. | ||
| PyenvVirtualEnv, // Pyenv virtualenvs. | ||
| Pipenv, | ||
| Poetry, | ||
| Hatch, | ||
| MacPythonOrg, |
There was a problem hiding this comment.
Copilot's findings
Comments suppressed due to low confidence (2)
crates/pet-hatch/src/lib.rs:181
- Here and elsewhere in this locator,
.lock().unwrap()will panic without context if the mutex is poisoned. For consistency with other PET locators, prefer.lock().expect("workspace_virtual_dirs mutex poisoned")(or equivalent) so failures are actionable.
if classification.is_none() {
let cache = self.workspace_virtual_dirs.lock().unwrap();
'workspaces: for (workspace, virtual_dirs, matcher) in cache.iter() {
crates/pet-hatch/src/lib.rs:237
find()clones the cached workspace state under a mutex using.lock().unwrap(). Consider using.lock().expect("workspace_virtual_dirs mutex poisoned")for a clearer panic message (matching the convention used across the repo).
let workspaces = self.workspace_virtual_dirs.lock().unwrap().clone();
- Files reviewed: 7/8 changed files
- Comments generated: 1
| *self | ||
| .workspace_virtual_dirs | ||
| .lock() | ||
| .expect("workspace_virtual_dirs mutex poisoned") = new_cache; |
| PythonEnvironmentBuilder::new(Some(PythonEnvironmentKind::Hatch)) | ||
| .name(env_name) | ||
| .executable(Some(executable)) | ||
| .version(cfg.version) | ||
| .prefix(Some(prefix.to_path_buf())) | ||
| .symlinks(Some(find_executables(prefix))) | ||
| .project(project_path) | ||
| .build(), |
Fixes #450.
Adds a new
pet-hatchcrate so Hatch-managed virtual environments are no longer misclassified as plainVenvby downstream consumers.Implementation
Matches Hatch's actual storage layout from
src/hatch/env/virtual.py:<data_dir>/env/virtual/<project_name>/<project_id>/<venv_name>(3 levels deep, not 2).HATCH_DATA_DIRis honoured and never silently falls back to the platform default when set.[tool.hatch.dirs.env].virtualinpyproject.tomlor[dirs.env]inhatch.toml. Handles thevirtual = ".hatch"example called out in the issue.Venvso it claims envs first.LocatorKind::Hatchregistered asRefreshStatePersistence::ConfiguredOnly(workspace-driven discovery).Tests
15 unit tests covering:
HATCH_DATA_DIRsemantics — used when set, no fallback to platform default.pyproject.tomlandhatch.toml; rejected withoutdirs.env.virtualconfig.platformdirsdefaults (Linux/macOS/Windows).Replaces #451
This PR replaces #451, which had a fundamental layout bug: it assumed envs are stored 2 levels deep under
<data_dir>/env/virtual(<project-hash>/<env-name>). Per Hatch's source, the actual layout is 3 levels deep (<project_name>/<project_id>/<venv_name>), so #451 would fail to detect any real Hatch env in the default storage location.