diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index d77cee35e797..e0e7f4b0d24b 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -448,6 +448,165 @@ pub(crate) enum ProjectInterpreter { Environment(PythonEnvironment), } +impl ProjectInterpreter { + /// Discover the interpreter to use in the current [`Workspace`]. + pub(crate) async fn discover( + workspace: &Workspace, + project_dir: &Path, + python_request: Option, + python_preference: PythonPreference, + python_downloads: PythonDownloads, + connectivity: Connectivity, + native_tls: bool, + allow_insecure_host: &[TrustedHost], + install_mirrors: PythonInstallMirrors, + no_config: bool, + cache: &Cache, + printer: Printer, + ) -> Result { + // Resolve the Python request and requirement for the workspace. + let WorkspacePython { + source, + python_request, + requires_python, + } = WorkspacePython::from_request(python_request, Some(workspace), project_dir, no_config) + .await?; + + // Read from the virtual environment first. + let venv = workspace.venv(); + match PythonEnvironment::from_root(&venv, cache) { + Ok(venv) => { + if python_request.as_ref().map_or(true, |request| { + if request.satisfied(venv.interpreter(), cache) { + debug!( + "The virtual environment's Python version satisfies `{}`", + request.to_canonical_string() + ); + true + } else { + debug!( + "The virtual environment's Python version does not satisfy `{}`", + request.to_canonical_string() + ); + false + } + }) { + if let Some(requires_python) = requires_python.as_ref() { + if requires_python.contains(venv.interpreter().python_version()) { + return Ok(Self::Environment(venv)); + } + debug!( + "The virtual environment's Python version does not meet the project's Python requirement: `{requires_python}`" + ); + } else { + return Ok(Self::Environment(venv)); + } + } + } + Err(uv_python::Error::MissingEnvironment(_)) => {} + Err(uv_python::Error::InvalidEnvironment(inner)) => { + // If there's an invalid environment with existing content, we error instead of + // deleting it later on + match inner.kind { + InvalidEnvironmentKind::NotDirectory => { + return Err(ProjectError::InvalidProjectEnvironmentDir( + venv, + inner.kind.to_string(), + )) + } + InvalidEnvironmentKind::MissingExecutable(_) => { + if fs_err::read_dir(&venv).is_ok_and(|mut dir| dir.next().is_some()) { + return Err(ProjectError::InvalidProjectEnvironmentDir( + venv, + "it is not a valid Python environment (no Python executable was found)" + .to_string(), + )); + } + } + // If the environment is an empty directory, it's fine to use + InvalidEnvironmentKind::Empty => {} + }; + } + Err(uv_python::Error::Query(uv_python::InterpreterError::NotFound(path))) => { + if path.is_symlink() { + let target_path = fs_err::read_link(&path)?; + warn_user!( + "Ignoring existing virtual environment linked to non-existent Python interpreter: {} -> {}", + path.user_display().cyan(), + target_path.user_display().cyan(), + ); + } + } + Err(err) => return Err(err.into()), + }; + + let client_builder = BaseClientBuilder::default() + .connectivity(connectivity) + .native_tls(native_tls) + .allow_insecure_host(allow_insecure_host.to_vec()); + + let reporter = PythonDownloadReporter::single(printer); + + // Locate the Python interpreter to use in the environment + let python = PythonInstallation::find_or_download( + python_request.as_ref(), + EnvironmentPreference::OnlySystem, + python_preference, + python_downloads, + &client_builder, + cache, + Some(&reporter), + install_mirrors.python_install_mirror.as_deref(), + install_mirrors.pypy_install_mirror.as_deref(), + ) + .await?; + + let managed = python.source().is_managed(); + let implementation = python.implementation(); + let interpreter = python.into_interpreter(); + + if managed { + writeln!( + printer.stderr(), + "Using {} {}", + implementation.pretty(), + interpreter.python_version().cyan() + )?; + } else { + writeln!( + printer.stderr(), + "Using {} {} interpreter at: {}", + implementation.pretty(), + interpreter.python_version(), + interpreter.sys_executable().user_display().cyan() + )?; + } + + if let Some(requires_python) = requires_python.as_ref() { + validate_requires_python(&interpreter, Some(workspace), requires_python, &source)?; + } + + Ok(Self::Interpreter(interpreter)) + } + + /// Convert the [`ProjectInterpreter`] into an [`Interpreter`]. + pub(crate) fn into_interpreter(self) -> Interpreter { + match self { + ProjectInterpreter::Interpreter(interpreter) => interpreter, + ProjectInterpreter::Environment(venv) => venv.into_interpreter(), + } + } +} + +/// The source of a `Requires-Python` specifier. +#[derive(Debug, Clone)] +pub(crate) enum RequiresPythonSource { + /// From the PEP 723 inline script metadata. + Script, + /// From a `pyproject.toml` in a workspace. + Project, +} + #[derive(Debug, Clone)] pub(crate) enum PythonRequestSource { /// The request was provided by the user. @@ -543,15 +702,6 @@ impl WorkspacePython { } } -/// The source of a `Requires-Python` specifier. -#[derive(Debug, Clone)] -pub(crate) enum RequiresPythonSource { - /// From the PEP 723 inline script metadata. - Script, - /// From a `pyproject.toml` in a workspace. - Project, -} - /// The resolved Python request and requirement for a [`Pep723Script`] #[derive(Debug, Clone)] pub(crate) struct ScriptPython { @@ -617,156 +767,6 @@ impl ScriptPython { } } -impl ProjectInterpreter { - /// Discover the interpreter to use in the current [`Workspace`]. - pub(crate) async fn discover( - workspace: &Workspace, - project_dir: &Path, - python_request: Option, - python_preference: PythonPreference, - python_downloads: PythonDownloads, - connectivity: Connectivity, - native_tls: bool, - allow_insecure_host: &[TrustedHost], - install_mirrors: PythonInstallMirrors, - no_config: bool, - cache: &Cache, - printer: Printer, - ) -> Result { - // Resolve the Python request and requirement for the workspace. - let WorkspacePython { - source, - python_request, - requires_python, - } = WorkspacePython::from_request(python_request, Some(workspace), project_dir, no_config) - .await?; - - // Read from the virtual environment first. - let venv = workspace.venv(); - match PythonEnvironment::from_root(&venv, cache) { - Ok(venv) => { - if python_request.as_ref().map_or(true, |request| { - if request.satisfied(venv.interpreter(), cache) { - debug!( - "The virtual environment's Python version satisfies `{}`", - request.to_canonical_string() - ); - true - } else { - debug!( - "The virtual environment's Python version does not satisfy `{}`", - request.to_canonical_string() - ); - false - } - }) { - if let Some(requires_python) = requires_python.as_ref() { - if requires_python.contains(venv.interpreter().python_version()) { - return Ok(Self::Environment(venv)); - } - debug!( - "The virtual environment's Python version does not meet the project's Python requirement: `{requires_python}`" - ); - } else { - return Ok(Self::Environment(venv)); - } - } - } - Err(uv_python::Error::MissingEnvironment(_)) => {} - Err(uv_python::Error::InvalidEnvironment(inner)) => { - // If there's an invalid environment with existing content, we error instead of - // deleting it later on - match inner.kind { - InvalidEnvironmentKind::NotDirectory => { - return Err(ProjectError::InvalidProjectEnvironmentDir( - venv, - inner.kind.to_string(), - )) - } - InvalidEnvironmentKind::MissingExecutable(_) => { - if fs_err::read_dir(&venv).is_ok_and(|mut dir| dir.next().is_some()) { - return Err(ProjectError::InvalidProjectEnvironmentDir( - venv, - "it is not a valid Python environment (no Python executable was found)" - .to_string(), - )); - } - } - // If the environment is an empty directory, it's fine to use - InvalidEnvironmentKind::Empty => {} - }; - } - Err(uv_python::Error::Query(uv_python::InterpreterError::NotFound(path))) => { - if path.is_symlink() { - let target_path = fs_err::read_link(&path)?; - warn_user!( - "Ignoring existing virtual environment linked to non-existent Python interpreter: {} -> {}", - path.user_display().cyan(), - target_path.user_display().cyan(), - ); - } - } - Err(err) => return Err(err.into()), - }; - - let client_builder = BaseClientBuilder::default() - .connectivity(connectivity) - .native_tls(native_tls) - .allow_insecure_host(allow_insecure_host.to_vec()); - - let reporter = PythonDownloadReporter::single(printer); - - // Locate the Python interpreter to use in the environment - let python = PythonInstallation::find_or_download( - python_request.as_ref(), - EnvironmentPreference::OnlySystem, - python_preference, - python_downloads, - &client_builder, - cache, - Some(&reporter), - install_mirrors.python_install_mirror.as_deref(), - install_mirrors.pypy_install_mirror.as_deref(), - ) - .await?; - - let managed = python.source().is_managed(); - let implementation = python.implementation(); - let interpreter = python.into_interpreter(); - - if managed { - writeln!( - printer.stderr(), - "Using {} {}", - implementation.pretty(), - interpreter.python_version().cyan() - )?; - } else { - writeln!( - printer.stderr(), - "Using {} {} interpreter at: {}", - implementation.pretty(), - interpreter.python_version(), - interpreter.sys_executable().user_display().cyan() - )?; - } - - if let Some(requires_python) = requires_python.as_ref() { - validate_requires_python(&interpreter, Some(workspace), requires_python, &source)?; - } - - Ok(Self::Interpreter(interpreter)) - } - - /// Convert the [`ProjectInterpreter`] into an [`Interpreter`]. - pub(crate) fn into_interpreter(self) -> Interpreter { - match self { - ProjectInterpreter::Interpreter(interpreter) => interpreter, - ProjectInterpreter::Environment(venv) => venv.into_interpreter(), - } - } -} - /// Initialize a virtual environment for the current project. pub(crate) async fn get_or_init_environment( workspace: &Workspace,