Skip to content

Commit 4f9b2b6

Browse files
committed
Rollback pyproject.toml changes on all errors
1 parent b3ac31d commit 4f9b2b6

File tree

3 files changed

+90
-62
lines changed

3 files changed

+90
-62
lines changed

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

Lines changed: 80 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ use crate::commands::project::ProjectError;
4141
use crate::commands::reporters::{PythonDownloadReporter, ResolverReporter};
4242
use crate::commands::{pip, project, ExitStatus, SharedState};
4343
use crate::printer::Printer;
44-
use crate::settings::ResolverInstallerSettings;
44+
use crate::settings::{ResolverInstallerSettings, ResolverInstallerSettingsRef};
4545

4646
/// Add one or more packages to the project requirements.
4747
#[allow(clippy::fn_params_excessive_bools)]
@@ -479,22 +479,27 @@ pub(crate) async fn add(
479479
return Ok(ExitStatus::Success);
480480
}
481481

482-
let existing = project.pyproject_toml();
482+
// Store the content prior to any modifications.
483+
let existing = project.pyproject_toml().as_ref().to_vec();
484+
let root = project.root().to_path_buf();
483485

484486
// Update the `pypackage.toml` in-memory.
485-
let mut project = project
486-
.clone()
487-
.with_pyproject_toml(toml::from_str(&content)?)
488-
.context("Failed to update `pyproject.toml`")?;
489-
490-
// Lock and sync the environment, if necessary.
491-
let mut lock = match project::lock::do_safe_lock(
487+
let project = project
488+
.with_pyproject_toml(toml::from_str(&content).map_err(ProjectError::TomlParse)?)
489+
.ok_or(ProjectError::TomlUpdate)?;
490+
491+
match lock_and_sync(
492+
project,
493+
&mut toml,
494+
&edits,
495+
&venv,
496+
state,
492497
locked,
493498
frozen,
494-
project.workspace(),
495-
venv.interpreter(),
496-
settings.as_ref().into(),
497-
Box::new(DefaultResolveLogger),
499+
no_sync,
500+
&dependency_type,
501+
raw_sources,
502+
settings.as_ref(),
498503
connectivity,
499504
concurrency,
500505
native_tls,
@@ -503,7 +508,7 @@ pub(crate) async fn add(
503508
)
504509
.await
505510
{
506-
Ok(result) => result.into_lock(),
511+
Ok(()) => Ok(ExitStatus::Success),
507512
Err(ProjectError::Operation(pip::operations::Error::Resolve(
508513
uv_resolver::ResolveError::NoSolution(err),
509514
))) => {
@@ -513,13 +518,56 @@ pub(crate) async fn add(
513518

514519
// Revert the changes to the `pyproject.toml`, if necessary.
515520
if modified {
516-
fs_err::write(project.root().join("pyproject.toml"), existing)?;
521+
fs_err::write(root.join("pyproject.toml"), existing)?;
517522
}
518523

519-
return Ok(ExitStatus::Failure);
524+
Ok(ExitStatus::Failure)
520525
}
521-
Err(err) => return Err(err.into()),
522-
};
526+
Err(err) => {
527+
// Revert the changes to the `pyproject.toml`, if necessary.
528+
if modified {
529+
fs_err::write(root.join("pyproject.toml"), existing)?;
530+
}
531+
Err(err.into())
532+
}
533+
}
534+
}
535+
536+
/// Re-lock and re-sync the project after a series of edits.
537+
#[allow(clippy::fn_params_excessive_bools)]
538+
async fn lock_and_sync(
539+
mut project: VirtualProject,
540+
toml: &mut PyProjectTomlMut,
541+
edits: &[DependencyEdit<'_>],
542+
venv: &PythonEnvironment,
543+
state: SharedState,
544+
locked: bool,
545+
frozen: bool,
546+
no_sync: bool,
547+
dependency_type: &DependencyType,
548+
raw_sources: bool,
549+
settings: ResolverInstallerSettingsRef<'_>,
550+
connectivity: Connectivity,
551+
concurrency: Concurrency,
552+
native_tls: bool,
553+
cache: &Cache,
554+
printer: Printer,
555+
) -> Result<(), ProjectError> {
556+
let mut lock = project::lock::do_safe_lock(
557+
locked,
558+
frozen,
559+
project.workspace(),
560+
venv.interpreter(),
561+
settings.into(),
562+
Box::new(DefaultResolveLogger),
563+
connectivity,
564+
concurrency,
565+
native_tls,
566+
cache,
567+
printer,
568+
)
569+
.await?
570+
.into_lock();
523571

524572
// Avoid modifying the user request further if `--raw-sources` is set.
525573
if !raw_sources {
@@ -543,7 +591,7 @@ pub(crate) async fn add(
543591

544592
// If any of the requirements were added without version specifiers, add a lower bound.
545593
let mut modified = false;
546-
for edit in &edits {
594+
for edit in edits {
547595
// Only set a minimum version for newly-added dependencies (as opposed to updates).
548596
let ArrayEdit::Add(index) = &edit.edit else {
549597
continue;
@@ -599,49 +647,31 @@ pub(crate) async fn add(
599647

600648
// Update the `pypackage.toml` in-memory.
601649
project = project
602-
.clone()
603-
.with_pyproject_toml(toml::from_str(&content)?)
604-
.context("Failed to update `pyproject.toml`")?;
650+
.with_pyproject_toml(toml::from_str(&content).map_err(ProjectError::TomlParse)?)
651+
.ok_or(ProjectError::TomlUpdate)?;
605652

606653
// If the file was modified, we have to lock again, though the only expected change is
607654
// the addition of the minimum version specifiers.
608-
lock = match project::lock::do_safe_lock(
655+
lock = project::lock::do_safe_lock(
609656
locked,
610657
frozen,
611658
project.workspace(),
612659
venv.interpreter(),
613-
settings.as_ref().into(),
660+
settings.into(),
614661
Box::new(SummaryResolveLogger),
615662
connectivity,
616663
concurrency,
617664
native_tls,
618665
cache,
619666
printer,
620667
)
621-
.await
622-
{
623-
Ok(result) => result.into_lock(),
624-
Err(ProjectError::Operation(pip::operations::Error::Resolve(
625-
uv_resolver::ResolveError::NoSolution(err),
626-
))) => {
627-
let header = err.header();
628-
let report = miette::Report::new(WithHelp { header, cause: err, help: Some("If this is intentional, run `uv add --frozen` to skip the lock and sync steps.") });
629-
anstream::eprint!("{report:?}");
630-
631-
// Revert the changes to the `pyproject.toml`, if necessary.
632-
if modified {
633-
fs_err::write(project.root().join("pyproject.toml"), existing)?;
634-
}
635-
636-
return Ok(ExitStatus::Failure);
637-
}
638-
Err(err) => return Err(err.into()),
639-
};
668+
.await?
669+
.into_lock();
640670
}
641671
}
642672

643673
if no_sync {
644-
return Ok(ExitStatus::Success);
674+
return Ok(());
645675
}
646676

647677
// Sync the environment.
@@ -663,19 +693,15 @@ pub(crate) async fn add(
663693
}
664694
};
665695

666-
// Initialize any shared state.
667-
let state = SharedState::default();
668-
let install_options = InstallOptions::default();
669-
670-
if let Err(err) = project::sync::do_sync(
696+
project::sync::do_sync(
671697
InstallTarget::from(&project),
672-
&venv,
698+
venv,
673699
&lock,
674700
&extras,
675701
dev,
676-
install_options,
702+
InstallOptions::default(),
677703
Modifications::Sufficient,
678-
settings.as_ref().into(),
704+
settings.into(),
679705
&state,
680706
Box::new(DefaultInstallLogger),
681707
connectivity,
@@ -684,16 +710,9 @@ pub(crate) async fn add(
684710
cache,
685711
printer,
686712
)
687-
.await
688-
{
689-
// Revert the changes to the `pyproject.toml`, if necessary.
690-
if modified {
691-
fs_err::write(project.root().join("pyproject.toml"), existing)?;
692-
}
693-
return Err(err.into());
694-
}
713+
.await?;
695714

696-
Ok(ExitStatus::Success)
715+
Ok(())
697716
}
698717

699718
/// Resolves the source for a requirement and processes it into a PEP 508 compliant format.

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,12 @@ pub(crate) enum ProjectError {
8484
#[error("Environment marker is empty")]
8585
EmptyEnvironment,
8686

87+
#[error("Failed to parse `pyproject.toml`")]
88+
TomlParse(#[source] toml::de::Error),
89+
90+
#[error("Failed to update `pyproject.toml`")]
91+
TomlUpdate,
92+
8793
#[error(transparent)]
8894
Python(#[from] uv_python::Error),
8995

@@ -120,6 +126,9 @@ pub(crate) enum ProjectError {
120126
#[error(transparent)]
121127
NamedRequirements(#[from] uv_requirements::NamedRequirementsError),
122128

129+
#[error(transparent)]
130+
PyprojectMut(#[from] uv_workspace::pyproject_mut::Error),
131+
123132
#[error(transparent)]
124133
Fmt(#[from] std::fmt::Error),
125134

crates/uv/src/settings.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1810,7 +1810,7 @@ impl From<ResolverOptions> for ResolverSettings {
18101810
}
18111811
}
18121812

1813-
#[derive(Debug, Clone)]
1813+
#[derive(Debug, Clone, Copy)]
18141814
pub(crate) struct ResolverInstallerSettingsRef<'a> {
18151815
pub(crate) index_locations: &'a IndexLocations,
18161816
pub(crate) index_strategy: IndexStrategy,

0 commit comments

Comments
 (0)