Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 17 additions & 93 deletions crates/uv/src/commands/venv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -42,96 +40,30 @@ use crate::settings::NetworkSettings;

use super::project::default_dependency_groups;

/// Create a virtual environment.
#[allow(clippy::unnecessary_wraps, clippy::fn_params_excessive_bools)]
pub(crate) async fn venv(
project_dir: &Path,
path: Option<PathBuf>,
python_request: Option<PythonRequest>,
install_mirrors: PythonInstallMirrors,
python_preference: PythonPreference,
python_downloads: PythonDownloads,
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,
allow_existing: bool,
exclude_newer: Option<ExcludeNewer>,
concurrency: Concurrency,
no_config: bool,
no_project: bool,
cache: &Cache,
printer: Printer,
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)]
#[derive(Error, Debug)]
enum VenvError {
#[error("Failed to create virtualenv")]
#[diagnostic(code(uv::venv::creation))]
#[error("Failed to create virtual environment")]
Creation(#[source] uv_virtualenv::Error),

#[error("Failed to install seed packages")]
#[diagnostic(code(uv::venv::seed))]
#[error("Failed to install seed packages into virtual environment")]
Seed(#[source] AnyErrorBuild),

#[error("Failed to extract interpreter tags")]
#[diagnostic(code(uv::venv::tags))]
#[error("Failed to extract interpreter tags for installing seed packages")]
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(
#[allow(clippy::unnecessary_wraps, clippy::fn_params_excessive_bools)]
pub(crate) async fn venv(
project_dir: &Path,
path: Option<PathBuf>,
python_request: Option<PythonRequest>,
install_mirrors: PythonInstallMirrors,
python_preference: PythonPreference,
python_downloads: PythonDownloads,
link_mode: LinkMode,
index_locations: &IndexLocations,
index_strategy: IndexStrategy,
Expand All @@ -141,8 +73,6 @@ async fn venv_impl(
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,
Expand All @@ -152,7 +82,7 @@ async fn venv_impl(
printer: Printer,
relocatable: bool,
preview: PreviewMode,
) -> miette::Result<ExitStatus> {
) -> Result<ExitStatus> {
let workspace_cache = WorkspaceCache::default();
let project = if no_project {
None
Expand Down Expand Up @@ -203,7 +133,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);
Expand All @@ -218,8 +148,7 @@ async fn venv_impl(
project_dir,
no_config,
)
.await
.into_diagnostic()?;
.await?;

// Locate the Python interpreter to use in the environment
let interpreter = {
Expand All @@ -236,9 +165,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()
};

Expand All @@ -265,8 +193,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
Expand Down Expand Up @@ -304,8 +231,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)
Expand Down Expand Up @@ -397,9 +323,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.
Expand Down Expand Up @@ -428,7 +352,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)
Expand Down
4 changes: 2 additions & 2 deletions crates/uv/tests/it/pip_compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
104 changes: 37 additions & 67 deletions crates/uv/tests/it/venv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these error code changes are fine?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, this did break tox-uv, tox-dev/tox-uv#223.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, sorry about that.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No worries, we released a fix, though internally at my company we have a 7 day delay ingest... that did cause some issues, as we only discovered the issue a few days ago, so pulling in the fix too will take a few days 🤦

----- 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
"
);

Expand Down Expand Up @@ -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
Expand All @@ -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###"
success: false
exit_code: 1
----- stdout -----

----- stderr -----
× No interpreter found for Python 3.100 in managed installations, search path, or registry
"###
);
} else {
uv_snapshot!(&mut command, @r###"
success: false
exit_code: 1
----- stdout -----
uv_snapshot!(context.filters(), &mut command, @r"
success: false
exit_code: 2
----- stdout -----

----- stderr -----
× No interpreter found for Python 3.100 in managed installations or search path
"###
);
}
----- stderr -----
error: No interpreter found for Python 3.100 in [PYTHON SOURCES]
"
);

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
Expand All @@ -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###"
success: false
exit_code: 1
----- 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 -----
uv_snapshot!(context.filters(), &mut command, @r"
success: false
exit_code: 2
----- stdout -----

----- stderr -----
× No interpreter found for Python 3.12.100 in managed installations or search path
"
);
}
----- stderr -----
error: No interpreter found for Python 3.12.[X] in [PYTHON SOURCES]
"
);

context.venv.assert(predicates::path::missing());
}
Expand Down Expand Up @@ -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(())
Expand Down Expand Up @@ -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(())
Expand All @@ -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()
Expand Down
Loading