Perform lock in uv sync by default (#4839)

## Summary

- `uv sync` will now lock by default.
- `uv sync --locked` will lock, and error if the generated lock does not
match `uv.lock` on-disk.
- `uv sync --frozen` will skip locking and just use `uv.lock`.

Closes https://github.com/astral-sh/uv/issues/4812.
Closes https://github.com/astral-sh/uv/issues/4803.
This commit is contained in:
Charlie Marsh 2024-07-09 15:18:30 -07:00 committed by GitHub
parent 8c9bd70c71
commit 540ff24302
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 401 additions and 214 deletions

View file

@ -1784,6 +1784,14 @@ pub struct RunArgs {
#[derive(Args)]
#[allow(clippy::struct_excessive_bools)]
pub struct SyncArgs {
/// Assert that the `uv.lock` will remain unchanged.
#[arg(long, conflicts_with = "frozen")]
pub locked: bool,
/// Install without updating the `uv.lock` file.
#[arg(long, conflicts_with = "locked")]
pub frozen: bool,
/// Include optional dependencies from the extra group name; may be provided more than once.
/// Only applies to `pyproject.toml`, `setup.py`, and `setup.cfg` sources.
#[arg(long, conflicts_with = "all_extras", value_parser = extra_name_with_clap_error)]
@ -1811,7 +1819,7 @@ pub struct SyncArgs {
pub no_clean: bool,
#[command(flatten)]
pub installer: InstallerArgs,
pub installer: ResolverInstallerArgs,
#[command(flatten)]
pub build: BuildArgs,

View file

@ -29,7 +29,6 @@ uv-resolver = { workspace = true, features = ["clap"] }
uv-types = { workspace = true }
uv-warnings = { workspace = true }
anstream = { workspace = true }
anyhow = { workspace = true }
configparser = { workspace = true }
console = { workspace = true }

View file

@ -1,12 +1,10 @@
use std::path::Path;
use anstream::eprint;
use anyhow::Result;
use requirements_txt::RequirementsTxt;
use uv_client::{BaseClientBuilder, Connectivity};
use uv_configuration::Upgrade;
use uv_distribution::Workspace;
use uv_git::ResolvedRepositoryReference;
use uv_resolver::{Lock, Preference, PreferenceError};
@ -63,34 +61,6 @@ pub async fn read_requirements_txt(
})
}
/// Load the lockfile from the workspace.
///
/// Returns `Ok(None)` if the lockfile does not exist, is invalid, or is not required for the given upgrade strategy.
pub async fn read_lockfile(workspace: &Workspace, upgrade: &Upgrade) -> Result<Option<Lock>> {
// As an optimization, skip reading the lockfile is we're upgrading all packages anyway.
if upgrade.is_all() {
return Ok(None);
}
// If an existing lockfile exists, build up a set of preferences.
let lockfile = workspace.install_path().join("uv.lock");
let lock = match fs_err::tokio::read_to_string(&lockfile).await {
Ok(encoded) => match toml::from_str::<Lock>(&encoded) {
Ok(lock) => lock,
Err(err) => {
eprint!("Failed to parse lockfile; ignoring locked requirements: {err}");
return Ok(None);
}
},
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
return Ok(None);
}
Err(err) => return Err(err.into()),
};
Ok(Some(lock))
}
/// Load the preferred requirements from an existing lockfile, applying the upgrade strategy.
pub fn read_lock_requirements(lock: &Lock, upgrade: &Upgrade) -> LockedRequirements {
let mut preferences = Vec::new();

View file

@ -10,6 +10,7 @@ use uv_distribution::pyproject_mut::PyProjectTomlMut;
use uv_distribution::{DistributionDatabase, ProjectWorkspace, VirtualProject, Workspace};
use uv_normalize::PackageName;
use uv_python::{PythonFetch, PythonPreference, PythonRequest};
use uv_requirements::{NamedRequirementsResolver, RequirementsSource, RequirementsSpecification};
use uv_resolver::FlatIndex;
use uv_types::{BuildIsolation, HashStrategy};
@ -17,6 +18,7 @@ use uv_warnings::warn_user_once;
use crate::commands::pip::operations::Modifications;
use crate::commands::pip::resolution_environment;
use crate::commands::project::lock::commit;
use crate::commands::reporters::ResolverReporter;
use crate::commands::{project, ExitStatus, SharedState};
use crate::printer::Printer;
@ -204,10 +206,14 @@ pub(crate) async fn add(
// Initialize any shared state.
let state = SharedState::default();
// Lock and sync the environment.
// Read the existing lockfile.
let existing = project::lock::read(project.workspace()).await?;
// Lock and sync the environment, if necessary.
let lock = project::lock::do_lock(
project.workspace(),
venv.interpreter(),
existing.as_ref(),
settings.as_ref().into(),
&state,
preview,
@ -218,6 +224,9 @@ pub(crate) async fn add(
printer,
)
.await?;
if !existing.is_some_and(|existing| existing == lock) {
commit(&lock, project.workspace()).await?;
}
// Perform a full sync, because we don't know what exactly is affected by the removal.
// TODO(ibraheem): Should we accept CLI overrides for this? Should we even sync here?

View file

@ -8,7 +8,7 @@ use uv_dispatch::BuildDispatch;
use uv_distribution::{Workspace, DEV_DEPENDENCIES};
use uv_git::ResolvedRepositoryReference;
use uv_python::{Interpreter, PythonFetch, PythonPreference, PythonRequest};
use uv_requirements::upgrade::{read_lock_requirements, read_lockfile, LockedRequirements};
use uv_requirements::upgrade::{read_lock_requirements, LockedRequirements};
use uv_resolver::{FlatIndex, Lock, OptionsBuilder, PythonRequirement, RequiresPython};
use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy};
use uv_warnings::{warn_user, warn_user_once};
@ -52,10 +52,14 @@ pub(crate) async fn lock(
.await?
.into_interpreter();
// Read the existing lockfile.
let existing = read(&workspace).await?;
// Perform the lock operation.
match do_lock(
&workspace,
&interpreter,
existing.as_ref(),
settings.as_ref(),
&SharedState::default(),
preview,
@ -67,7 +71,12 @@ pub(crate) async fn lock(
)
.await
{
Ok(_) => Ok(ExitStatus::Success),
Ok(lock) => {
if !existing.is_some_and(|existing| existing == lock) {
commit(&lock, &workspace).await?;
}
Ok(ExitStatus::Success)
}
Err(ProjectError::Operation(pip::operations::Error::Resolve(
uv_resolver::ResolveError::NoSolution(err),
))) => {
@ -84,6 +93,7 @@ pub(crate) async fn lock(
pub(super) async fn do_lock(
workspace: &Workspace,
interpreter: &Interpreter,
existing: Option<&Lock>,
settings: ResolverSettingsRef<'_>,
state: &SharedState,
preview: PreviewMode,
@ -175,9 +185,7 @@ pub(super) async fn do_lock(
};
// If an existing lockfile exists, build up a set of preferences.
let existing_lock = read_lockfile(workspace, upgrade).await?;
let LockedRequirements { preferences, git } = existing_lock
.as_ref()
let LockedRequirements { preferences, git } = existing
.map(|lock| read_lock_requirements(lock, upgrade))
.unwrap_or_default();
@ -238,15 +246,29 @@ pub(super) async fn do_lock(
// Notify the user of any resolution diagnostics.
pip::operations::diagnose_resolution(resolution.diagnostics(), printer)?;
// Avoid serializing and writing to disk if the lock hasn't changed.
let lock = Lock::from_resolution_graph(&resolution)?;
if existing_lock.is_some_and(|existing_lock| existing_lock == lock) {
return Ok(lock);
}
// Write the lockfile to disk.
let encoded = lock.to_toml()?;
fs_err::tokio::write(workspace.install_path().join("uv.lock"), encoded.as_bytes()).await?;
Ok(lock)
Ok(Lock::from_resolution_graph(&resolution)?)
}
/// Write the lockfile to disk.
pub(crate) async fn commit(lock: &Lock, workspace: &Workspace) -> Result<(), ProjectError> {
let encoded = lock.to_toml()?;
fs_err::tokio::write(workspace.install_path().join("uv.lock"), encoded).await?;
Ok(())
}
/// Read the lockfile from the workspace.
///
/// Returns `Ok(None)` if the lockfile does not exist.
pub(crate) async fn read(workspace: &Workspace) -> Result<Option<Lock>, ProjectError> {
match fs_err::tokio::read_to_string(&workspace.install_path().join("uv.lock")).await {
Ok(encoded) => match toml::from_str::<Lock>(&encoded) {
Ok(lock) => Ok(Some(lock)),
Err(err) => {
eprint!("Failed to parse lockfile; ignoring locked requirements: {err}");
Ok(None)
}
},
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(err) => Err(err.into()),
}
}

View file

@ -41,6 +41,14 @@ pub(crate) mod tree;
#[derive(thiserror::Error, Debug)]
pub(crate) enum ProjectError {
#[error("The lockfile at `uv.lock` needs to be updated, but `--locked` was provided. To update the lockfile, run `uv lock`.")]
LockMismatch,
#[error(
"Unable to find lockfile at `uv.lock`. To create a lockfile, run `uv lock` or `uv sync`."
)]
MissingLockfile,
#[error("The current Python version ({0}) is not compatible with the locked Python requirement: `{1}`")]
LockedPythonIncompatibility(Version, RequiresPython),

View file

@ -11,6 +11,7 @@ use uv_python::{PythonFetch, PythonPreference, PythonRequest};
use uv_warnings::{warn_user, warn_user_once};
use crate::commands::pip::operations::Modifications;
use crate::commands::project::lock::commit;
use crate::commands::{project, ExitStatus, SharedState};
use crate::printer::Printer;
use crate::settings::ResolverInstallerSettings;
@ -98,10 +99,14 @@ pub(crate) async fn remove(
// Initialize any shared state.
let state = SharedState::default();
// Lock and sync the environment.
// Read the existing lockfile.
let existing = project::lock::read(project.workspace()).await?;
// Lock and sync the environment, if necessary.
let lock = project::lock::do_lock(
project.workspace(),
venv.interpreter(),
existing.as_ref(),
settings.as_ref().into(),
&state,
preview,
@ -112,6 +117,9 @@ pub(crate) async fn remove(
printer,
)
.await?;
if !existing.is_some_and(|existing| existing == lock) {
commit(&lock, project.workspace()).await?;
}
// Perform a full sync, because we don't know what exactly is affected by the removal.
// TODO(ibraheem): Should we accept CLI overrides for this? Should we even sync here?

View file

@ -22,6 +22,7 @@ use uv_python::{
request_from_version_file, EnvironmentPreference, Interpreter, PythonEnvironment, PythonFetch,
PythonInstallation, PythonPreference, PythonRequest, VersionRequest,
};
use uv_requirements::{RequirementsSource, RequirementsSpecification};
use uv_warnings::warn_user_once;
@ -179,10 +180,14 @@ pub(crate) async fn run(
)
.await?;
// Read the existing lockfile.
let existing = project::lock::read(project.workspace()).await?;
// Lock and sync the environment.
let lock = project::lock::do_lock(
project.workspace(),
venv.interpreter(),
existing.as_ref(),
settings.as_ref().into(),
&state,
preview,
@ -193,6 +198,11 @@ pub(crate) async fn run(
printer,
)
.await?;
if !existing.is_some_and(|existing| existing == lock) {
project::lock::commit(&lock, project.workspace()).await?;
}
project::sync::do_sync(
&project,
&venv,

View file

@ -7,25 +7,30 @@ use uv_dispatch::BuildDispatch;
use uv_distribution::{VirtualProject, DEV_DEPENDENCIES};
use uv_installer::SitePackages;
use uv_python::{PythonEnvironment, PythonFetch, PythonPreference, PythonRequest};
use uv_resolver::{FlatIndex, Lock};
use uv_types::{BuildIsolation, HashStrategy};
use uv_warnings::warn_user_once;
use crate::commands::pip::operations::Modifications;
use crate::commands::project::lock::do_lock;
use crate::commands::project::{ProjectError, SharedState};
use crate::commands::{pip, project, ExitStatus};
use crate::printer::Printer;
use crate::settings::{InstallerSettings, InstallerSettingsRef};
use crate::settings::{InstallerSettingsRef, ResolverInstallerSettings};
/// Sync the project environment.
#[allow(clippy::fn_params_excessive_bools)]
pub(crate) async fn sync(
locked: bool,
frozen: bool,
extras: ExtrasSpecification,
dev: bool,
modifications: Modifications,
python: Option<String>,
python_preference: PythonPreference,
python_fetch: PythonFetch,
settings: InstallerSettings,
settings: ResolverInstallerSettings,
preview: PreviewMode,
connectivity: Connectivity,
concurrency: Concurrency,
@ -53,12 +58,88 @@ pub(crate) async fn sync(
)
.await?;
// Read the lockfile.
let lock: Lock = {
let encoded =
fs_err::tokio::read_to_string(project.workspace().install_path().join("uv.lock"))
.await?;
toml::from_str(&encoded)?
// Initialize any shared state.
let state = SharedState::default();
let lock = if frozen {
// Read the existing lockfile.
project::lock::read(project.workspace())
.await?
.ok_or_else(|| ProjectError::MissingLockfile)?
} else if locked {
// Read the existing lockfile.
let existing = project::lock::read(project.workspace())
.await?
.ok_or_else(|| ProjectError::MissingLockfile)?;
// Perform the lock operation, but don't write the lockfile to disk.
let lock = match do_lock(
project.workspace(),
venv.interpreter(),
Some(&existing),
settings.as_ref().into(),
&state,
preview,
connectivity,
concurrency,
native_tls,
cache,
printer,
)
.await
{
Ok(lock) => lock,
Err(ProjectError::Operation(pip::operations::Error::Resolve(
uv_resolver::ResolveError::NoSolution(err),
))) => {
let report = miette::Report::msg(format!("{err}"))
.context("No solution found when resolving dependencies:");
anstream::eprint!("{report:?}");
return Ok(ExitStatus::Failure);
}
Err(err) => return Err(err.into()),
};
// If the locks disagree, return an error.
if lock != existing {
return Err(ProjectError::LockMismatch.into());
}
lock
} else {
// Read the existing lockfile.
let existing = project::lock::read(project.workspace()).await?;
// Perform the lock operation.
match do_lock(
project.workspace(),
venv.interpreter(),
existing.as_ref(),
settings.as_ref().into(),
&state,
preview,
connectivity,
concurrency,
native_tls,
cache,
printer,
)
.await
{
Ok(lock) => {
project::lock::commit(&lock, project.workspace()).await?;
lock
}
Err(ProjectError::Operation(pip::operations::Error::Resolve(
uv_resolver::ResolveError::NoSolution(err),
))) => {
let report = miette::Report::msg(format!("{err}"))
.context("No solution found when resolving dependencies:");
anstream::eprint!("{report:?}");
return Ok(ExitStatus::Failure);
}
Err(err) => return Err(err.into()),
}
};
// Perform the sync operation.
@ -69,8 +150,8 @@ pub(crate) async fn sync(
extras,
dev,
modifications,
settings.as_ref(),
&SharedState::default(),
settings.as_ref().into(),
&state,
preview,
connectivity,
concurrency,

View file

@ -10,15 +10,15 @@ use uv_client::Connectivity;
use uv_configuration::{Concurrency, PreviewMode};
use uv_distribution::Workspace;
use uv_python::{PythonFetch, PythonPreference, PythonRequest};
use uv_warnings::warn_user_once;
use crate::commands::pip::tree::DisplayDependencyGraph;
use crate::commands::project::FoundInterpreter;
use crate::commands::ExitStatus;
use crate::commands::{project, ExitStatus};
use crate::printer::Printer;
use crate::settings::ResolverSettings;
use super::lock::do_lock;
use super::SharedState;
/// Run a command.
@ -60,10 +60,14 @@ pub(crate) async fn tree(
.await?
.into_interpreter();
// Update the lock file.
let lock = do_lock(
// Read the existing lockfile.
let existing = project::lock::read(&workspace).await?;
// Update the lock file, if necessary.
let lock = project::lock::do_lock(
&workspace,
&interpreter,
existing.as_ref(),
settings.as_ref(),
&SharedState::default(),
preview,
@ -74,6 +78,9 @@ pub(crate) async fn tree(
printer,
)
.await?;
if !existing.is_some_and(|existing| existing == lock) {
project::lock::commit(&lock, &workspace).await?;
}
// Read packages from the lockfile.
let mut packages: IndexMap<_, Vec<_>> = IndexMap::new();

View file

@ -884,6 +884,8 @@ async fn run_project(
let cache = cache.init()?.with_refresh(args.refresh);
commands::sync(
args.locked,
args.frozen,
args.extras,
args.dev,
args.modifications,

View file

@ -9,7 +9,7 @@ use install_wheel_rs::linker::LinkMode;
use pep508_rs::{ExtraName, RequirementOrigin};
use pypi_types::Requirement;
use uv_cache::{CacheArgs, Refresh};
use uv_cli::options::{flag, installer_options, resolver_installer_options, resolver_options};
use uv_cli::options::{flag, resolver_installer_options, resolver_options};
use uv_cli::{
AddArgs, ColorChoice, Commands, ExternalCommand, GlobalArgs, ListFormat, LockArgs, Maybe,
PipCheckArgs, PipCompileArgs, PipFreezeArgs, PipInstallArgs, PipListArgs, PipShowArgs,
@ -29,8 +29,7 @@ use uv_python::{Prefix, PythonFetch, PythonPreference, PythonVersion, Target};
use uv_requirements::RequirementsSource;
use uv_resolver::{AnnotationStyle, DependencyMode, ExcludeNewer, PreReleaseMode, ResolutionMode};
use uv_settings::{
Combine, FilesystemOptions, InstallerOptions, Options, PipOptions, ResolverInstallerOptions,
ResolverOptions,
Combine, FilesystemOptions, Options, PipOptions, ResolverInstallerOptions, ResolverOptions,
};
use crate::commands::pip::operations::Modifications;
@ -415,12 +414,14 @@ impl PythonFindSettings {
#[allow(clippy::struct_excessive_bools, dead_code)]
#[derive(Debug, Clone)]
pub(crate) struct SyncSettings {
pub(crate) locked: bool,
pub(crate) frozen: bool,
pub(crate) extras: ExtrasSpecification,
pub(crate) dev: bool,
pub(crate) modifications: Modifications,
pub(crate) python: Option<String>,
pub(crate) refresh: Refresh,
pub(crate) settings: InstallerSettings,
pub(crate) settings: ResolverInstallerSettings,
}
impl SyncSettings {
@ -428,6 +429,8 @@ impl SyncSettings {
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn resolve(args: SyncArgs, filesystem: Option<FilesystemOptions>) -> Self {
let SyncArgs {
locked,
frozen,
extra,
all_extras,
no_all_extras,
@ -447,6 +450,8 @@ impl SyncSettings {
};
Self {
locked,
frozen,
extras: ExtrasSpecification::from_args(
flag(all_extras, no_all_extras).unwrap_or_default(),
extra.unwrap_or_default(),
@ -455,7 +460,10 @@ impl SyncSettings {
modifications,
python,
refresh: Refresh::from(refresh),
settings: InstallerSettings::combine(installer_options(installer, build), filesystem),
settings: ResolverInstallerSettings::combine(
resolver_installer_options(installer, build),
filesystem,
),
}
}
}
@ -1272,20 +1280,6 @@ impl VenvSettings {
///
/// Combines the `[tool.uv]` persistent configuration with the command-line arguments
/// ([`InstallerArgs`], represented as [`InstallerOptions`]).
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone, Default)]
pub(crate) struct InstallerSettings {
pub(crate) index_locations: IndexLocations,
pub(crate) index_strategy: IndexStrategy,
pub(crate) keyring_provider: KeyringProviderType,
pub(crate) config_setting: ConfigSettings,
pub(crate) exclude_newer: Option<ExcludeNewer>,
pub(crate) link_mode: LinkMode,
pub(crate) compile_bytecode: bool,
pub(crate) reinstall: Reinstall,
pub(crate) build_options: BuildOptions,
}
#[derive(Debug, Clone)]
pub(crate) struct InstallerSettingsRef<'a> {
pub(crate) index_locations: &'a IndexLocations,
@ -1299,100 +1293,6 @@ pub(crate) struct InstallerSettingsRef<'a> {
pub(crate) build_options: &'a BuildOptions,
}
impl InstallerSettings {
/// Resolve the [`InstallerSettings`] from the CLI and filesystem configuration.
pub(crate) fn combine(args: InstallerOptions, filesystem: Option<FilesystemOptions>) -> Self {
let ResolverInstallerOptions {
index_url,
extra_index_url,
no_index,
find_links,
index_strategy,
keyring_provider,
resolution: _,
prerelease: _,
config_settings,
exclude_newer,
link_mode,
compile_bytecode,
upgrade: _,
upgrade_package: _,
reinstall,
reinstall_package,
no_build,
no_build_package,
no_binary,
no_binary_package,
} = filesystem
.map(FilesystemOptions::into_options)
.map(|options| options.top_level)
.unwrap_or_default();
Self {
index_locations: IndexLocations::new(
args.index_url.combine(index_url),
args.extra_index_url
.combine(extra_index_url)
.unwrap_or_default(),
args.find_links.combine(find_links).unwrap_or_default(),
args.no_index.combine(no_index).unwrap_or_default(),
),
index_strategy: args
.index_strategy
.combine(index_strategy)
.unwrap_or_default(),
keyring_provider: args
.keyring_provider
.combine(keyring_provider)
.unwrap_or_default(),
config_setting: args
.config_settings
.combine(config_settings)
.unwrap_or_default(),
exclude_newer: args.exclude_newer.combine(exclude_newer),
link_mode: args.link_mode.combine(link_mode).unwrap_or_default(),
compile_bytecode: args
.compile_bytecode
.combine(compile_bytecode)
.unwrap_or_default(),
reinstall: Reinstall::from_args(
args.reinstall.combine(reinstall),
args.reinstall_package
.combine(reinstall_package)
.unwrap_or_default(),
),
build_options: BuildOptions::new(
NoBinary::from_args(
args.no_binary.combine(no_binary),
args.no_binary_package
.combine(no_binary_package)
.unwrap_or_default(),
),
NoBuild::from_args(
args.no_build.combine(no_build),
args.no_build_package
.combine(no_build_package)
.unwrap_or_default(),
),
),
}
}
pub(crate) fn as_ref(&self) -> InstallerSettingsRef {
InstallerSettingsRef {
index_locations: &self.index_locations,
index_strategy: self.index_strategy,
keyring_provider: self.keyring_provider,
config_setting: &self.config_setting,
exclude_newer: self.exclude_newer,
link_mode: self.link_mode,
compile_bytecode: self.compile_bytecode,
reinstall: &self.reinstall,
build_options: &self.build_options,
}
}
}
/// The resolved settings to use for an invocation of the `uv` CLI when resolving dependencies.
///
/// Combines the `[tool.uv]` persistent configuration with the command-line arguments

View file

@ -112,7 +112,7 @@ fn add_registry() -> Result<()> {
});
// Install from the lockfile.
uv_snapshot!(context.filters(), context.sync(), @r###"
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
success: true
exit_code: 0
----- stdout -----
@ -149,7 +149,7 @@ fn add_git() -> Result<()> {
Resolved 4 packages in [TIME]
"###);
uv_snapshot!(context.filters(), context.sync(), @r###"
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
success: true
exit_code: 0
----- stdout -----
@ -270,7 +270,7 @@ fn add_git() -> Result<()> {
});
// Install from the lockfile.
uv_snapshot!(context.filters(), context.sync(), @r###"
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
success: true
exit_code: 0
----- stdout -----
@ -307,7 +307,7 @@ fn add_git_raw() -> Result<()> {
Resolved 4 packages in [TIME]
"###);
uv_snapshot!(context.filters(), context.sync(), @r###"
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
success: true
exit_code: 0
----- stdout -----
@ -416,7 +416,7 @@ fn add_git_raw() -> Result<()> {
});
// Install from the lockfile.
uv_snapshot!(context.filters(), context.sync(), @r###"
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
success: true
exit_code: 0
----- stdout -----
@ -506,7 +506,7 @@ fn add_unnamed() -> Result<()> {
});
// Install from the lockfile.
uv_snapshot!(context.filters(), context.sync(), @r###"
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
success: true
exit_code: 0
----- stdout -----
@ -626,7 +626,7 @@ fn add_remove_dev() -> Result<()> {
});
// Install from the lockfile.
uv_snapshot!(context.filters(), context.sync(), @r###"
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
success: true
exit_code: 0
----- stdout -----
@ -705,7 +705,7 @@ fn add_remove_dev() -> Result<()> {
});
// Install from the lockfile.
uv_snapshot!(context.filters(), context.sync(), @r###"
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
success: true
exit_code: 0
----- stdout -----
@ -786,7 +786,7 @@ fn add_remove_optional() -> Result<()> {
});
// Install from the lockfile.
uv_snapshot!(context.filters(), context.sync(), @r###"
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
success: true
exit_code: 0
----- stdout -----
@ -862,7 +862,7 @@ fn add_remove_optional() -> Result<()> {
});
// Install from the lockfile.
uv_snapshot!(context.filters(), context.sync(), @r###"
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
success: true
exit_code: 0
----- stdout -----
@ -993,7 +993,7 @@ fn add_remove_workspace() -> Result<()> {
});
// Install from the lockfile.
uv_snapshot!(context.filters(), context.sync().current_dir(&child1), @r###"
uv_snapshot!(context.filters(), context.sync().arg("--frozen").current_dir(&child1), @r###"
success: true
exit_code: 0
----- stdout -----
@ -1062,7 +1062,7 @@ fn add_remove_workspace() -> Result<()> {
});
// Install from the lockfile.
uv_snapshot!(context.filters(), context.sync().current_dir(&child1), @r###"
uv_snapshot!(context.filters(), context.sync().arg("--frozen").current_dir(&child1), @r###"
success: true
exit_code: 0
----- stdout -----
@ -1173,7 +1173,7 @@ fn add_workspace_editable() -> Result<()> {
});
// Install from the lockfile.
uv_snapshot!(context.filters(), context.sync().current_dir(&child1), @r###"
uv_snapshot!(context.filters(), context.sync().arg("--frozen").current_dir(&child1), @r###"
success: true
exit_code: 0
----- stdout -----
@ -1212,7 +1212,7 @@ fn update() -> Result<()> {
Resolved 6 packages in [TIME]
"###);
uv_snapshot!(context.filters(), context.sync(), @r###"
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
success: true
exit_code: 0
----- stdout -----
@ -1451,7 +1451,7 @@ fn update() -> Result<()> {
});
// Install from the lockfile.
uv_snapshot!(context.filters(), context.sync(), @r###"
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
success: true
exit_code: 0
----- stdout -----
@ -1490,7 +1490,7 @@ fn add_no_clean() -> Result<()> {
Resolved 4 packages in [TIME]
"###);
uv_snapshot!(context.filters(), context.sync(), @r###"
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
success: true
exit_code: 0
----- stdout -----
@ -1579,7 +1579,7 @@ fn add_no_clean() -> Result<()> {
});
// Install from the lockfile without cleaning the environment.
uv_snapshot!(context.filters(), context.sync().arg("--no-clean"), @r###"
uv_snapshot!(context.filters(), context.sync().arg("--frozen").arg("--no-clean"), @r###"
success: true
exit_code: 0
----- stdout -----
@ -1590,7 +1590,7 @@ fn add_no_clean() -> Result<()> {
"###);
// Install from the lockfile, cleaning the environment.
uv_snapshot!(context.filters(), context.sync(), @r###"
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
success: true
exit_code: 0
----- stdout -----
@ -1630,7 +1630,7 @@ fn remove_registry() -> Result<()> {
Resolved 4 packages in [TIME]
"###);
uv_snapshot!(context.filters(), context.sync(), @r###"
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
success: true
exit_code: 0
----- stdout -----
@ -1698,7 +1698,7 @@ fn remove_registry() -> Result<()> {
});
// Install from the lockfile.
uv_snapshot!(context.filters(), context.sync(), @r###"
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
success: true
exit_code: 0
----- stdout -----

View file

@ -88,7 +88,7 @@ fn lock_wheel_registry() -> Result<()> {
});
// Install from the lockfile.
uv_snapshot!(context.filters(), context.sync(), @r###"
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
success: true
exit_code: 0
----- stdout -----
@ -160,7 +160,7 @@ fn lock_sdist_registry() -> Result<()> {
});
// Install from the lockfile.
uv_snapshot!(context.filters(), context.sync().env_remove("UV_EXCLUDE_NEWER"), @r###"
uv_snapshot!(context.filters(), context.sync().arg("--frozen").env_remove("UV_EXCLUDE_NEWER"), @r###"
success: true
exit_code: 0
----- stdout -----
@ -229,7 +229,7 @@ fn lock_sdist_git() -> Result<()> {
});
// Install from the lockfile.
uv_snapshot!(context.filters(), context.sync(), @r###"
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
success: true
exit_code: 0
----- stdout -----
@ -323,7 +323,7 @@ fn lock_wheel_url() -> Result<()> {
});
// Install from the lockfile.
uv_snapshot!(context.filters(), context.sync(), @r###"
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
success: true
exit_code: 0
----- stdout -----
@ -417,7 +417,7 @@ fn lock_sdist_url() -> Result<()> {
});
// Install from the lockfile.
uv_snapshot!(context.filters(), context.sync(), @r###"
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
success: true
exit_code: 0
----- stdout -----
@ -531,7 +531,7 @@ fn lock_project_extra() -> Result<()> {
});
// Install the base dependencies from the lockfile.
uv_snapshot!(context.filters(), context.sync(), @r###"
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
success: true
exit_code: 0
----- stdout -----
@ -547,7 +547,7 @@ fn lock_project_extra() -> Result<()> {
"###);
// Install the extras from the lockfile.
uv_snapshot!(context.filters(), context.sync().arg("--extra").arg("test"), @r###"
uv_snapshot!(context.filters(), context.sync().arg("--frozen").arg("--extra").arg("test"), @r###"
success: true
exit_code: 0
----- stdout -----
@ -591,7 +591,7 @@ fn lock_project_with_overrides() -> Result<()> {
"###);
// Install the base dependencies from the lockfile.
uv_snapshot!(context.filters(), context.sync(), @r###"
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
success: true
exit_code: 0
----- stdout -----
@ -772,7 +772,7 @@ fn lock_dependency_extra() -> Result<()> {
});
// Install from the lockfile.
uv_snapshot!(context.filters(), context.sync(), @r###"
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
success: true
exit_code: 0
----- stdout -----
@ -998,7 +998,7 @@ fn lock_conditional_dependency_extra() -> Result<()> {
});
// Install from the lockfile.
uv_snapshot!(context.filters(), context.sync(), @r###"
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
success: true
exit_code: 0
----- stdout -----
@ -1022,7 +1022,7 @@ fn lock_conditional_dependency_extra() -> Result<()> {
fs_err::copy(lockfile, context_38.temp_dir.join("uv.lock"))?;
// Install from the lockfile.
uv_snapshot!(context_38.filters(), context_38.sync(), @r###"
uv_snapshot!(context_38.filters(), context_38.sync().arg("--frozen"), @r###"
success: true
exit_code: 0
----- stdout -----
@ -1189,7 +1189,7 @@ fn lock_dependency_non_existent_extra() -> Result<()> {
});
// Install from the lockfile.
uv_snapshot!(context.filters(), context.sync(), @r###"
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
success: true
exit_code: 0
----- stdout -----
@ -1943,7 +1943,7 @@ fn lock_requires_python() -> Result<()> {
// Install from the lockfile.
// Note we need to disable Python fetches or we'll just download 3.12
uv_snapshot!(filters, context38.sync().arg("--python-fetch").arg("manual"), @r###"
uv_snapshot!(filters, context38.sync().arg("--frozen").arg("--python-fetch").arg("manual"), @r###"
success: false
exit_code: 2
----- stdout -----
@ -2398,7 +2398,7 @@ fn lock_dev() -> Result<()> {
});
// Install from the lockfile, excluding development dependencies.
uv_snapshot!(context.filters(), context.sync().arg("--no-dev"), @r###"
uv_snapshot!(context.filters(), context.sync().arg("--frozen").arg("--no-dev"), @r###"
success: true
exit_code: 0
----- stdout -----
@ -2412,7 +2412,7 @@ fn lock_dev() -> Result<()> {
"###);
// Install from the lockfile, including development dependencies (the default).
uv_snapshot!(context.filters(), context.sync(), @r###"
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
success: true
exit_code: 0
----- stdout -----
@ -2806,7 +2806,7 @@ fn lock_cycles() -> Result<()> {
});
// Install from the lockfile.
uv_snapshot!(context.filters(), context.sync(), @r###"
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
success: true
exit_code: 0
----- stdout -----

164
crates/uv/tests/sync.rs Normal file
View file

@ -0,0 +1,164 @@
#![cfg(all(feature = "python", feature = "pypi"))]
use anyhow::Result;
use assert_cmd::prelude::*;
use assert_fs::prelude::*;
use common::{uv_snapshot, TestContext};
mod common;
#[test]
fn sync() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["iniconfig"]
"#,
)?;
// Running `uv sync` should generate a lockfile.
uv_snapshot!(context.filters(), context.sync(), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: `uv sync` is experimental and may change without warning.
Resolved 2 packages in [TIME]
Prepared 2 packages in [TIME]
Installed 2 packages in [TIME]
+ iniconfig==2.0.0
+ project==0.1.0 (from file://[TEMP_DIR]/)
"###);
assert!(context.temp_dir.child("uv.lock").exists());
Ok(())
}
#[test]
fn locked() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["anyio==3.7.0"]
"#,
)?;
// Running with `--locked` should error, if no lockfile is present.
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
warning: `uv sync` is experimental and may change without warning.
error: Unable to find lockfile at `uv.lock`. To create a lockfile, run `uv lock` or `uv sync`.
"###);
// Lock the initial requirements.
context.lock().assert().success();
let existing = fs_err::read_to_string(context.temp_dir.child("uv.lock"))?;
// Update the requirements.
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["iniconfig"]
"#,
)?;
// Running with `--locked` should error.
uv_snapshot!(context.filters(), context.sync().arg("--locked"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
warning: `uv sync` is experimental and may change without warning.
Resolved 2 packages in [TIME]
error: The lockfile at `uv.lock` needs to be updated, but `--locked` was provided. To update the lockfile, run `uv lock`.
"###);
let updated = fs_err::read_to_string(context.temp_dir.child("uv.lock"))?;
// And the lockfile should be unchanged.
assert_eq!(existing, updated);
Ok(())
}
#[test]
fn frozen() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["anyio==3.7.0"]
"#,
)?;
// Running with `--frozen` should error, if no lockfile is present.
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
warning: `uv sync` is experimental and may change without warning.
error: Unable to find lockfile at `uv.lock`. To create a lockfile, run `uv lock` or `uv sync`.
"###);
context.lock().assert().success();
// Update the requirements.
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["iniconfig"]
"#,
)?;
// Running with `--frozen` should install the stale lockfile.
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: `uv sync` is experimental and may change without warning.
Prepared 4 packages in [TIME]
Installed 4 packages in [TIME]
+ anyio==3.7.0
+ idna==3.6
+ project==0.1.0 (from file://[TEMP_DIR]/)
+ sniffio==1.3.1
"###);
Ok(())
}