Skip to content

Avoid re-creating virtual environment with --no-sync #13287

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 5, 2025
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
3 changes: 3 additions & 0 deletions crates/uv/src/commands/project/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ pub(crate) async fn add(
python_preference,
python_downloads,
&install_mirrors,
false,
no_config,
active,
cache,
Expand Down Expand Up @@ -225,6 +226,7 @@ pub(crate) async fn add(
python_preference,
python_downloads,
&install_mirrors,
false,
no_config,
active,
cache,
Expand All @@ -243,6 +245,7 @@ pub(crate) async fn add(
&network_settings,
python_preference,
python_downloads,
no_sync,
no_config,
active,
cache,
Expand Down
2 changes: 2 additions & 0 deletions crates/uv/src/commands/project/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ pub(crate) async fn export(
python_downloads,
&install_mirrors,
no_config,
false,
Some(false),
cache,
printer,
Expand All @@ -152,6 +153,7 @@ pub(crate) async fn export(
python_preference,
python_downloads,
&install_mirrors,
false,
no_config,
Some(false),
cache,
Expand Down
2 changes: 2 additions & 0 deletions crates/uv/src/commands/project/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ pub(crate) async fn lock(
python_preference,
python_downloads,
&install_mirrors,
false,
no_config,
Some(false),
cache,
Expand All @@ -160,6 +161,7 @@ pub(crate) async fn lock(
python_preference,
python_downloads,
&install_mirrors,
false,
no_config,
Some(false),
cache,
Expand Down
131 changes: 88 additions & 43 deletions crates/uv/src/commands/project/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,7 @@ impl ScriptInterpreter {
python_preference: PythonPreference,
python_downloads: PythonDownloads,
install_mirrors: &PythonInstallMirrors,
keep_incompatible: bool,
no_config: bool,
active: Option<bool>,
cache: &Cache,
Expand All @@ -662,31 +663,26 @@ impl ScriptInterpreter {
let root = Self::root(script, active, cache);
match PythonEnvironment::from_root(&root, cache) {
Ok(venv) => {
if python_request.as_ref().is_none_or(|request| {
if request.satisfied(venv.interpreter(), cache) {
debug!(
"The script environment's Python version satisfies `{}`",
request.to_canonical_string()
);
true
} else {
debug!(
"The script 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 script environment's Python version does not meet the script's Python requirement: `{requires_python}`"
match environment_is_usable(
&venv,
EnvironmentKind::Script,
python_request.as_ref(),
requires_python
.as_ref()
.map(|(requires_python, _)| requires_python),
cache,
) {
Ok(()) => return Ok(Self::Environment(venv)),
Err(err) if keep_incompatible => {
warn_user!(
"Using incompatible environment (`{}`) due to `--no-sync` ({err})",
root.user_display().cyan(),
);
} else {
return Ok(Self::Environment(venv));
}
Err(err) => {
debug!("{err}");
}
}
}
Err(uv_python::Error::MissingEnvironment(_)) => {}
Expand Down Expand Up @@ -766,41 +762,74 @@ impl ScriptInterpreter {
}
}

/// Whether an environment is usable for the project, i.e., if it matches the requirements.
#[derive(Debug)]
pub(crate) enum EnvironmentKind {
Script,
Project,
}

impl std::fmt::Display for EnvironmentKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Script => write!(f, "script"),
Self::Project => write!(f, "project"),
}
}
}

#[derive(Debug, thiserror::Error)]
pub(crate) enum EnvironmentIncompatibilityError {
#[error("The {0} environment's Python version does not satisfy the request: `{1}`")]
PythonRequest(EnvironmentKind, PythonRequest),

#[error("The {0} environment's Python version does not meet the Python requirement: `{1}`")]
RequiresPython(EnvironmentKind, RequiresPython),

#[error("The interpreter in the {0} environment has different version ({1}) than it was created with ({2})")]
PyenvVersionConflict(EnvironmentKind, Version, Version),
}

/// Whether an environment is usable for a project or script, i.e., if it matches the requirements.
fn environment_is_usable(
environment: &PythonEnvironment,
kind: EnvironmentKind,
python_request: Option<&PythonRequest>,
requires_python: Option<&RequiresPython>,
cache: &Cache,
) -> bool {
) -> Result<(), EnvironmentIncompatibilityError> {
if let Some((cfg_version, int_version)) = environment.get_pyvenv_version_conflict() {
debug!("The interpreter in the virtual environment has different version ({int_version}) than it was created with ({cfg_version})");
return false;
return Err(EnvironmentIncompatibilityError::PyenvVersionConflict(
kind,
cfg_version,
int_version,
));
}

if let Some(request) = python_request {
if request.satisfied(environment.interpreter(), cache) {
debug!("The virtual environment's Python version satisfies the request: `{request}`");
debug!("The {kind} environment's Python version satisfies the request: `{request}`");
} else {
debug!("The virtual environment's Python version does not satisfy the request: `{request}`");
return false;
return Err(EnvironmentIncompatibilityError::PythonRequest(
kind,
request.clone(),
));
}
}

if let Some(requires_python) = requires_python.as_ref() {
if let Some(requires_python) = requires_python {
if requires_python.contains(environment.interpreter().python_version()) {
trace!(
"The virtual environment's Python version meets the Python requirement: `{requires_python}`"
"The {kind} environment's Python version meets the Python requirement: `{requires_python}`"
);
} else {
debug!(
"The virtual environment's Python version does not meet the Python requirement: `{requires_python}`"
);
return false;
return Err(EnvironmentIncompatibilityError::RequiresPython(
kind,
requires_python.clone(),
));
}
}

true
Ok(())
}

/// An interpreter suitable for the project.
Expand All @@ -823,6 +852,7 @@ impl ProjectInterpreter {
python_preference: PythonPreference,
python_downloads: PythonDownloads,
install_mirrors: &PythonInstallMirrors,
keep_incompatible: bool,
no_config: bool,
active: Option<bool>,
cache: &Cache,
Expand All @@ -837,16 +867,27 @@ impl ProjectInterpreter {
.await?;

// Read from the virtual environment first.
let venv = workspace.venv(active);
match PythonEnvironment::from_root(&venv, cache) {
let root = workspace.venv(active);
match PythonEnvironment::from_root(&root, cache) {
Ok(venv) => {
if environment_is_usable(
match environment_is_usable(
&venv,
EnvironmentKind::Project,
python_request.as_ref(),
requires_python.as_ref(),
cache,
) {
return Ok(Self::Environment(venv));
Ok(()) => return Ok(Self::Environment(venv)),
Err(err) if keep_incompatible => {
warn_user!(
"Using incompatible environment (`{}`) due to `--no-sync` ({err})",
root.user_display().cyan(),
);
return Ok(Self::Environment(venv));
}
Err(err) => {
debug!("{err}");
}
}
}
Err(uv_python::Error::MissingEnvironment(_)) => {}
Expand All @@ -856,14 +897,14 @@ impl ProjectInterpreter {
match inner.kind {
InvalidEnvironmentKind::NotDirectory => {
return Err(ProjectError::InvalidProjectEnvironmentDir(
venv,
root,
inner.kind.to_string(),
))
}
InvalidEnvironmentKind::MissingExecutable(_) => {
if fs_err::read_dir(&venv).is_ok_and(|mut dir| dir.next().is_some()) {
if fs_err::read_dir(&root).is_ok_and(|mut dir| dir.next().is_some()) {
return Err(ProjectError::InvalidProjectEnvironmentDir(
venv,
root,
"it is not a valid Python environment (no Python executable was found)"
.to_string(),
));
Expand Down Expand Up @@ -1167,6 +1208,7 @@ impl ProjectEnvironment {
network_settings: &NetworkSettings,
python_preference: PythonPreference,
python_downloads: PythonDownloads,
no_sync: bool,
no_config: bool,
active: Option<bool>,
cache: &Cache,
Expand All @@ -1184,6 +1226,7 @@ impl ProjectEnvironment {
python_preference,
python_downloads,
install_mirrors,
no_sync,
no_config,
active,
cache,
Expand Down Expand Up @@ -1369,6 +1412,7 @@ impl ScriptEnvironment {
python_preference: PythonPreference,
python_downloads: PythonDownloads,
install_mirrors: &PythonInstallMirrors,
no_sync: bool,
no_config: bool,
active: Option<bool>,
cache: &Cache,
Expand All @@ -1385,6 +1429,7 @@ impl ScriptEnvironment {
python_preference,
python_downloads,
install_mirrors,
no_sync,
no_config,
active,
cache,
Expand Down
3 changes: 3 additions & 0 deletions crates/uv/src/commands/project/remove.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ pub(crate) async fn remove(
python_preference,
python_downloads,
&install_mirrors,
false,
no_config,
active,
cache,
Expand All @@ -233,6 +234,7 @@ pub(crate) async fn remove(
&network_settings,
python_preference,
python_downloads,
no_sync,
no_config,
active,
cache,
Expand All @@ -253,6 +255,7 @@ pub(crate) async fn remove(
python_preference,
python_downloads,
&install_mirrors,
no_sync,
no_config,
active,
cache,
Expand Down
4 changes: 4 additions & 0 deletions crates/uv/src/commands/project/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
python_preference,
python_downloads,
&install_mirrors,
no_sync,
no_config,
active.map_or(Some(false), Some),
cache,
Expand Down Expand Up @@ -351,6 +352,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
python_preference,
python_downloads,
&install_mirrors,
no_sync,
no_config,
active.map_or(Some(false), Some),
cache,
Expand Down Expand Up @@ -425,6 +427,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
python_preference,
python_downloads,
&install_mirrors,
no_sync,
no_config,
active.map_or(Some(false), Some),
cache,
Expand Down Expand Up @@ -648,6 +651,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
&network_settings,
python_preference,
python_downloads,
no_sync,
no_config,
active,
cache,
Expand Down
2 changes: 2 additions & 0 deletions crates/uv/src/commands/project/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ pub(crate) async fn sync(
&network_settings,
python_preference,
python_downloads,
false,
no_config,
active,
cache,
Expand All @@ -152,6 +153,7 @@ pub(crate) async fn sync(
python_preference,
python_downloads,
&install_mirrors,
false,
no_config,
active,
cache,
Expand Down
2 changes: 2 additions & 0 deletions crates/uv/src/commands/project/tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ pub(crate) async fn tree(
python_preference,
python_downloads,
&install_mirrors,
false,
no_config,
Some(false),
cache,
Expand All @@ -106,6 +107,7 @@ pub(crate) async fn tree(
python_preference,
python_downloads,
&install_mirrors,
false,
no_config,
Some(false),
cache,
Expand Down
1 change: 1 addition & 0 deletions crates/uv/src/commands/python/find.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ pub(crate) async fn find_script(
python_preference,
python_downloads,
&PythonInstallMirrors::default(),
false,
no_config,
Some(false),
cache,
Expand Down
2 changes: 1 addition & 1 deletion crates/uv/tests/it/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15748,7 +15748,7 @@ fn lock_explicit_default_index() -> Result<()> {
DEBUG No Python version file found in workspace: [TEMP_DIR]/
DEBUG Using Python request `>=3.12` from `requires-python` metadata
DEBUG Checking for Python environment at `.venv`
DEBUG The virtual environment's Python version satisfies the request: `Python >=3.12`
DEBUG The project environment's Python version satisfies the request: `Python >=3.12`
DEBUG Using request timeout of [TIME]
DEBUG Found static `pyproject.toml` for: project @ file://[TEMP_DIR]/
DEBUG No workspace root found, using project root
Expand Down
Loading
Loading