From 0c25a26ee3f8c507c077ded730131e9a075a994f Mon Sep 17 00:00:00 2001 From: Justin Chapman Date: Fri, 7 Mar 2025 08:47:42 -0700 Subject: [PATCH] respect --project during uv python find (#11990) ## Summary this pr updates a couple discovery-related functions to accept a directory argument, used as the root directory when searching for a virtual environment ## Test Plan todo --- crates/uv-python/src/discovery.rs | 110 ++++++++++++++++++++------- crates/uv-python/src/environment.rs | 1 + crates/uv-python/src/installation.rs | 8 +- crates/uv-python/src/virtualenv.rs | 8 +- 4 files changed, 96 insertions(+), 31 deletions(-) diff --git a/crates/uv-python/src/discovery.rs b/crates/uv-python/src/discovery.rs index b5a4972d69f2..f6ffc9f7b0bd 100644 --- a/crates/uv-python/src/discovery.rs +++ b/crates/uv-python/src/discovery.rs @@ -29,7 +29,7 @@ use crate::managed::ManagedPythonInstallations; use crate::microsoft_store::find_microsoft_store_pythons; use crate::virtualenv::Error as VirtualEnvError; use crate::virtualenv::{ - conda_environment_from_env, virtualenv_from_env, virtualenv_from_working_dir, + conda_environment_from_env, virtualenv_from_dir, virtualenv_from_env, virtualenv_python_executable, CondaEnvironmentKind, }; #[cfg(windows)] @@ -251,6 +251,7 @@ pub enum Error { /// /// Notably, "system" environments are excluded. See [`python_executables_from_installed`]. fn python_executables_from_virtual_environments<'a>( + root_directory: &'a Path, ) -> impl Iterator> + 'a { let from_active_environment = iter::once_with(|| { virtualenv_from_env() @@ -269,8 +270,8 @@ fn python_executables_from_virtual_environments<'a>( }) .flatten(); - let from_discovered_environment = iter::once_with(|| { - virtualenv_from_working_dir() + let from_discovered_environment = iter::once_with(move || { + virtualenv_from_dir(root_directory) .map(|path| { path.map(virtualenv_python_executable) .map(|path| (PythonSource::DiscoveredEnvironment, path)) @@ -420,6 +421,7 @@ fn python_executables<'a>( implementation: Option<&'a ImplementationName>, environments: EnvironmentPreference, preference: PythonPreference, + root_directory: &'a Path, ) -> Box> + 'a> { // Always read from `UV_INTERNAL__PARENT_INTERPRETER` — it could be a system interpreter let from_parent_interpreter = iter::once_with(|| { @@ -438,7 +440,7 @@ fn python_executables<'a>( }) .flatten(); - let from_virtual_environments = python_executables_from_virtual_environments(); + let from_virtual_environments = python_executables_from_virtual_environments(root_directory); let from_installed = python_executables_from_installed(version, implementation, preference); // Limit the search to the relevant environment preference; this avoids unnecessary work like @@ -615,16 +617,22 @@ fn python_interpreters<'a>( environments: EnvironmentPreference, preference: PythonPreference, cache: &'a Cache, + root_directory: &'a Path, ) -> impl Iterator> + 'a { python_interpreters_from_executables( // Perform filtering on the discovered executables based on their source. This avoids // unnecessary interpreter queries, which are generally expensive. We'll filter again // with `interpreter_satisfies_environment_preference` after querying. - python_executables(version, implementation, environments, preference).filter_ok( - move |(source, path)| { - source_satisfies_environment_preference(*source, path, environments) - }, - ), + python_executables( + version, + implementation, + environments, + preference, + root_directory, + ) + .filter_ok(move |(source, path)| { + source_satisfies_environment_preference(*source, path, environments) + }), cache, ) .filter_ok(move |(source, interpreter)| { @@ -856,12 +864,25 @@ fn python_interpreters_with_executable_name<'a>( ) } +/// Iterate over all Python installations that satisfy the given request, starting from the current directory. +/// +/// See [`find_python_installations`] for implementation details. +pub fn find_python_installations_at_current_directory<'a>( + request: &'a PythonRequest, + environments: EnvironmentPreference, + preference: PythonPreference, + cache: &'a Cache, +) -> Box> + 'a> { + find_python_installations(request, environments, preference, cache) +} + /// Iterate over all Python installations that satisfy the given request. pub fn find_python_installations<'a>( request: &'a PythonRequest, environments: EnvironmentPreference, preference: PythonPreference, cache: &'a Cache, + root_directory: &'a Path, ) -> Box> + 'a> { let sources = DiscoveryPreferences { python_preference: preference, @@ -942,8 +963,15 @@ pub fn find_python_installations<'a>( } PythonRequest::Any => Box::new({ debug!("Searching for any Python interpreter in {sources}"); - python_interpreters(&VersionRequest::Any, None, environments, preference, cache) - .map_ok(|tuple| Ok(PythonInstallation::from_tuple(tuple))) + python_interpreters( + &VersionRequest::Any, + None, + environments, + preference, + cache, + root_directory, + ) + .map_ok(|tuple| Ok(PythonInstallation::from_tuple(tuple))) }), PythonRequest::Default => Box::new({ debug!("Searching for default Python interpreter in {sources}"); @@ -953,6 +981,7 @@ pub fn find_python_installations<'a>( environments, preference, cache, + root_directory, ) .map_ok(|tuple| Ok(PythonInstallation::from_tuple(tuple))) }), @@ -962,8 +991,15 @@ pub fn find_python_installations<'a>( }; Box::new({ debug!("Searching for {request} in {sources}"); - python_interpreters(version, None, environments, preference, cache) - .map_ok(|tuple| Ok(PythonInstallation::from_tuple(tuple))) + python_interpreters( + version, + None, + environments, + preference, + cache, + root_directory, + ) + .map_ok(|tuple| Ok(PythonInstallation::from_tuple(tuple))) }) } PythonRequest::Implementation(implementation) => Box::new({ @@ -974,6 +1010,7 @@ pub fn find_python_installations<'a>( environments, preference, cache, + root_directory, ) .filter_ok(|(_source, interpreter)| { interpreter @@ -994,6 +1031,7 @@ pub fn find_python_installations<'a>( environments, preference, cache, + root_directory, ) .filter_ok(|(_source, interpreter)| { interpreter @@ -1017,6 +1055,7 @@ pub fn find_python_installations<'a>( environments, preference, cache, + root_directory, ) .filter_ok(|(_source, interpreter)| request.satisfied_by_interpreter(interpreter)) .map_ok(|tuple| Ok(PythonInstallation::from_tuple(tuple))) @@ -1034,8 +1073,10 @@ pub(crate) fn find_python_installation( environments: EnvironmentPreference, preference: PythonPreference, cache: &Cache, + root_directory: &Path, ) -> Result { - let installations = find_python_installations(request, environments, preference, cache); + let installations = + find_python_installations(request, environments, preference, cache, root_directory); let mut first_prerelease = None; let mut first_error = None; for result in installations { @@ -1136,7 +1177,13 @@ pub fn find_best_python_installation( // First, check for an exact match (or the first available version if no Python version was provided) debug!("Looking for exact match for request {request}"); - let result = find_python_installation(request, environments, preference, cache); + let result = find_python_installation( + request, + environments, + preference, + cache, + crate::current_dir()?.as_path(), + ); match result { Ok(Ok(installation)) => { warn_on_unsupported_python(installation.interpreter()); @@ -1164,7 +1211,13 @@ pub fn find_best_python_installation( _ => None, } { debug!("Looking for relaxed patch version {request}"); - let result = find_python_installation(&request, environments, preference, cache); + let result = find_python_installation( + &request, + environments, + preference, + cache, + crate::current_dir()?.as_path(), + ); match result { Ok(Ok(installation)) => { warn_on_unsupported_python(installation.interpreter()); @@ -1180,16 +1233,21 @@ pub fn find_best_python_installation( // If a Python version was requested but cannot be fulfilled, just take any version debug!("Looking for a default Python installation"); let request = PythonRequest::Default; - Ok( - find_python_installation(&request, environments, preference, cache)?.map_err(|err| { - // Use a more general error in this case since we looked for multiple versions - PythonNotFound { - request, - python_preference: err.python_preference, - environment_preference: err.environment_preference, - } - }), - ) + Ok(find_python_installation( + &request, + environments, + preference, + cache, + crate::current_dir()?.as_path(), + )? + .map_err(|err| { + // Use a more general error in this case since we looked for multiple versions + PythonNotFound { + request, + python_preference: err.python_preference, + environment_preference: err.environment_preference, + } + })) } /// Display a warning if the Python version of the [`Interpreter`] is unsupported by uv. diff --git a/crates/uv-python/src/environment.rs b/crates/uv-python/src/environment.rs index 311f0ee1a0cc..5fc7d30d0e74 100644 --- a/crates/uv-python/src/environment.rs +++ b/crates/uv-python/src/environment.rs @@ -153,6 +153,7 @@ impl PythonEnvironment { // Ignore managed installations when looking for environments PythonPreference::OnlySystem, cache, + crate::current_dir()?.as_path(), )? { Ok(installation) => installation, Err(err) => return Err(EnvironmentNotFound::from(err).into()), diff --git a/crates/uv-python/src/installation.rs b/crates/uv-python/src/installation.rs index c1ad57c1ef14..ad823533baac 100644 --- a/crates/uv-python/src/installation.rs +++ b/crates/uv-python/src/installation.rs @@ -55,7 +55,13 @@ impl PythonInstallation { preference: PythonPreference, cache: &Cache, ) -> Result { - let installation = find_python_installation(request, environments, preference, cache)??; + let installation = find_python_installation( + request, + environments, + preference, + cache, + crate::current_dir()?.as_path(), + )??; Ok(installation) } diff --git a/crates/uv-python/src/virtualenv.rs b/crates/uv-python/src/virtualenv.rs index 4fb989b7ccc5..76279510dace 100644 --- a/crates/uv-python/src/virtualenv.rs +++ b/crates/uv-python/src/virtualenv.rs @@ -118,13 +118,13 @@ pub(crate) fn conda_environment_from_env(kind: CondaEnvironmentKind) -> Option

Result, Error> { - let current_dir = crate::current_dir()?; +pub(crate) fn virtualenv_from_dir(dir: impl AsRef) -> Result, Error> { + let dir = dir.as_ref(); - for dir in current_dir.ancestors() { + for dir in dir.ancestors() { // If we're _within_ a virtualenv, return it. if dir.join("pyvenv.cfg").is_file() { return Ok(Some(dir.to_path_buf()));