Skip to content

Commit 21c7c90

Browse files
committed
Remove lock
1 parent 59f3ec2 commit 21c7c90

File tree

3 files changed

+201
-40
lines changed

3 files changed

+201
-40
lines changed

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

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::collections::hash_map::Entry;
22
use std::fmt::Write;
3+
use std::io;
34
use std::path::{Path, PathBuf};
45

56
use anyhow::{bail, Context, Result};
@@ -44,7 +45,7 @@ use crate::commands::pip::loggers::{
4445
use crate::commands::pip::operations::Modifications;
4546
use crate::commands::project::lock::LockMode;
4647
use crate::commands::project::{
47-
init_script_python_requirement, validate_script_requires_python, ProjectError,
48+
init_script_python_requirement, lock, validate_script_requires_python, ProjectError,
4849
ProjectInterpreter, ScriptPython,
4950
};
5051
use crate::commands::reporters::{PythonDownloadReporter, ResolverReporter};
@@ -618,8 +619,10 @@ pub(crate) async fn add(
618619
}
619620

620621
// Store the content prior to any modifications.
621-
let existing = project.pyproject_toml().as_ref().to_vec();
622-
let root = project.root().to_path_buf();
622+
let project_root = project.root().to_path_buf();
623+
let workspace_root = project.workspace().install_path().clone();
624+
let existing_pyproject_toml = project.pyproject_toml().as_ref().to_vec();
625+
let existing_uv_lock = lock::read_bytes(project.workspace()).await?;
623626

624627
// Update the `pypackage.toml` in-memory.
625628
let project = project
@@ -628,12 +631,18 @@ pub(crate) async fn add(
628631

629632
// Set the Ctrl-C handler to revert changes on exit.
630633
let _ = ctrlc::set_handler({
631-
let root = root.clone();
632-
let existing = existing.clone();
634+
let project_root = project_root.clone();
635+
let workspace_root = workspace_root.clone();
636+
let existing_pyproject_toml = existing_pyproject_toml.clone();
637+
let existing_uv_lock = existing_uv_lock.clone();
633638
move || {
634-
// Revert the changes to the `pyproject.toml`, if necessary.
635639
if modified {
636-
let _ = fs_err::write(root.join("pyproject.toml"), &existing);
640+
let _ = revert(
641+
&project_root,
642+
&workspace_root,
643+
&existing_pyproject_toml,
644+
existing_uv_lock.as_deref(),
645+
);
637646
}
638647

639648
#[allow(clippy::exit, clippy::cast_possible_wrap)]
@@ -667,9 +676,13 @@ pub(crate) async fn add(
667676
{
668677
Ok(()) => Ok(ExitStatus::Success),
669678
Err(err) => {
670-
// Revert the changes to the `pyproject.toml`, if necessary.
671679
if modified {
672-
fs_err::write(root.join("pyproject.toml"), &existing)?;
680+
revert(
681+
&project_root,
682+
&workspace_root,
683+
&existing_pyproject_toml,
684+
existing_uv_lock.as_deref(),
685+
)?;
673686
}
674687

675688
match err {
@@ -691,13 +704,7 @@ pub(crate) async fn add(
691704
diagnostics::build(dist, err);
692705
Ok(ExitStatus::Failure)
693706
}
694-
err => {
695-
// Revert the changes to the `pyproject.toml`, if necessary.
696-
if modified {
697-
fs_err::write(root.join("pyproject.toml"), &existing)?;
698-
}
699-
Err(err.into())
700-
}
707+
err => Err(err.into()),
701708
}
702709
}
703710
}
@@ -931,6 +938,25 @@ async fn lock_and_sync(
931938
Ok(())
932939
}
933940

941+
/// Revert the changes to the `pyproject.toml` and `uv.lock`, if necessary.
942+
fn revert(
943+
project_root: &Path,
944+
workspace_root: &Path,
945+
pyproject_toml: &[u8],
946+
uv_lock: Option<&[u8]>,
947+
) -> Result<(), io::Error> {
948+
debug!("Reverting changes to `pyproject.toml`");
949+
fs_err::write(project_root.join("pyproject.toml"), pyproject_toml)?;
950+
if let Some(uv_lock) = uv_lock.as_ref() {
951+
debug!("Reverting changes to `uv.lock`");
952+
fs_err::write(workspace_root.join("uv.lock"), uv_lock)?;
953+
} else {
954+
debug!("Removing `uv.lock`");
955+
fs_err::remove_file(workspace_root.join("uv.lock"))?;
956+
}
957+
Ok(())
958+
}
959+
934960
/// Augment a user-provided requirement by attaching any specification data that was provided
935961
/// separately from the requirement itself (e.g., `--branch main`).
936962
fn augment_requirement(

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -998,6 +998,17 @@ pub(crate) async fn read(workspace: &Workspace) -> Result<Option<Lock>, ProjectE
998998
}
999999
}
10001000

1001+
/// Read the lockfile from the workspace as bytes.
1002+
///
1003+
/// Returns `Ok(None)` if the lockfile does not exist.
1004+
pub(crate) async fn read_bytes(workspace: &Workspace) -> Result<Option<Vec<u8>>, ProjectError> {
1005+
match fs_err::tokio::read(&workspace.install_path().join("uv.lock")).await {
1006+
Ok(encoded) => Ok(Some(encoded)),
1007+
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
1008+
Err(err) => Err(err.into()),
1009+
}
1010+
}
1011+
10011012
/// Reports on the versions that were upgraded in the new lockfile.
10021013
///
10031014
/// Returns `true` if any upgrades were reported.

crates/uv/tests/it/edit.rs

Lines changed: 148 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5506,76 +5506,200 @@ fn add_git_to_script() -> Result<()> {
55065506
Ok(())
55075507
}
55085508

5509-
/// Revert changes to a `pyproject.toml` the `add` fails.
5509+
/// Revert changes to the `pyproject.toml` and `uv.lock` when the `add` operation fails.
55105510
#[test]
55115511
fn fail_to_add_revert_project() -> Result<()> {
55125512
let context = TestContext::new("3.12");
55135513

5514-
let pyproject_toml = context.temp_dir.child("pyproject.toml");
5515-
pyproject_toml.write_str(indoc! {r#"
5514+
context
5515+
.temp_dir
5516+
.child("pyproject.toml")
5517+
.write_str(indoc! {r#"
55165518
[project]
5517-
name = "project"
5519+
name = "parent"
55185520
version = "0.1.0"
55195521
requires-python = ">=3.12"
55205522
dependencies = []
5523+
"#})?;
5524+
5525+
// Add a dependency on a package that declares static metadata (so can always resolve), but
5526+
// can't be installed.
5527+
let pyproject_toml = context.temp_dir.child("child/pyproject.toml");
5528+
pyproject_toml.write_str(indoc! {r#"
5529+
[project]
5530+
name = "child"
5531+
version = "0.1.0"
5532+
requires-python = ">=3.12"
5533+
dependencies = ["iniconfig"]
55215534
55225535
[build-system]
55235536
requires = ["setuptools>=42"]
55245537
build-backend = "setuptools.build_meta"
55255538
"#})?;
5539+
context
5540+
.temp_dir
5541+
.child("src")
5542+
.child("child")
5543+
.child("__init__.py")
5544+
.touch()?;
5545+
context
5546+
.temp_dir
5547+
.child("child")
5548+
.child("setup.py")
5549+
.write_str("1/0")?;
55265550

5527-
// Adding `pytorch==1.0.2` should produce an error
5528-
let filters = std::iter::once((r"exit code: 1", "exit status: 1"))
5529-
.chain(context.filters())
5530-
.collect::<Vec<_>>();
5531-
uv_snapshot!(filters, context.add().arg("pytorch==1.0.2"), @r###"
5551+
uv_snapshot!(context.filters(), context.add().arg("./child"), @r###"
55325552
success: false
55335553
exit_code: 2
55345554
----- stdout -----
55355555
55365556
----- stderr -----
5537-
Resolved 2 packages in [TIME]
5557+
Resolved 3 packages in [TIME]
55385558
error: Failed to prepare distributions
5539-
Caused by: Failed to download and build `pytorch==1.0.2`
5540-
Caused by: Build backend failed to build wheel through `build_wheel` (exit status: 1)
5559+
Caused by: Failed to build `child @ file://[TEMP_DIR]/child`
5560+
Caused by: Build backend failed to determine requirements with `build_wheel()` (exit status: 1)
55415561
55425562
[stderr]
55435563
Traceback (most recent call last):
5544-
File "<string>", line 11, in <module>
5545-
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 410, in build_wheel
5546-
return self._build_with_temp_dir(
5547-
^^^^^^^^^^^^^^^^^^^^^^^^^^
5548-
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 395, in _build_with_temp_dir
5564+
File "<string>", line 14, in <module>
5565+
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 325, in get_requires_for_build_wheel
5566+
return self._get_build_requires(config_settings, requirements=['wheel'])
5567+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
5568+
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 295, in _get_build_requires
55495569
self.run_setup()
5550-
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 487, in run_setup
5551-
super().run_setup(setup_script=setup_script)
55525570
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 311, in run_setup
55535571
exec(code, locals())
5554-
File "<string>", line 15, in <module>
5555-
Exception: You tried to install "pytorch". The package named for PyTorch is "torch"
5556-
5572+
File "<string>", line 1, in <module>
5573+
ZeroDivisionError: division by zero
55575574
"###);
55585575

5559-
let pyproject_toml = context.read("pyproject.toml");
5576+
let pyproject_toml = fs_err::read_to_string(context.temp_dir.join("pyproject.toml"))?;
55605577

55615578
insta::with_settings!({
55625579
filters => context.filters(),
55635580
}, {
55645581
assert_snapshot!(
55655582
pyproject_toml, @r###"
55665583
[project]
5567-
name = "project"
5584+
name = "parent"
5585+
version = "0.1.0"
5586+
requires-python = ">=3.12"
5587+
dependencies = []
5588+
"###
5589+
);
5590+
});
5591+
5592+
// The lockfile should not exist, even though resolution succeeded.
5593+
assert!(!context.temp_dir.join("uv.lock").exists());
5594+
5595+
Ok(())
5596+
}
5597+
5598+
/// Revert changes to the `pyproject.toml` and `uv.lock` when the `add` operation fails.
5599+
///
5600+
/// In this case, the project has an existing lockfile.
5601+
#[test]
5602+
fn fail_to_edit_revert_project() -> Result<()> {
5603+
let context = TestContext::new("3.12");
5604+
5605+
context
5606+
.temp_dir
5607+
.child("pyproject.toml")
5608+
.write_str(indoc! {r#"
5609+
[project]
5610+
name = "parent"
55685611
version = "0.1.0"
55695612
requires-python = ">=3.12"
55705613
dependencies = []
5614+
"#})?;
5615+
5616+
uv_snapshot!(context.filters(), context.add().arg("iniconfig"), @r###"
5617+
success: true
5618+
exit_code: 0
5619+
----- stdout -----
5620+
5621+
----- stderr -----
5622+
Resolved 2 packages in [TIME]
5623+
Prepared 1 package in [TIME]
5624+
Installed 1 package in [TIME]
5625+
+ iniconfig==2.0.0
5626+
"###);
5627+
5628+
let before = fs_err::read_to_string(context.temp_dir.join("uv.lock"))?;
5629+
5630+
// Add a dependency on a package that declares static metadata (so can always resolve), but
5631+
// can't be installed.
5632+
let pyproject_toml = context.temp_dir.child("child/pyproject.toml");
5633+
pyproject_toml.write_str(indoc! {r#"
5634+
[project]
5635+
name = "child"
5636+
version = "0.1.0"
5637+
requires-python = ">=3.12"
5638+
dependencies = ["iniconfig"]
55715639
55725640
[build-system]
55735641
requires = ["setuptools>=42"]
55745642
build-backend = "setuptools.build_meta"
5643+
"#})?;
5644+
context
5645+
.temp_dir
5646+
.child("src")
5647+
.child("child")
5648+
.child("__init__.py")
5649+
.touch()?;
5650+
context
5651+
.temp_dir
5652+
.child("child")
5653+
.child("setup.py")
5654+
.write_str("1/0")?;
5655+
5656+
uv_snapshot!(context.filters(), context.add().arg("./child"), @r###"
5657+
success: false
5658+
exit_code: 2
5659+
----- stdout -----
5660+
5661+
----- stderr -----
5662+
Resolved 3 packages in [TIME]
5663+
error: Failed to prepare distributions
5664+
Caused by: Failed to build `child @ file://[TEMP_DIR]/child`
5665+
Caused by: Build backend failed to determine requirements with `build_wheel()` (exit status: 1)
5666+
5667+
[stderr]
5668+
Traceback (most recent call last):
5669+
File "<string>", line 14, in <module>
5670+
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 325, in get_requires_for_build_wheel
5671+
return self._get_build_requires(config_settings, requirements=['wheel'])
5672+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
5673+
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 295, in _get_build_requires
5674+
self.run_setup()
5675+
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 311, in run_setup
5676+
exec(code, locals())
5677+
File "<string>", line 1, in <module>
5678+
ZeroDivisionError: division by zero
5679+
"###);
5680+
5681+
let pyproject_toml = fs_err::read_to_string(context.temp_dir.join("pyproject.toml"))?;
5682+
5683+
insta::with_settings!({
5684+
filters => context.filters(),
5685+
}, {
5686+
assert_snapshot!(
5687+
pyproject_toml, @r###"
5688+
[project]
5689+
name = "parent"
5690+
version = "0.1.0"
5691+
requires-python = ">=3.12"
5692+
dependencies = [
5693+
"iniconfig>=2.0.0",
5694+
]
55755695
"###
55765696
);
55775697
});
55785698

5699+
// The lockfile should exist, but be unchanged.
5700+
let after = fs_err::read_to_string(context.temp_dir.join("uv.lock"))?;
5701+
assert_eq!(before, after);
5702+
55795703
Ok(())
55805704
}
55815705

0 commit comments

Comments
 (0)