Tear miette out of the uv venv command (#14546)

This has some changes to the user-facing output, but makes it more
consistent with the rest of uv.
This commit is contained in:
Zanie Blue 2025-07-11 07:47:06 -05:00
parent dff9ced40a
commit dbaec0537a
3 changed files with 62 additions and 168 deletions

View file

@ -4,9 +4,7 @@ use std::str::FromStr;
use std::sync::Arc;
use std::vec;
use anstream::eprint;
use anyhow::Result;
use miette::{Diagnostic, IntoDiagnostic};
use owo_colors::OwoColorize;
use thiserror::Error;
@ -42,6 +40,21 @@ use crate::settings::NetworkSettings;
use super::project::default_dependency_groups;
#[derive(Error, Debug)]
enum VenvError {
#[error("Failed to create virtual environment")]
Creation(#[source] uv_virtualenv::Error),
#[error("Failed to install seed packages into virtual environment")]
Seed(#[source] AnyErrorBuild),
#[error("Failed to extract interpreter tags for installing seed packages")]
Tags(#[source] uv_platform_tags::TagsError),
#[error("Failed to resolve `--find-links` entry")]
FlatIndex(#[source] uv_client::FlatIndexError),
}
/// Create a virtual environment.
#[allow(clippy::unnecessary_wraps, clippy::fn_params_excessive_bools)]
pub(crate) async fn venv(
@ -70,89 +83,6 @@ pub(crate) async fn venv(
relocatable: bool,
preview: PreviewMode,
) -> Result<ExitStatus> {
match venv_impl(
project_dir,
path,
python_request,
install_mirrors,
link_mode,
index_locations,
index_strategy,
dependency_metadata,
keyring_provider,
network_settings,
prompt,
system_site_packages,
seed,
python_preference,
python_downloads,
allow_existing,
exclude_newer,
concurrency,
no_config,
no_project,
cache,
printer,
relocatable,
preview,
)
.await
{
Ok(status) => Ok(status),
Err(err) => {
eprint!("{err:?}");
Ok(ExitStatus::Failure)
}
}
}
#[derive(Error, Debug, Diagnostic)]
enum VenvError {
#[error("Failed to create virtualenv")]
#[diagnostic(code(uv::venv::creation))]
Creation(#[source] uv_virtualenv::Error),
#[error("Failed to install seed packages")]
#[diagnostic(code(uv::venv::seed))]
Seed(#[source] AnyErrorBuild),
#[error("Failed to extract interpreter tags")]
#[diagnostic(code(uv::venv::tags))]
Tags(#[source] uv_platform_tags::TagsError),
#[error("Failed to resolve `--find-links` entry")]
#[diagnostic(code(uv::venv::flat_index))]
FlatIndex(#[source] uv_client::FlatIndexError),
}
/// Create a virtual environment.
#[allow(clippy::fn_params_excessive_bools)]
async fn venv_impl(
project_dir: &Path,
path: Option<PathBuf>,
python_request: Option<PythonRequest>,
install_mirrors: PythonInstallMirrors,
link_mode: LinkMode,
index_locations: &IndexLocations,
index_strategy: IndexStrategy,
dependency_metadata: DependencyMetadata,
keyring_provider: KeyringProviderType,
network_settings: &NetworkSettings,
prompt: uv_virtualenv::Prompt,
system_site_packages: bool,
seed: bool,
python_preference: PythonPreference,
python_downloads: PythonDownloads,
allow_existing: bool,
exclude_newer: Option<ExcludeNewer>,
concurrency: Concurrency,
no_config: bool,
no_project: bool,
cache: &Cache,
printer: Printer,
relocatable: bool,
preview: PreviewMode,
) -> miette::Result<ExitStatus> {
let workspace_cache = WorkspaceCache::default();
let project = if no_project {
None
@ -206,7 +136,7 @@ async fn venv_impl(
// If the default dependency-groups demand a higher requires-python
// we should bias an empty venv to that to avoid churn.
let default_groups = match &project {
Some(project) => default_dependency_groups(project.pyproject_toml()).into_diagnostic()?,
Some(project) => default_dependency_groups(project.pyproject_toml())?,
None => DefaultGroups::default(),
};
let groups = DependencyGroups::default().with_defaults(default_groups);
@ -221,8 +151,7 @@ async fn venv_impl(
project_dir,
no_config,
)
.await
.into_diagnostic()?;
.await?;
// Locate the Python interpreter to use in the environment
let interpreter = {
@ -239,9 +168,8 @@ async fn venv_impl(
install_mirrors.python_downloads_json_url.as_deref(),
preview,
)
.await
.into_diagnostic()?;
report_interpreter(&python, false, printer).into_diagnostic()?;
.await?;
report_interpreter(&python, false, printer)?;
python.into_interpreter()
};
@ -268,8 +196,7 @@ async fn venv_impl(
"Creating virtual environment {}at: {}",
if seed { "with seed packages " } else { "" },
path.user_display().cyan()
)
.into_diagnostic()?;
)?;
let upgradeable = preview.is_enabled()
&& python_request
@ -307,8 +234,7 @@ async fn venv_impl(
}
// Instantiate a client.
let client = RegistryClientBuilder::try_from(client_builder)
.into_diagnostic()?
let client = RegistryClientBuilder::try_from(client_builder)?
.cache(cache.clone())
.index_locations(index_locations)
.index_strategy(index_strategy)
@ -400,9 +326,7 @@ async fn venv_impl(
.map_err(|err| VenvError::Seed(err.into()))?;
let changelog = Changelog::from_installed(installed);
DefaultInstallLogger
.on_complete(&changelog, printer)
.into_diagnostic()?;
DefaultInstallLogger.on_complete(&changelog, printer)?;
}
// Determine the appropriate activation command.
@ -431,7 +355,7 @@ async fn venv_impl(
Some(Shell::Cmd) => Some(shlex_windows(venv.scripts().join("activate"), Shell::Cmd)),
};
if let Some(act) = activation {
writeln!(printer.stderr(), "Activate with: {}", act.green()).into_diagnostic()?;
writeln!(printer.stderr(), "Activate with: {}", act.green())?;
}
Ok(ExitStatus::Success)

View file

@ -17411,11 +17411,11 @@ fn compile_broken_active_venv() -> Result<()> {
.arg(&broken_system_python)
.arg("venv2"), @r"
success: false
exit_code: 1
exit_code: 2
----- stdout -----
----- stderr -----
× No interpreter found at path `python3.14159`
error: No interpreter found at path `python3.14159`
");
// Simulate a removed Python interpreter

View file

@ -656,13 +656,13 @@ fn create_venv_respects_group_requires_python() -> Result<()> {
uv_snapshot!(context.filters(), context.venv().arg("--python").arg("3.11"), @r"
success: false
exit_code: 1
exit_code: 2
----- stdout -----
----- stderr -----
× Found conflicting Python requirements:
- foo: <3.12
- foo:dev: >=3.12
error: Found conflicting Python requirements:
- foo: <3.12
- foo:dev: >=3.12
"
);
@ -808,7 +808,7 @@ fn seed_older_python_version() {
#[test]
fn create_venv_unknown_python_minor() {
let context = TestContext::new_with_versions(&["3.12"]);
let context = TestContext::new_with_versions(&["3.12"]).with_filtered_python_sources();
let mut command = context.venv();
command
@ -819,34 +819,22 @@ fn create_venv_unknown_python_minor() {
// Unset this variable to force what the user would see
.env_remove(EnvVars::UV_TEST_PYTHON_PATH);
if cfg!(windows) {
uv_snapshot!(&mut command, @r###"
uv_snapshot!(context.filters(), &mut command, @r"
success: false
exit_code: 1
exit_code: 2
----- stdout -----
----- stderr -----
× No interpreter found for Python 3.100 in managed installations, search path, or registry
"###
error: No interpreter found for Python 3.100 in [PYTHON SOURCES]
"
);
} else {
uv_snapshot!(&mut command, @r###"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
× No interpreter found for Python 3.100 in managed installations or search path
"###
);
}
context.venv.assert(predicates::path::missing());
}
#[test]
fn create_venv_unknown_python_patch() {
let context = TestContext::new_with_versions(&["3.12"]);
let context = TestContext::new_with_versions(&["3.12"]).with_filtered_python_sources();
let mut command = context.venv();
command
@ -857,27 +845,15 @@ fn create_venv_unknown_python_patch() {
// Unset this variable to force what the user would see
.env_remove(EnvVars::UV_TEST_PYTHON_PATH);
if cfg!(windows) {
uv_snapshot!(&mut command, @r###"
uv_snapshot!(context.filters(), &mut command, @r"
success: false
exit_code: 1
exit_code: 2
----- stdout -----
----- stderr -----
× No interpreter found for Python 3.12.100 in managed installations, search path, or registry
"###
);
} else {
uv_snapshot!(&mut command, @r"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
× No interpreter found for Python 3.12.100 in managed installations or search path
error: No interpreter found for Python 3.12.[X] in [PYTHON SOURCES]
"
);
}
context.venv.assert(predicates::path::missing());
}
@ -915,19 +891,17 @@ fn file_exists() -> Result<()> {
uv_snapshot!(context.filters(), context.venv()
.arg(context.venv.as_os_str())
.arg("--python")
.arg("3.12"), @r###"
.arg("3.12"), @r"
success: false
exit_code: 1
exit_code: 2
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtual environment at: .venv
uv::venv::creation
× Failed to create virtualenv
File exists at `.venv`
"###
error: Failed to create virtual environment
Caused by: File exists at `.venv`
"
);
Ok(())
@ -970,19 +944,17 @@ fn non_empty_dir_exists() -> Result<()> {
uv_snapshot!(context.filters(), context.venv()
.arg(context.venv.as_os_str())
.arg("--python")
.arg("3.12"), @r###"
.arg("3.12"), @r"
success: false
exit_code: 1
exit_code: 2
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtual environment at: .venv
uv::venv::creation
× Failed to create virtualenv
The directory `.venv` exists, but it's not a virtual environment
"###
error: Failed to create virtual environment
Caused by: The directory `.venv` exists, but it's not a virtual environment
"
);
Ok(())
@ -1000,19 +972,17 @@ fn non_empty_dir_exists_allow_existing() -> Result<()> {
uv_snapshot!(context.filters(), context.venv()
.arg(context.venv.as_os_str())
.arg("--python")
.arg("3.12"), @r###"
.arg("3.12"), @r"
success: false
exit_code: 1
exit_code: 2
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtual environment at: .venv
uv::venv::creation
× Failed to create virtualenv
The directory `.venv` exists, but it's not a virtual environment
"###
error: Failed to create virtual environment
Caused by: The directory `.venv` exists, but it's not a virtual environment
"
);
uv_snapshot!(context.filters(), context.venv()