Skip to content

Commit 9e851c1

Browse files
Skip Python interpreter discovery for uv export (#8638)
## 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 ```
1 parent bfa84cd commit 9e851c1

File tree

7 files changed

+219
-150
lines changed

7 files changed

+219
-150
lines changed

crates/uv/src/commands/project/add.rs

+9-10
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ use crate::commands::pip::loggers::{
4242
};
4343
use crate::commands::pip::operations::Modifications;
4444
use crate::commands::pip::resolution_environment;
45+
use crate::commands::project::lock::LockMode;
4546
use crate::commands::project::{script_python_requirement, ProjectError};
4647
use crate::commands::reporters::{PythonDownloadReporter, ResolverReporter};
4748
use crate::commands::{diagnostics, pip, project, ExitStatus, SharedState};
@@ -627,7 +628,6 @@ pub(crate) async fn add(
627628
&venv,
628629
state,
629630
locked,
630-
frozen,
631631
no_sync,
632632
&dependency_type,
633633
raw_sources,
@@ -688,7 +688,6 @@ async fn lock_and_sync(
688688
venv: &PythonEnvironment,
689689
state: SharedState,
690690
locked: bool,
691-
frozen: bool,
692691
no_sync: bool,
693692
dependency_type: &DependencyType,
694693
raw_sources: bool,
@@ -700,12 +699,15 @@ async fn lock_and_sync(
700699
cache: &Cache,
701700
printer: Printer,
702701
) -> Result<(), ProjectError> {
702+
let mode = if locked {
703+
LockMode::Locked(venv.interpreter())
704+
} else {
705+
LockMode::Write(venv.interpreter())
706+
};
707+
703708
let mut lock = project::lock::do_safe_lock(
704-
locked,
705-
frozen,
706-
false,
709+
mode,
707710
project.workspace(),
708-
venv.interpreter(),
709711
settings.into(),
710712
bounds,
711713
&state,
@@ -821,11 +823,8 @@ async fn lock_and_sync(
821823
// If the file was modified, we have to lock again, though the only expected change is
822824
// the addition of the minimum version specifiers.
823825
lock = project::lock::do_safe_lock(
824-
locked,
825-
frozen,
826-
false,
826+
mode,
827827
project.workspace(),
828-
venv.interpreter(),
829828
settings.into(),
830829
bounds,
831830
&state,

crates/uv/src/commands/project/export.rs

+27-18
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use uv_resolver::RequirementsTxtExport;
1717
use uv_workspace::{DiscoveryOptions, MemberDiscovery, VirtualProject, Workspace};
1818

1919
use crate::commands::pip::loggers::DefaultResolveLogger;
20-
use crate::commands::project::lock::do_safe_lock;
20+
use crate::commands::project::lock::{do_safe_lock, LockMode};
2121
use crate::commands::project::{
2222
default_dependency_groups, validate_dependency_groups, ProjectError, ProjectInterpreter,
2323
};
@@ -80,30 +80,39 @@ pub(crate) async fn export(
8080
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"));
8181
};
8282

83-
// Find an interpreter for the project
84-
let interpreter = ProjectInterpreter::discover(
85-
project.workspace(),
86-
python.as_deref().map(PythonRequest::parse),
87-
python_preference,
88-
python_downloads,
89-
connectivity,
90-
native_tls,
91-
cache,
92-
printer,
93-
)
94-
.await?
95-
.into_interpreter();
83+
// Determine the lock mode.
84+
let interpreter;
85+
let mode = if frozen {
86+
LockMode::Frozen
87+
} else {
88+
// Find an interpreter for the project
89+
interpreter = ProjectInterpreter::discover(
90+
project.workspace(),
91+
python.as_deref().map(PythonRequest::parse),
92+
python_preference,
93+
python_downloads,
94+
connectivity,
95+
native_tls,
96+
cache,
97+
printer,
98+
)
99+
.await?
100+
.into_interpreter();
101+
102+
if locked {
103+
LockMode::Locked(&interpreter)
104+
} else {
105+
LockMode::Write(&interpreter)
106+
}
107+
};
96108

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

100112
// Lock the project.
101113
let lock = match do_safe_lock(
102-
locked,
103-
frozen,
104-
false,
114+
mode,
105115
project.workspace(),
106-
&interpreter,
107116
settings.as_ref(),
108117
LowerBound::Warn,
109118
&state,

crates/uv/src/commands/project/lock.rs

+112-86
Original file line numberDiff line numberDiff line change
@@ -88,30 +88,41 @@ pub(crate) async fn lock(
8888
// Find the project requirements.
8989
let workspace = Workspace::discover(project_dir, &DiscoveryOptions::default()).await?;
9090

91-
// Find an interpreter for the project
92-
let interpreter = ProjectInterpreter::discover(
93-
&workspace,
94-
python.as_deref().map(PythonRequest::parse),
95-
python_preference,
96-
python_downloads,
97-
connectivity,
98-
native_tls,
99-
cache,
100-
printer,
101-
)
102-
.await?
103-
.into_interpreter();
91+
// Determine the lock mode.
92+
let interpreter;
93+
let mode = if frozen {
94+
LockMode::Frozen
95+
} else {
96+
// Find an interpreter for the project
97+
interpreter = ProjectInterpreter::discover(
98+
&workspace,
99+
python.as_deref().map(PythonRequest::parse),
100+
python_preference,
101+
python_downloads,
102+
connectivity,
103+
native_tls,
104+
cache,
105+
printer,
106+
)
107+
.await?
108+
.into_interpreter();
109+
110+
if locked {
111+
LockMode::Locked(&interpreter)
112+
} else if dry_run {
113+
LockMode::DryRun(&interpreter)
114+
} else {
115+
LockMode::Write(&interpreter)
116+
}
117+
};
104118

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

108122
// Perform the lock operation.
109123
match do_safe_lock(
110-
locked,
111-
frozen,
112-
dry_run,
124+
mode,
113125
&workspace,
114-
&interpreter,
115126
settings.as_ref(),
116127
LowerBound::Warn,
117128
&state,
@@ -169,14 +180,23 @@ pub(crate) async fn lock(
169180
}
170181
}
171182

183+
#[derive(Debug, Clone, Copy)]
184+
pub(super) enum LockMode<'env> {
185+
/// Write the lockfile to disk.
186+
Write(&'env Interpreter),
187+
/// Perform a resolution, but don't write the lockfile to disk.
188+
DryRun(&'env Interpreter),
189+
/// Error if the lockfile is not up-to-date with the project requirements.
190+
Locked(&'env Interpreter),
191+
/// Use the existing lockfile without performing a resolution.
192+
Frozen,
193+
}
194+
172195
/// Perform a lock operation, respecting the `--locked` and `--frozen` parameters.
173196
#[allow(clippy::fn_params_excessive_bools)]
174197
pub(super) async fn do_safe_lock(
175-
locked: bool,
176-
frozen: bool,
177-
dry_run: bool,
198+
mode: LockMode<'_>,
178199
workspace: &Workspace,
179-
interpreter: &Interpreter,
180200
settings: ResolverSettingsRef<'_>,
181201
bounds: LowerBound,
182202
state: &SharedState,
@@ -187,78 +207,84 @@ pub(super) async fn do_safe_lock(
187207
cache: &Cache,
188208
printer: Printer,
189209
) -> Result<LockResult, ProjectError> {
190-
if frozen {
191-
// Read the existing lockfile, but don't attempt to lock the project.
192-
let existing = read(workspace)
193-
.await?
194-
.ok_or_else(|| ProjectError::MissingLockfile)?;
195-
Ok(LockResult::Unchanged(existing))
196-
} else if locked {
197-
// Read the existing lockfile.
198-
let existing = read(workspace)
199-
.await?
200-
.ok_or_else(|| ProjectError::MissingLockfile)?;
201-
202-
// Perform the lock operation, but don't write the lockfile to disk.
203-
let result = do_lock(
204-
workspace,
205-
interpreter,
206-
Some(existing),
207-
settings,
208-
bounds,
209-
state,
210-
logger,
211-
connectivity,
212-
concurrency,
213-
native_tls,
214-
cache,
215-
printer,
216-
)
217-
.await?;
218-
219-
// If the lockfile changed, return an error.
220-
if matches!(result, LockResult::Changed(_, _)) {
221-
return Err(ProjectError::LockMismatch);
210+
match mode {
211+
LockMode::Frozen => {
212+
// Read the existing lockfile, but don't attempt to lock the project.
213+
let existing = read(workspace)
214+
.await?
215+
.ok_or_else(|| ProjectError::MissingLockfile)?;
216+
Ok(LockResult::Unchanged(existing))
222217
}
218+
LockMode::Locked(interpreter) => {
219+
// Read the existing lockfile.
220+
let existing = read(workspace)
221+
.await?
222+
.ok_or_else(|| ProjectError::MissingLockfile)?;
223+
224+
// Perform the lock operation, but don't write the lockfile to disk.
225+
let result = do_lock(
226+
workspace,
227+
interpreter,
228+
Some(existing),
229+
settings,
230+
bounds,
231+
state,
232+
logger,
233+
connectivity,
234+
concurrency,
235+
native_tls,
236+
cache,
237+
printer,
238+
)
239+
.await?;
223240

224-
Ok(result)
225-
} else {
226-
// Read the existing lockfile.
227-
let existing = match read(workspace).await {
228-
Ok(Some(existing)) => Some(existing),
229-
Ok(None) => None,
230-
Err(ProjectError::Lock(err)) => {
231-
warn_user!("Failed to read existing lockfile; ignoring locked requirements: {err}");
232-
None
241+
// If the lockfile changed, return an error.
242+
if matches!(result, LockResult::Changed(_, _)) {
243+
return Err(ProjectError::LockMismatch);
233244
}
234-
Err(err) => return Err(err),
235-
};
236245

237-
// Perform the lock operation.
238-
let result = do_lock(
239-
workspace,
240-
interpreter,
241-
existing,
242-
settings,
243-
bounds,
244-
state,
245-
logger,
246-
connectivity,
247-
concurrency,
248-
native_tls,
249-
cache,
250-
printer,
251-
)
252-
.await?;
246+
Ok(result)
247+
}
248+
LockMode::Write(interpreter) | LockMode::DryRun(interpreter) => {
249+
// Read the existing lockfile.
250+
let existing = match read(workspace).await {
251+
Ok(Some(existing)) => Some(existing),
252+
Ok(None) => None,
253+
Err(ProjectError::Lock(err)) => {
254+
warn_user!(
255+
"Failed to read existing lockfile; ignoring locked requirements: {err}"
256+
);
257+
None
258+
}
259+
Err(err) => return Err(err),
260+
};
253261

254-
// If the lockfile changed, write it to disk.
255-
if !dry_run {
256-
if let LockResult::Changed(_, lock) = &result {
257-
commit(lock, workspace).await?;
262+
// Perform the lock operation.
263+
let result = do_lock(
264+
workspace,
265+
interpreter,
266+
existing,
267+
settings,
268+
bounds,
269+
state,
270+
logger,
271+
connectivity,
272+
concurrency,
273+
native_tls,
274+
cache,
275+
printer,
276+
)
277+
.await?;
278+
279+
// If the lockfile changed, write it to disk.
280+
if !matches!(mode, LockMode::DryRun(_)) {
281+
if let LockResult::Changed(_, lock) = &result {
282+
commit(lock, workspace).await?;
283+
}
258284
}
259-
}
260285

261-
Ok(result)
286+
Ok(result)
287+
}
262288
}
263289
}
264290

crates/uv/src/commands/project/remove.rs

+11-4
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use uv_workspace::{DiscoveryOptions, InstallTarget, VirtualProject, Workspace};
2121
use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger};
2222
use crate::commands::pip::operations::Modifications;
2323
use crate::commands::project::default_dependency_groups;
24+
use crate::commands::project::lock::LockMode;
2425
use crate::commands::{project, ExitStatus, SharedState};
2526
use crate::printer::Printer;
2627
use crate::settings::ResolverInstallerSettings;
@@ -193,16 +194,22 @@ pub(crate) async fn remove(
193194
)
194195
.await?;
195196

197+
// Determine the lock mode.
198+
let mode = if frozen {
199+
LockMode::Frozen
200+
} else if locked {
201+
LockMode::Locked(venv.interpreter())
202+
} else {
203+
LockMode::Write(venv.interpreter())
204+
};
205+
196206
// Initialize any shared state.
197207
let state = SharedState::default();
198208

199209
// Lock and sync the environment, if necessary.
200210
let lock = project::lock::do_safe_lock(
201-
locked,
202-
frozen,
203-
false,
211+
mode,
204212
project.workspace(),
205-
venv.interpreter(),
206213
settings.as_ref().into(),
207214
LowerBound::Allow,
208215
&state,

0 commit comments

Comments
 (0)