From de0d2726353ba2c71b0ee749c5466ab1dae6729f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 6 May 2026 15:51:23 +0000 Subject: [PATCH 1/2] Initial plan From 5e987f66f6694d659440ad4b6af3ca2ae4102af2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 6 May 2026 16:03:20 +0000 Subject: [PATCH 2/2] Add pet-hatch locator for Hatch-managed virtual environments Agent-Logs-Url: https://github.com/microsoft/python-environment-tools/sessions/26aa38e4-33be-45b3-9227-4fdb97b20b24 Co-authored-by: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> --- Cargo.lock | 13 + crates/pet-core/src/lib.rs | 1 + crates/pet-core/src/python_environment.rs | 1 + crates/pet-hatch/Cargo.toml | 17 + crates/pet-hatch/src/lib.rs | 648 ++++++++++++++++++++++ crates/pet/Cargo.toml | 1 + crates/pet/src/jsonrpc.rs | 1 + crates/pet/src/locators.rs | 2 + 8 files changed, 684 insertions(+) create mode 100644 crates/pet-hatch/Cargo.toml create mode 100644 crates/pet-hatch/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 8c49f670..18da3bfc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -448,6 +448,7 @@ dependencies = [ "pet-env-var-path", "pet-fs", "pet-global-virtualenvs", + "pet-hatch", "pet-homebrew", "pet-jsonrpc", "pet-linux-global-python", @@ -548,6 +549,18 @@ dependencies = [ "pet-virtualenv", ] +[[package]] +name = "pet-hatch" +version = "0.1.0" +dependencies = [ + "log", + "msvc_spectre_libs", + "pet-core", + "pet-fs", + "pet-python-utils", + "tempfile", +] + [[package]] name = "pet-homebrew" version = "0.1.0" diff --git a/crates/pet-core/src/lib.rs b/crates/pet-core/src/lib.rs index fe8f4018..a4460405 100644 --- a/crates/pet-core/src/lib.rs +++ b/crates/pet-core/src/lib.rs @@ -43,6 +43,7 @@ pub struct Configuration { #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum LocatorKind { Conda, + Hatch, Homebrew, LinuxGlobal, MacCommandLineTools, diff --git a/crates/pet-core/src/python_environment.rs b/crates/pet-core/src/python_environment.rs index 7bbfe5b8..c6d0c595 100644 --- a/crates/pet-core/src/python_environment.rs +++ b/crates/pet-core/src/python_environment.rs @@ -19,6 +19,7 @@ pub enum PythonEnvironmentKind { PyenvVirtualEnv, // Pyenv virtualenvs. Pipenv, Poetry, + Hatch, MacPythonOrg, MacCommandLineTools, LinuxGlobal, diff --git a/crates/pet-hatch/Cargo.toml b/crates/pet-hatch/Cargo.toml new file mode 100644 index 00000000..f93536e0 --- /dev/null +++ b/crates/pet-hatch/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "pet-hatch" +version.workspace = true +edition.workspace = true +license.workspace = true + +[target.'cfg(target_os = "windows")'.dependencies] +msvc_spectre_libs = { version = "0.1.1", features = ["error"] } + +[dependencies] +pet-core = { path = "../pet-core" } +pet-fs = { path = "../pet-fs" } +pet-python-utils = { path = "../pet-python-utils" } +log = "0.4.21" + +[dev-dependencies] +tempfile = "3.13" diff --git a/crates/pet-hatch/src/lib.rs b/crates/pet-hatch/src/lib.rs new file mode 100644 index 00000000..bab09882 --- /dev/null +++ b/crates/pet-hatch/src/lib.rs @@ -0,0 +1,648 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Hatch environment locator. +//! +//! Hatch () creates standard PEP 405 virtual environments +//! (with a `pyvenv.cfg`), but stores them in a known layout that allows us to +//! distinguish them from generic venvs. By default, Hatch stores environments in: +//! +//! ```text +//! /env/virtual/// +//! ``` +//! +//! where `` is the platform-specific Hatch data directory (see +//! [`hatch_data_dir`]) and `` encodes the originating project's +//! path. Hatch sets the `prompt` field in `pyvenv.cfg` to the environment name. +//! +//! Projects may also be configured to keep environments under a project-local +//! `.hatch/` directory (via `path = ".hatch"` in `hatch.toml` or +//! `[tool.hatch.envs.*]`). In that case the layout is +//! `/.hatch//`. + +use std::{ + fs, + path::{Path, PathBuf}, + sync::{Arc, Mutex}, +}; + +use log::trace; +use pet_core::{ + env::PythonEnv, + os_environment::Environment, + python_environment::{PythonEnvironment, PythonEnvironmentBuilder, PythonEnvironmentKind}, + pyvenv_cfg::PyVenvCfg, + reporter::Reporter, + Configuration, Locator, LocatorKind, RefreshStatePersistence, +}; +use pet_fs::path::norm_case; +use pet_python_utils::executable::{find_executable, find_executables}; + +/// Subdirectory under the Hatch data directory where the default +/// "virtual" environment storage lives. +const VIRTUAL_ENV_SUBDIR: &[&str] = &["env", "virtual"]; + +/// Conventional name of the project-local Hatch environment directory. +const PROJECT_LOCAL_DIR: &str = ".hatch"; + +pub struct Hatch { + /// Directory where Hatch stores managed virtual environments, + /// e.g. `~/.local/share/hatch/env/virtual` on Linux. + virtual_storage_dir: Option, + /// Workspace directories supplied via configuration. Used to discover + /// project-local `.hatch/` environments. + workspace_directories: Arc>>, +} + +impl Default for Hatch { + fn default() -> Self { + Self::from(&pet_core::os_environment::EnvironmentApi::new()) + } +} + +impl Hatch { + pub fn new() -> Self { + Self::default() + } + + pub fn from(environment: &dyn Environment) -> Self { + Self { + virtual_storage_dir: get_hatch_virtual_storage_dir(environment), + workspace_directories: Arc::new(Mutex::new(Vec::new())), + } + } +} + +impl Locator for Hatch { + fn get_kind(&self) -> LocatorKind { + LocatorKind::Hatch + } + + fn refresh_state(&self) -> RefreshStatePersistence { + RefreshStatePersistence::ConfiguredOnly + } + + fn supported_categories(&self) -> Vec { + vec![PythonEnvironmentKind::Hatch] + } + + fn configure(&self, config: &Configuration) { + let mut ws = self + .workspace_directories + .lock() + .expect("workspace_directories mutex poisoned"); + ws.clear(); + if let Some(dirs) = config.workspace_directories.as_ref() { + ws.extend(dirs.iter().cloned()); + } + } + + fn try_from(&self, env: &PythonEnv) -> Option { + // Determine the prefix (sysprefix) of this environment. + let prefix = env.prefix.clone().or_else(|| { + env.executable + .parent() + .and_then(|p| p.parent().map(Path::to_path_buf)) + })?; + + let (project_path, env_name) = + classify_hatch_prefix(&prefix, self.virtual_storage_dir.as_deref())?; + + // A pyvenv.cfg must be present for this to be a valid venv created by Hatch. + let cfg = PyVenvCfg::find(&prefix)?; + // Hatch always writes a `prompt` field; treat its absence as a stronger + // signal that this isn't actually a Hatch-managed env, only when the + // env lives in a path-shape we can't otherwise verify (project-local). + // For the default virtual storage, the path itself is authoritative. + let env_name = cfg.prompt.clone().unwrap_or(env_name); + + trace!( + "Hatch env {} found at {}", + env_name, + env.executable.display() + ); + + Some( + PythonEnvironmentBuilder::new(Some(PythonEnvironmentKind::Hatch)) + .name(Some(env_name)) + .executable(Some(env.executable.clone())) + .version(env.version.clone().or(cfg.version)) + .symlinks(Some(find_executables(&prefix))) + .prefix(Some(prefix)) + .project(project_path) + .build(), + ) + } + + fn find(&self, reporter: &dyn Reporter) { + // 1. Discover environments under the default virtual storage dir. + if let Some(ref storage) = self.virtual_storage_dir { + for env in find_envs_in_virtual_storage(storage) { + reporter.report_environment(&env); + } + } + + // 2. Discover project-local `.hatch/` environments under each workspace dir. + let workspaces = self + .workspace_directories + .lock() + .expect("workspace_directories mutex poisoned") + .clone(); + for workspace in &workspaces { + for env in find_project_local_envs(workspace) { + reporter.report_environment(&env); + } + } + } +} + +/// Determine where Hatch stores its managed virtual environments. +/// +/// Resolution order: +/// 1. `HATCH_DATA_DIR` env var (then append `env/virtual`). +/// 2. Platform-specific platformdirs default for `hatch` (with no app-author), +/// then append `env/virtual`. +fn get_hatch_virtual_storage_dir(environment: &dyn Environment) -> Option { + if let Some(custom) = environment.get_env_var("HATCH_DATA_DIR".to_string()) { + let path = build_virtual_subdir(PathBuf::from(custom)); + if path.is_dir() { + return Some(norm_case(path)); + } + } + let data_dir = hatch_data_dir(environment)?; + let path = build_virtual_subdir(data_dir); + if path.is_dir() { + Some(norm_case(path)) + } else { + None + } +} + +fn build_virtual_subdir(data_dir: PathBuf) -> PathBuf { + let mut path = data_dir; + for segment in VIRTUAL_ENV_SUBDIR { + path.push(segment); + } + path +} + +/// Returns the platform default Hatch data directory. +/// +/// Mirrors `platformdirs.user_data_dir("hatch", appauthor=False)` which is the +/// behavior used by the Hatch CLI itself. +#[cfg(target_os = "linux")] +fn hatch_data_dir(environment: &dyn Environment) -> Option { + if let Some(xdg) = environment.get_env_var("XDG_DATA_HOME".to_string()) { + if !xdg.is_empty() { + return Some(PathBuf::from(xdg).join("hatch")); + } + } + Some( + environment + .get_user_home()? + .join(".local") + .join("share") + .join("hatch"), + ) +} + +#[cfg(target_os = "macos")] +fn hatch_data_dir(environment: &dyn Environment) -> Option { + Some( + environment + .get_user_home()? + .join("Library") + .join("Application Support") + .join("hatch"), + ) +} + +#[cfg(target_os = "windows")] +fn hatch_data_dir(environment: &dyn Environment) -> Option { + let local_app_data = environment.get_env_var("LOCALAPPDATA".to_string())?; + Some(PathBuf::from(local_app_data).join("hatch")) +} + +#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] +fn hatch_data_dir(environment: &dyn Environment) -> Option { + Some( + environment + .get_user_home()? + .join(".local") + .join("share") + .join("hatch"), + ) +} + +/// Classify whether a given prefix is a Hatch environment, returning the +/// inferred project path (if known) and a default name for the env. +/// +/// Returns `Some((project_path, env_name))` when the prefix is recognised as +/// a Hatch environment, and `None` otherwise. +fn classify_hatch_prefix( + prefix: &Path, + virtual_storage_dir: Option<&Path>, +) -> Option<(Option, String)> { + // Case 1: default virtual storage layout: // + if let Some(storage) = virtual_storage_dir { + if let Ok(rel) = prefix.strip_prefix(storage) { + let parts: Vec<_> = rel.iter().collect(); + // Must be exactly two components: project-hash and env-name. + if parts.len() == 2 { + let env_name = parts[1].to_string_lossy().to_string(); + return Some((None, env_name)); + } + } + } + + // Case 2: project-local .hatch/ + let parent = prefix.parent()?; + if parent.file_name().is_some_and(|n| n == PROJECT_LOCAL_DIR) { + if let Some(project) = parent.parent() { + if has_hatch_project_marker(project) { + let env_name = prefix + .file_name() + .map(|n| n.to_string_lossy().to_string()) + .unwrap_or_default(); + return Some((Some(norm_case(project)), env_name)); + } + } + } + + None +} + +/// Returns true if `project` has a Hatch project marker (`hatch.toml` or +/// `[tool.hatch]` in `pyproject.toml`). +fn has_hatch_project_marker(project: &Path) -> bool { + if project.join("hatch.toml").is_file() { + return true; + } + let pyproject = project.join("pyproject.toml"); + if let Ok(contents) = fs::read_to_string(&pyproject) { + // Lightweight check; we don't need a full TOML parser here. Hatch + // configuration always lives under a `[tool.hatch]` table (or + // sub-tables like `[tool.hatch.envs.default]`). + for line in contents.lines() { + let trimmed = line.trim(); + if let Some(rest) = trimmed.strip_prefix('[') { + if let Some(name) = rest.strip_suffix(']') { + let name = name.trim(); + if name == "tool.hatch" || name.starts_with("tool.hatch.") { + return true; + } + } + } + } + } + false +} + +/// Walk `` and report all envs in the `/` layout. +fn find_envs_in_virtual_storage(storage: &Path) -> Vec { + let mut envs = Vec::new(); + let project_dirs = match fs::read_dir(storage) { + Ok(d) => d, + Err(_) => return envs, + }; + for project_entry in project_dirs.filter_map(Result::ok) { + let project_dir = project_entry.path(); + if !project_dir.is_dir() { + continue; + } + let env_dirs = match fs::read_dir(&project_dir) { + Ok(d) => d, + Err(_) => continue, + }; + for env_entry in env_dirs.filter_map(Result::ok) { + let env_dir = env_entry.path(); + if !env_dir.is_dir() { + continue; + } + if let Some(env) = build_env_from_prefix(&env_dir, None) { + envs.push(env); + } + } + } + envs +} + +/// Walk `/.hatch/` and report each environment found. +fn find_project_local_envs(workspace: &Path) -> Vec { + let mut envs = Vec::new(); + let hatch_dir = workspace.join(PROJECT_LOCAL_DIR); + if !hatch_dir.is_dir() { + return envs; + } + if !has_hatch_project_marker(workspace) { + // A bare `.hatch/` without any project marker is unlikely to belong to Hatch. + return envs; + } + let entries = match fs::read_dir(&hatch_dir) { + Ok(d) => d, + Err(_) => return envs, + }; + for entry in entries.filter_map(Result::ok) { + let env_dir = entry.path(); + if !env_dir.is_dir() { + continue; + } + if let Some(env) = build_env_from_prefix(&env_dir, Some(workspace.to_path_buf())) { + envs.push(env); + } + } + envs +} + +fn build_env_from_prefix( + prefix: &Path, + project_path: Option, +) -> Option { + let cfg = PyVenvCfg::find(prefix)?; + let executable = find_executable(prefix)?; + let env_name = cfg + .prompt + .clone() + .or_else(|| prefix.file_name().map(|n| n.to_string_lossy().to_string())); + Some( + 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(), + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashMap; + use tempfile::TempDir; + + struct TestEnv { + home: Option, + vars: HashMap, + } + + impl Environment for TestEnv { + fn get_user_home(&self) -> Option { + self.home.clone() + } + fn get_root(&self) -> Option { + None + } + fn get_env_var(&self, key: String) -> Option { + self.vars.get(&key).cloned() + } + fn get_know_global_search_locations(&self) -> Vec { + vec![] + } + } + + fn write_pyvenv_cfg(prefix: &Path, prompt: &str, version: &str) -> PathBuf { + fs::create_dir_all(prefix).unwrap(); + let cfg = prefix.join("pyvenv.cfg"); + fs::write( + &cfg, + format!("home = /usr/bin\nversion = {version}\nprompt = {prompt}\n"), + ) + .unwrap(); + cfg + } + + fn write_python_exe(prefix: &Path) -> PathBuf { + let bin = prefix.join(if cfg!(windows) { "Scripts" } else { "bin" }); + fs::create_dir_all(&bin).unwrap(); + let exe = bin.join(if cfg!(windows) { + "python.exe" + } else { + "python" + }); + fs::write(&exe, b"").unwrap(); + exe + } + + #[test] + fn kind_and_supported_categories() { + let locator = Hatch { + virtual_storage_dir: None, + workspace_directories: Arc::new(Mutex::new(vec![])), + }; + assert_eq!(locator.get_kind(), LocatorKind::Hatch); + assert_eq!( + locator.supported_categories(), + vec![PythonEnvironmentKind::Hatch] + ); + } + + #[test] + fn try_from_identifies_env_in_default_storage() { + let temp = TempDir::new().unwrap(); + let storage = temp.path().join("env").join("virtual"); + let prefix = storage.join("myproj-AbCdEfGh").join("default"); + write_pyvenv_cfg(&prefix, "default", "3.12.1"); + let exe = write_python_exe(&prefix); + + let locator = Hatch { + virtual_storage_dir: Some(norm_case(&storage)), + workspace_directories: Arc::new(Mutex::new(vec![])), + }; + let env = PythonEnv::new(exe.clone(), Some(prefix.clone()), None); + let identified = locator.try_from(&env).unwrap(); + assert_eq!(identified.kind, Some(PythonEnvironmentKind::Hatch)); + assert_eq!(identified.name, Some("default".to_string())); + assert_eq!(identified.version, Some("3.12.1".to_string())); + assert_eq!(identified.prefix, Some(norm_case(&prefix))); + // Project path is unknown for the default storage layout. + assert!(identified.project.is_none()); + } + + #[test] + fn try_from_returns_none_for_non_hatch_env() { + let temp = TempDir::new().unwrap(); + let prefix = temp.path().join(".venv"); + write_pyvenv_cfg(&prefix, "venv", "3.12.1"); + let exe = write_python_exe(&prefix); + + let locator = Hatch { + virtual_storage_dir: Some(temp.path().join("nonexistent")), + workspace_directories: Arc::new(Mutex::new(vec![])), + }; + let env = PythonEnv::new(exe, Some(prefix), None); + assert!(locator.try_from(&env).is_none()); + } + + #[test] + fn try_from_identifies_project_local_env() { + let temp = TempDir::new().unwrap(); + let project = temp.path().join("project"); + fs::create_dir_all(&project).unwrap(); + // Marker via hatch.toml. + fs::write(project.join("hatch.toml"), b"[envs.default]\n").unwrap(); + let prefix = project.join(".hatch").join("default"); + write_pyvenv_cfg(&prefix, "default", "3.11.0"); + let exe = write_python_exe(&prefix); + + let locator = Hatch { + virtual_storage_dir: None, + workspace_directories: Arc::new(Mutex::new(vec![])), + }; + let env = PythonEnv::new(exe, Some(prefix.clone()), None); + let identified = locator.try_from(&env).unwrap(); + assert_eq!(identified.kind, Some(PythonEnvironmentKind::Hatch)); + assert_eq!(identified.name, Some("default".to_string())); + assert_eq!(identified.project, Some(norm_case(&project))); + } + + #[test] + fn try_from_identifies_project_local_env_via_pyproject_marker() { + let temp = TempDir::new().unwrap(); + let project = temp.path().join("project"); + fs::create_dir_all(&project).unwrap(); + fs::write( + project.join("pyproject.toml"), + b"[project]\nname = \"foo\"\n\n[tool.hatch.envs.default]\n", + ) + .unwrap(); + let prefix = project.join(".hatch").join("default"); + write_pyvenv_cfg(&prefix, "default", "3.11.0"); + let exe = write_python_exe(&prefix); + + let locator = Hatch { + virtual_storage_dir: None, + workspace_directories: Arc::new(Mutex::new(vec![])), + }; + let env = PythonEnv::new(exe, Some(prefix), None); + let identified = locator.try_from(&env).unwrap(); + assert_eq!(identified.kind, Some(PythonEnvironmentKind::Hatch)); + assert_eq!(identified.project, Some(norm_case(&project))); + } + + #[test] + fn try_from_rejects_project_local_without_marker() { + let temp = TempDir::new().unwrap(); + let project = temp.path().join("project"); + fs::create_dir_all(&project).unwrap(); + // No hatch.toml or pyproject.toml marker. + let prefix = project.join(".hatch").join("default"); + write_pyvenv_cfg(&prefix, "default", "3.11.0"); + let exe = write_python_exe(&prefix); + + let locator = Hatch { + virtual_storage_dir: None, + workspace_directories: Arc::new(Mutex::new(vec![])), + }; + let env = PythonEnv::new(exe, Some(prefix), None); + assert!(locator.try_from(&env).is_none()); + } + + #[test] + fn try_from_rejects_wrong_depth_under_storage() { + let temp = TempDir::new().unwrap(); + let storage = temp.path().join("env").join("virtual"); + // Only one component under storage (missing env-name). + let prefix = storage.join("myproj-AbCdEfGh"); + write_pyvenv_cfg(&prefix, "default", "3.12.1"); + let exe = write_python_exe(&prefix); + + let locator = Hatch { + virtual_storage_dir: Some(norm_case(&storage)), + workspace_directories: Arc::new(Mutex::new(vec![])), + }; + let env = PythonEnv::new(exe, Some(prefix), None); + assert!(locator.try_from(&env).is_none()); + } + + #[test] + fn find_reports_envs_in_virtual_storage() { + let temp = TempDir::new().unwrap(); + let storage = temp.path().join("env").join("virtual"); + for name in ["default", "test"] { + let prefix = storage.join("myproj-AbCdEfGh").join(name); + write_pyvenv_cfg(&prefix, name, "3.12.1"); + write_python_exe(&prefix); + } + + let envs = find_envs_in_virtual_storage(&storage); + assert_eq!(envs.len(), 2); + for env in envs { + assert_eq!(env.kind, Some(PythonEnvironmentKind::Hatch)); + } + } + + #[test] + fn find_reports_project_local_envs() { + let temp = TempDir::new().unwrap(); + let project = temp.path().join("proj"); + fs::create_dir_all(&project).unwrap(); + fs::write(project.join("hatch.toml"), b"").unwrap(); + let prefix = project.join(".hatch").join("default"); + write_pyvenv_cfg(&prefix, "default", "3.11.0"); + write_python_exe(&prefix); + + let envs = find_project_local_envs(&project); + assert_eq!(envs.len(), 1); + assert_eq!(envs[0].project, Some(norm_case(&project))); + } + + #[test] + fn find_skips_project_local_without_marker() { + let temp = TempDir::new().unwrap(); + let project = temp.path().join("proj"); + let prefix = project.join(".hatch").join("default"); + write_pyvenv_cfg(&prefix, "default", "3.11.0"); + write_python_exe(&prefix); + + let envs = find_project_local_envs(&project); + assert!(envs.is_empty()); + } + + #[cfg(target_os = "linux")] + #[test] + fn data_dir_uses_xdg_data_home_when_set() { + let temp = TempDir::new().unwrap(); + let mut vars = HashMap::new(); + vars.insert( + "XDG_DATA_HOME".to_string(), + temp.path().to_string_lossy().to_string(), + ); + let env = TestEnv { + home: Some(PathBuf::from("/home/test")), + vars, + }; + assert_eq!(hatch_data_dir(&env), Some(temp.path().join("hatch"))); + } + + #[cfg(target_os = "linux")] + #[test] + fn data_dir_falls_back_to_local_share() { + let env = TestEnv { + home: Some(PathBuf::from("/home/test")), + vars: HashMap::new(), + }; + assert_eq!( + hatch_data_dir(&env), + Some(PathBuf::from("/home/test/.local/share/hatch")) + ); + } + + #[test] + fn virtual_storage_dir_honours_hatch_data_dir_env_var() { + let temp = TempDir::new().unwrap(); + let virt = temp.path().join("env").join("virtual"); + fs::create_dir_all(&virt).unwrap(); + let mut vars = HashMap::new(); + vars.insert( + "HATCH_DATA_DIR".to_string(), + temp.path().to_string_lossy().to_string(), + ); + let env = TestEnv { + home: Some(temp.path().to_path_buf()), + vars, + }; + assert_eq!(get_hatch_virtual_storage_dir(&env), Some(norm_case(virt))); + } +} diff --git a/crates/pet/Cargo.toml b/crates/pet/Cargo.toml index 36515074..ee5ac5be 100644 --- a/crates/pet/Cargo.toml +++ b/crates/pet/Cargo.toml @@ -26,6 +26,7 @@ pet-jsonrpc = { path = "../pet-jsonrpc" } pet-fs = { path = "../pet-fs" } pet-pyenv = { path = "../pet-pyenv" } pet-poetry = { path = "../pet-poetry" } +pet-hatch = { path = "../pet-hatch" } pet-reporter = { path = "../pet-reporter" } pet-virtualenvwrapper = { path = "../pet-virtualenvwrapper" } pet-python-utils = { path = "../pet-python-utils" } diff --git a/crates/pet/src/jsonrpc.rs b/crates/pet/src/jsonrpc.rs index f85c5eb7..6a395f2e 100644 --- a/crates/pet/src/jsonrpc.rs +++ b/crates/pet/src/jsonrpc.rs @@ -2067,6 +2067,7 @@ mod tests { LocatorKind::VirtualEnvWrapper, RefreshStatePersistence::Stateless, ), + (LocatorKind::Hatch, RefreshStatePersistence::ConfiguredOnly), (LocatorKind::Venv, RefreshStatePersistence::Stateless), (LocatorKind::VirtualEnv, RefreshStatePersistence::Stateless), #[cfg(unix)] diff --git a/crates/pet/src/locators.rs b/crates/pet/src/locators.rs index 6a78e7e5..c25473bf 100644 --- a/crates/pet/src/locators.rs +++ b/crates/pet/src/locators.rs @@ -10,6 +10,7 @@ use pet_core::python_environment::{ PythonEnvironment, PythonEnvironmentBuilder, PythonEnvironmentKind, }; use pet_core::Locator; +use pet_hatch::Hatch; use pet_linux_global_python::LinuxGlobalPython; use pet_mac_commandlinetools::MacCmdLineTools; use pet_mac_python_org::MacPythonOrg; @@ -69,6 +70,7 @@ pub fn create_locators( locators.push(poetry_locator); locators.push(Arc::new(PipEnv::from(environment))); locators.push(Arc::new(VirtualEnvWrapper::from(environment))); + locators.push(Arc::new(Hatch::from(environment))); locators.push(Arc::new(Venv::new())); // VirtualEnv is the most generic, hence should be the last. locators.push(Arc::new(VirtualEnv::new()));