Skip to content

Commit

Permalink
Skip Python interpreter discovery for uv export (#8638)
Browse files Browse the repository at this point in the history
## Summary

We can skip interpreter discovery with `--frozen` in a few cases: `uv
export`, `uv tree --universal`, etc.

Closes #8634.

## Test Plan

Before:

```
❯ uv export --python-preference only-managed --no-python-downloads
error: No interpreter found for Python 3.12 in managed installations
```

After:

```
❯ cargo run export --python-preference only-managed --no-python-downloads --frozen
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.16s
     Running `/Users/crmarsh/workspace/uv/target/debug/uv export --python-preference only-managed --no-python-downloads --frozen`
# This file was autogenerated by uv via the following command:
#    uv export --python-preference only-managed --no-python-downloads --frozen
blinker==1.8.2 \
    --hash=sha256:8f77b09d3bf7c795e969e9486f39c2c5e9c39d4ee07424be2bc594ece9642d83 \
    --hash=sha256:1779309f71bf239144b9399d06ae925637cf6634cf6bd131104184531bf67c01
click==8.1.7 \
    --hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de \
    --hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28
colorama==0.4.6 ; platform_system == 'Windows' \
    --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
    --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
flask==3.0.3 \
    --hash=sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842 \
    --hash=sha256:34e815dfaa43340d1d15a5c3a02b8476004037eb4840b34910c6e21679d288f3
itsdangerous==2.2.0 \
    --hash=sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173 \
    --hash=sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef
jinja2==3.1.4 \
    --hash=sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369 \
    --hash=sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d
markupsafe==3.0.2 \
    --hash=sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0 \
    --hash=sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf \
    --hash=sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225 \
    --hash=sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028 \
    --hash=sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8 \
    --hash=sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c \
    --hash=sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557 \
    --hash=sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22 \
    --hash=sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48 \
    --hash=sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30 \
    --hash=sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87 \
    --hash=sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd \
    --hash=sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430 \
    --hash=sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094 \
    --hash=sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396 \
    --hash=sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79 \
    --hash=sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a \
    --hash=sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca \
    --hash=sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c \
    --hash=sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1 \
    --hash=sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f \
    --hash=sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c \
    --hash=sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb \
    --hash=sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c \
    --hash=sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d \
    --hash=sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe \
    --hash=sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5 \
    --hash=sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a \
    --hash=sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9 \
    --hash=sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6 \
    --hash=sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f
werkzeug==3.0.6 \
    --hash=sha256:a8dd59d4de28ca70471a34cba79bed5f7ef2e036a76b3ab0835474246eb41f8d \
    --hash=sha256:1bc0c2310d2fbb07b1dd1105eba2f7af72f322e1e455f2f93c993bee8c8a5f17
```
  • Loading branch information
charliermarsh authored Oct 28, 2024
1 parent bfa84cd commit 9e851c1
Show file tree
Hide file tree
Showing 7 changed files with 219 additions and 150 deletions.
19 changes: 9 additions & 10 deletions crates/uv/src/commands/project/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ use crate::commands::pip::loggers::{
};
use crate::commands::pip::operations::Modifications;
use crate::commands::pip::resolution_environment;
use crate::commands::project::lock::LockMode;
use crate::commands::project::{script_python_requirement, ProjectError};
use crate::commands::reporters::{PythonDownloadReporter, ResolverReporter};
use crate::commands::{diagnostics, pip, project, ExitStatus, SharedState};
Expand Down Expand Up @@ -627,7 +628,6 @@ pub(crate) async fn add(
&venv,
state,
locked,
frozen,
no_sync,
&dependency_type,
raw_sources,
Expand Down Expand Up @@ -688,7 +688,6 @@ async fn lock_and_sync(
venv: &PythonEnvironment,
state: SharedState,
locked: bool,
frozen: bool,
no_sync: bool,
dependency_type: &DependencyType,
raw_sources: bool,
Expand All @@ -700,12 +699,15 @@ async fn lock_and_sync(
cache: &Cache,
printer: Printer,
) -> Result<(), ProjectError> {
let mode = if locked {
LockMode::Locked(venv.interpreter())
} else {
LockMode::Write(venv.interpreter())
};

let mut lock = project::lock::do_safe_lock(
locked,
frozen,
false,
mode,
project.workspace(),
venv.interpreter(),
settings.into(),
bounds,
&state,
Expand Down Expand Up @@ -821,11 +823,8 @@ async fn lock_and_sync(
// If the file was modified, we have to lock again, though the only expected change is
// the addition of the minimum version specifiers.
lock = project::lock::do_safe_lock(
locked,
frozen,
false,
mode,
project.workspace(),
venv.interpreter(),
settings.into(),
bounds,
&state,
Expand Down
45 changes: 27 additions & 18 deletions crates/uv/src/commands/project/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use uv_resolver::RequirementsTxtExport;
use uv_workspace::{DiscoveryOptions, MemberDiscovery, VirtualProject, Workspace};

use crate::commands::pip::loggers::DefaultResolveLogger;
use crate::commands::project::lock::do_safe_lock;
use crate::commands::project::lock::{do_safe_lock, LockMode};
use crate::commands::project::{
default_dependency_groups, validate_dependency_groups, ProjectError, ProjectInterpreter,
};
Expand Down Expand Up @@ -80,30 +80,39 @@ pub(crate) async fn export(
return Err(anyhow::anyhow!("Legacy non-project roots are not supported in `uv export`; add a `[project]` table to your `pyproject.toml` to enable exports"));
};

// Find an interpreter for the project
let interpreter = ProjectInterpreter::discover(
project.workspace(),
python.as_deref().map(PythonRequest::parse),
python_preference,
python_downloads,
connectivity,
native_tls,
cache,
printer,
)
.await?
.into_interpreter();
// Determine the lock mode.
let interpreter;
let mode = if frozen {
LockMode::Frozen
} else {
// Find an interpreter for the project
interpreter = ProjectInterpreter::discover(
project.workspace(),
python.as_deref().map(PythonRequest::parse),
python_preference,
python_downloads,
connectivity,
native_tls,
cache,
printer,
)
.await?
.into_interpreter();

if locked {
LockMode::Locked(&interpreter)
} else {
LockMode::Write(&interpreter)
}
};

// Initialize any shared state.
let state = SharedState::default();

// Lock the project.
let lock = match do_safe_lock(
locked,
frozen,
false,
mode,
project.workspace(),
&interpreter,
settings.as_ref(),
LowerBound::Warn,
&state,
Expand Down
198 changes: 112 additions & 86 deletions crates/uv/src/commands/project/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,30 +88,41 @@ pub(crate) async fn lock(
// Find the project requirements.
let workspace = Workspace::discover(project_dir, &DiscoveryOptions::default()).await?;

// Find an interpreter for the project
let interpreter = ProjectInterpreter::discover(
&workspace,
python.as_deref().map(PythonRequest::parse),
python_preference,
python_downloads,
connectivity,
native_tls,
cache,
printer,
)
.await?
.into_interpreter();
// Determine the lock mode.
let interpreter;
let mode = if frozen {
LockMode::Frozen
} else {
// Find an interpreter for the project
interpreter = ProjectInterpreter::discover(
&workspace,
python.as_deref().map(PythonRequest::parse),
python_preference,
python_downloads,
connectivity,
native_tls,
cache,
printer,
)
.await?
.into_interpreter();

if locked {
LockMode::Locked(&interpreter)
} else if dry_run {
LockMode::DryRun(&interpreter)
} else {
LockMode::Write(&interpreter)
}
};

// Initialize any shared state.
let state = SharedState::default();

// Perform the lock operation.
match do_safe_lock(
locked,
frozen,
dry_run,
mode,
&workspace,
&interpreter,
settings.as_ref(),
LowerBound::Warn,
&state,
Expand Down Expand Up @@ -169,14 +180,23 @@ pub(crate) async fn lock(
}
}

#[derive(Debug, Clone, Copy)]
pub(super) enum LockMode<'env> {
/// Write the lockfile to disk.
Write(&'env Interpreter),
/// Perform a resolution, but don't write the lockfile to disk.
DryRun(&'env Interpreter),
/// Error if the lockfile is not up-to-date with the project requirements.
Locked(&'env Interpreter),
/// Use the existing lockfile without performing a resolution.
Frozen,
}

/// Perform a lock operation, respecting the `--locked` and `--frozen` parameters.
#[allow(clippy::fn_params_excessive_bools)]
pub(super) async fn do_safe_lock(
locked: bool,
frozen: bool,
dry_run: bool,
mode: LockMode<'_>,
workspace: &Workspace,
interpreter: &Interpreter,
settings: ResolverSettingsRef<'_>,
bounds: LowerBound,
state: &SharedState,
Expand All @@ -187,78 +207,84 @@ pub(super) async fn do_safe_lock(
cache: &Cache,
printer: Printer,
) -> Result<LockResult, ProjectError> {
if frozen {
// Read the existing lockfile, but don't attempt to lock the project.
let existing = read(workspace)
.await?
.ok_or_else(|| ProjectError::MissingLockfile)?;
Ok(LockResult::Unchanged(existing))
} else if locked {
// Read the existing lockfile.
let existing = read(workspace)
.await?
.ok_or_else(|| ProjectError::MissingLockfile)?;

// Perform the lock operation, but don't write the lockfile to disk.
let result = do_lock(
workspace,
interpreter,
Some(existing),
settings,
bounds,
state,
logger,
connectivity,
concurrency,
native_tls,
cache,
printer,
)
.await?;

// If the lockfile changed, return an error.
if matches!(result, LockResult::Changed(_, _)) {
return Err(ProjectError::LockMismatch);
match mode {
LockMode::Frozen => {
// Read the existing lockfile, but don't attempt to lock the project.
let existing = read(workspace)
.await?
.ok_or_else(|| ProjectError::MissingLockfile)?;
Ok(LockResult::Unchanged(existing))
}
LockMode::Locked(interpreter) => {
// Read the existing lockfile.
let existing = read(workspace)
.await?
.ok_or_else(|| ProjectError::MissingLockfile)?;

// Perform the lock operation, but don't write the lockfile to disk.
let result = do_lock(
workspace,
interpreter,
Some(existing),
settings,
bounds,
state,
logger,
connectivity,
concurrency,
native_tls,
cache,
printer,
)
.await?;

Ok(result)
} else {
// Read the existing lockfile.
let existing = match read(workspace).await {
Ok(Some(existing)) => Some(existing),
Ok(None) => None,
Err(ProjectError::Lock(err)) => {
warn_user!("Failed to read existing lockfile; ignoring locked requirements: {err}");
None
// If the lockfile changed, return an error.
if matches!(result, LockResult::Changed(_, _)) {
return Err(ProjectError::LockMismatch);
}
Err(err) => return Err(err),
};

// Perform the lock operation.
let result = do_lock(
workspace,
interpreter,
existing,
settings,
bounds,
state,
logger,
connectivity,
concurrency,
native_tls,
cache,
printer,
)
.await?;
Ok(result)
}
LockMode::Write(interpreter) | LockMode::DryRun(interpreter) => {
// Read the existing lockfile.
let existing = match read(workspace).await {
Ok(Some(existing)) => Some(existing),
Ok(None) => None,
Err(ProjectError::Lock(err)) => {
warn_user!(
"Failed to read existing lockfile; ignoring locked requirements: {err}"
);
None
}
Err(err) => return Err(err),
};

// If the lockfile changed, write it to disk.
if !dry_run {
if let LockResult::Changed(_, lock) = &result {
commit(lock, workspace).await?;
// Perform the lock operation.
let result = do_lock(
workspace,
interpreter,
existing,
settings,
bounds,
state,
logger,
connectivity,
concurrency,
native_tls,
cache,
printer,
)
.await?;

// If the lockfile changed, write it to disk.
if !matches!(mode, LockMode::DryRun(_)) {
if let LockResult::Changed(_, lock) = &result {
commit(lock, workspace).await?;
}
}
}

Ok(result)
Ok(result)
}
}
}

Expand Down
15 changes: 11 additions & 4 deletions crates/uv/src/commands/project/remove.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use uv_workspace::{DiscoveryOptions, InstallTarget, VirtualProject, Workspace};
use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger};
use crate::commands::pip::operations::Modifications;
use crate::commands::project::default_dependency_groups;
use crate::commands::project::lock::LockMode;
use crate::commands::{project, ExitStatus, SharedState};
use crate::printer::Printer;
use crate::settings::ResolverInstallerSettings;
Expand Down Expand Up @@ -193,16 +194,22 @@ pub(crate) async fn remove(
)
.await?;

// Determine the lock mode.
let mode = if frozen {
LockMode::Frozen
} else if locked {
LockMode::Locked(venv.interpreter())
} else {
LockMode::Write(venv.interpreter())
};

// Initialize any shared state.
let state = SharedState::default();

// Lock and sync the environment, if necessary.
let lock = project::lock::do_safe_lock(
locked,
frozen,
false,
mode,
project.workspace(),
venv.interpreter(),
settings.as_ref().into(),
LowerBound::Allow,
&state,
Expand Down
Loading

0 comments on commit 9e851c1

Please sign in to comment.