Skip to content

Commit b8cea02

Browse files
committed
Remove lock
1 parent 5187f33 commit b8cea02

File tree

3 files changed

+204
-37
lines changed

3 files changed

+204
-37
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+
let _ = 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+
let () = 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+
let () = fs_err::write(workspace_root.join("uv.lock"), uv_lock)?;
953+
} else {
954+
debug!("Removing `uv.lock`");
955+
let () = 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: 151 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5506,76 +5506,206 @@ 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
55285551
let filters = std::iter::once((r"exit code: 1", "exit status: 1"))
55295552
.chain(context.filters())
55305553
.collect::<Vec<_>>();
5531-
uv_snapshot!(filters, context.add().arg("pytorch==1.0.2"), @r###"
5554+
uv_snapshot!(filters, context.add().arg("./child"), @r###"
55325555
success: false
55335556
exit_code: 2
55345557
----- stdout -----
55355558
55365559
----- stderr -----
5537-
Resolved 2 packages in [TIME]
5560+
Resolved 3 packages in [TIME]
55385561
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)
5562+
Caused by: Failed to build `child @ file://[TEMP_DIR]/child`
5563+
Caused by: Build backend failed to determine requirements with `build_wheel()` (exit status: 1)
55415564
55425565
[stderr]
55435566
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
5567+
File "<string>", line 14, in <module>
5568+
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 325, in get_requires_for_build_wheel
5569+
return self._get_build_requires(config_settings, requirements=['wheel'])
5570+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
5571+
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 295, in _get_build_requires
55495572
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)
55525573
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 311, in run_setup
55535574
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-
5575+
File "<string>", line 1, in <module>
5576+
ZeroDivisionError: division by zero
55575577
"###);
55585578

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

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

5705+
// The lockfile should exist, but be unchanged.
5706+
let after = fs_err::read_to_string(context.temp_dir.join("uv.lock"))?;
5707+
assert_eq!(before, after);
5708+
55795709
Ok(())
55805710
}
55815711

0 commit comments

Comments
 (0)