mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-19 19:44:40 +00:00
Don't panic in uv export --frozen when the lockfile is outdated (#16407)
Provide a good error message when the discovered workspace members mismatch with the locked workspace members in `uv export --frozen`, instead of panicking. Fixes #16406
This commit is contained in:
parent
940a3f63ce
commit
491293362f
7 changed files with 71 additions and 8 deletions
|
|
@ -16,11 +16,12 @@ use uv_pep508::MarkerTree;
|
|||
use uv_pypi_types::ConflictItem;
|
||||
|
||||
use crate::graph_ops::{Reachable, marker_reachability};
|
||||
use crate::lock::LockErrorKind;
|
||||
pub(crate) use crate::lock::export::pylock_toml::PylockTomlPackage;
|
||||
pub use crate::lock::export::pylock_toml::{PylockToml, PylockTomlErrorKind};
|
||||
pub use crate::lock::export::requirements_txt::RequirementsTxtExport;
|
||||
use crate::universal_marker::resolve_conflicts;
|
||||
use crate::{Installable, Package};
|
||||
use crate::{Installable, LockError, Package};
|
||||
|
||||
mod pylock_toml;
|
||||
mod requirements_txt;
|
||||
|
|
@ -49,7 +50,7 @@ impl<'lock> ExportableRequirements<'lock> {
|
|||
groups: &DependencyGroupsWithDefaults,
|
||||
annotate: bool,
|
||||
install_options: &'lock InstallOptions,
|
||||
) -> Self {
|
||||
) -> Result<Self, LockError> {
|
||||
let size_guess = target.lock().packages.len();
|
||||
let mut graph = Graph::<Node<'lock>, Edge<'lock>>::with_capacity(size_guess, size_guess);
|
||||
let mut inverse = FxHashMap::with_capacity_and_hasher(size_guess, FxBuildHasher);
|
||||
|
|
@ -73,8 +74,12 @@ impl<'lock> ExportableRequirements<'lock> {
|
|||
let dist = target
|
||||
.lock()
|
||||
.find_by_name(root_name)
|
||||
.expect("found too many packages matching root")
|
||||
.expect("could not find root");
|
||||
.map_err(|_| LockErrorKind::MultipleRootPackages {
|
||||
name: root_name.clone(),
|
||||
})?
|
||||
.ok_or_else(|| LockErrorKind::MissingRootPackage {
|
||||
name: root_name.clone(),
|
||||
})?;
|
||||
|
||||
if groups.prod() {
|
||||
// Add the workspace package to the graph.
|
||||
|
|
@ -330,7 +335,7 @@ impl<'lock> ExportableRequirements<'lock> {
|
|||
.filter(|requirement| !requirement.marker.is_false())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Self(nodes)
|
||||
Ok(Self(nodes))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -631,7 +631,7 @@ impl<'lock> PylockToml {
|
|||
dev,
|
||||
annotate,
|
||||
install_options,
|
||||
);
|
||||
)?;
|
||||
|
||||
// Sort the nodes.
|
||||
nodes.sort_unstable_by_key(|node| &node.package.id);
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ impl<'lock> RequirementsTxtExport<'lock> {
|
|||
dev,
|
||||
annotate,
|
||||
install_options,
|
||||
);
|
||||
)?;
|
||||
|
||||
// Sort the nodes, such that unnamed URLs (editables) appear at the top.
|
||||
nodes.sort_unstable_by(|a, b| {
|
||||
|
|
|
|||
|
|
@ -1269,7 +1269,7 @@ impl Lock {
|
|||
/// Returns the package with the given name. If there are multiple
|
||||
/// matching packages, then an error is returned. If there are no
|
||||
/// matching packages, then `Ok(None)` is returned.
|
||||
fn find_by_name(&self, name: &PackageName) -> Result<Option<&Package>, String> {
|
||||
pub fn find_by_name(&self, name: &PackageName) -> Result<Option<&Package>, String> {
|
||||
let mut found_dist = None;
|
||||
for dist in &self.packages {
|
||||
if &dist.id.name == name {
|
||||
|
|
|
|||
|
|
@ -334,6 +334,17 @@ impl<'env> LockOperation<'env> {
|
|||
.read()
|
||||
.await?
|
||||
.ok_or_else(|| ProjectError::MissingLockfile)?;
|
||||
// Check if the discovered workspace members match the locked workspace members.
|
||||
if let LockTarget::Workspace(workspace) = target {
|
||||
for package_name in workspace.packages().keys() {
|
||||
existing
|
||||
.find_by_name(package_name)
|
||||
.map_err(|_| ProjectError::LockWorkspaceMismatch(package_name.clone()))?
|
||||
.ok_or_else(|| {
|
||||
ProjectError::LockWorkspaceMismatch(package_name.clone())
|
||||
})?;
|
||||
}
|
||||
}
|
||||
Ok(LockResult::Unchanged(existing))
|
||||
}
|
||||
LockMode::Locked(interpreter, lock_source) => {
|
||||
|
|
|
|||
|
|
@ -87,6 +87,11 @@ pub(crate) enum ProjectError {
|
|||
)]
|
||||
MissingLockfile,
|
||||
|
||||
#[error(
|
||||
"The lockfile at `uv.lock` needs to be updated, but `--frozen` was provided: Missing workspace member `{0}`. To update the lockfile, run `uv lock`."
|
||||
)]
|
||||
LockWorkspaceMismatch(PackageName),
|
||||
|
||||
#[error(
|
||||
"The lockfile at `uv.lock` uses an unsupported schema version (v{1}, but only v{0} is supported). Downgrade to a compatible uv version, or remove the `uv.lock` prior to running `uv lock` or `uv sync`."
|
||||
)]
|
||||
|
|
|
|||
|
|
@ -4618,3 +4618,45 @@ fn export_only_group_and_extra_conflict() -> Result<()> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The members in the workspace (`foo`) and in the lockfile (`bar`) mismatch.
|
||||
#[test]
|
||||
fn export_lock_workspace_mismatch_with_frozen() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "foo"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("uv.lock");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[[package]]
|
||||
name = "bar"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = []
|
||||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.export().arg("--frozen"), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: The lockfile at `uv.lock` needs to be updated, but `--frozen` was provided: Missing workspace member `foo`. To update the lockfile, run `uv lock`.
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue