From 45a96eb487038f6913fcd093a8dca3e1f70aa41e Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 27 Aug 2024 19:02:02 -0400 Subject: [PATCH] Avoid using editable tag in lockfile for non-package dependencies --- crates/distribution-types/src/cached.rs | 9 + crates/distribution-types/src/lib.rs | 5 + crates/distribution-types/src/resolution.rs | 1 + crates/pypi-types/src/parsed_url.rs | 7 +- crates/pypi-types/src/requirement.rs | 30 +++ ...ts_txt__test__parse-unix-bare-url.txt.snap | 3 + ...ts_txt__test__parse-unix-editable.txt.snap | 6 + .../uv-distribution/src/index/cached_wheel.rs | 14 ++ .../uv-distribution/src/metadata/lowering.rs | 55 +++++- crates/uv-installer/src/plan.rs | 2 + crates/uv-installer/src/satisfies.rs | 1 + crates/uv-requirements/src/lookahead.rs | 2 + crates/uv-resolver/src/lock.rs | 38 +++- .../uv-resolver/src/pubgrub/dependencies.rs | 2 + crates/uv-workspace/src/workspace.rs | 18 +- crates/uv/src/commands/project/sync.rs | 39 ++-- crates/uv/tests/edit.rs | 2 +- crates/uv/tests/lock.rs | 183 +++++++++++++++++- crates/uv/tests/lock_scenarios.rs | 52 ++--- .../workflow__jax_instability-2.snap | 2 +- crates/uv/tests/workflow.rs | 4 +- 21 files changed, 397 insertions(+), 78 deletions(-) diff --git a/crates/distribution-types/src/cached.rs b/crates/distribution-types/src/cached.rs index dc944f9487fc9..eaa95116961a3 100644 --- a/crates/distribution-types/src/cached.rs +++ b/crates/distribution-types/src/cached.rs @@ -34,6 +34,7 @@ pub struct CachedDirectUrlDist { pub url: VerbatimUrl, pub path: PathBuf, pub editable: bool, + pub r#virtual: bool, pub hashes: Vec, } @@ -57,6 +58,7 @@ impl CachedDist { hashes, path, editable: false, + r#virtual: false, }), Dist::Built(BuiltDist::Path(dist)) => Self::Url(CachedDirectUrlDist { filename, @@ -64,6 +66,7 @@ impl CachedDist { hashes, path, editable: false, + r#virtual: false, }), Dist::Source(SourceDist::Registry(_dist)) => Self::Registry(CachedRegistryDist { filename, @@ -76,6 +79,7 @@ impl CachedDist { hashes, path, editable: false, + r#virtual: false, }), Dist::Source(SourceDist::Git(dist)) => Self::Url(CachedDirectUrlDist { filename, @@ -83,6 +87,7 @@ impl CachedDist { hashes, path, editable: false, + r#virtual: false, }), Dist::Source(SourceDist::Path(dist)) => Self::Url(CachedDirectUrlDist { filename, @@ -90,6 +95,7 @@ impl CachedDist { hashes, path, editable: false, + r#virtual: false, }), Dist::Source(SourceDist::Directory(dist)) => Self::Url(CachedDirectUrlDist { filename, @@ -97,6 +103,7 @@ impl CachedDist { hashes, path, editable: dist.editable, + r#virtual: dist.r#virtual, }), } } @@ -124,6 +131,7 @@ impl CachedDist { url: dist.url.raw().clone(), install_path: path, editable: dist.editable, + r#virtual: dist.r#virtual, }))) } else { Ok(Some(ParsedUrl::try_from(dist.url.to_url())?)) @@ -161,6 +169,7 @@ impl CachedDirectUrlDist { hashes, path, editable: false, + r#virtual: false, } } } diff --git a/crates/distribution-types/src/lib.rs b/crates/distribution-types/src/lib.rs index f2659bf34d89b..c1771ef34c351 100644 --- a/crates/distribution-types/src/lib.rs +++ b/crates/distribution-types/src/lib.rs @@ -309,6 +309,8 @@ pub struct DirectorySourceDist { pub install_path: PathBuf, /// Whether the package should be installed in editable mode. pub editable: bool, + /// Whether the package should be built and installed. + pub r#virtual: bool, /// The URL as it was provided by the user. pub url: VerbatimUrl, } @@ -404,6 +406,7 @@ impl Dist { url: VerbatimUrl, install_path: &Path, editable: bool, + r#virtual: bool, ) -> Result { // Convert to an absolute path. let install_path = path::absolute(install_path)?; @@ -421,6 +424,7 @@ impl Dist { name, install_path, editable, + r#virtual, url, }))) } @@ -458,6 +462,7 @@ impl Dist { url.verbatim, &directory.install_path, directory.editable, + directory.r#virtual, ), ParsedUrl::Git(git) => { Self::from_git_url(name, url.verbatim, git.url, git.subdirectory) diff --git a/crates/distribution-types/src/resolution.rs b/crates/distribution-types/src/resolution.rs index 6cc263abc99d6..4171b4ddeb469 100644 --- a/crates/distribution-types/src/resolution.rs +++ b/crates/distribution-types/src/resolution.rs @@ -220,6 +220,7 @@ impl From<&ResolvedDist> for Requirement { install_path: sdist.install_path.clone(), url: sdist.url.clone(), editable: sdist.editable, + r#virtual: sdist.r#virtual, }, }, ResolvedDist::Installed(dist) => RequirementSource::Registry { diff --git a/crates/pypi-types/src/parsed_url.rs b/crates/pypi-types/src/parsed_url.rs index fd5ad4350bbe4..40437156ccaf5 100644 --- a/crates/pypi-types/src/parsed_url.rs +++ b/crates/pypi-types/src/parsed_url.rs @@ -72,6 +72,7 @@ impl UnnamedRequirementUrl for VerbatimParsedUrl { url: verbatim.to_url(), install_path: verbatim.as_path()?, editable: false, + r#virtual: false, }) } else { ParsedUrl::Path(ParsedPathUrl { @@ -101,6 +102,7 @@ impl UnnamedRequirementUrl for VerbatimParsedUrl { url: verbatim.to_url(), install_path: verbatim.as_path()?, editable: false, + r#virtual: false, }) } else { ParsedUrl::Path(ParsedPathUrl { @@ -208,15 +210,17 @@ pub struct ParsedDirectoryUrl { /// The absolute path to the distribution which we use for installing. pub install_path: PathBuf, pub editable: bool, + pub r#virtual: bool, } impl ParsedDirectoryUrl { /// Construct a [`ParsedDirectoryUrl`] from a path requirement source. - pub fn from_source(install_path: PathBuf, editable: bool, url: Url) -> Self { + pub fn from_source(install_path: PathBuf, editable: bool, r#virtual: bool, url: Url) -> Self { Self { url, install_path, editable, + r#virtual, } } } @@ -370,6 +374,7 @@ impl TryFrom for ParsedUrl { url, install_path: path.clone(), editable: false, + r#virtual: false, })) } else { Ok(Self::Path(ParsedPathUrl { diff --git a/crates/pypi-types/src/requirement.rs b/crates/pypi-types/src/requirement.rs index 594b187cf1bcf..babc8501bc6c7 100644 --- a/crates/pypi-types/src/requirement.rs +++ b/crates/pypi-types/src/requirement.rs @@ -197,12 +197,14 @@ impl From for pep508_rs::Requirement { RequirementSource::Directory { install_path, editable, + r#virtual, url, } => Some(VersionOrUrl::Url(VerbatimParsedUrl { parsed_url: ParsedUrl::Directory(ParsedDirectoryUrl { url: url.to_url(), install_path, editable, + r#virtual, }), verbatim: url, })), @@ -360,6 +362,8 @@ pub enum RequirementSource { install_path: PathBuf, /// For a source tree (a directory), whether to install as an editable. editable: bool, + /// For a source tree (a directory), whether the project should be built and installed. + r#virtual: bool, /// The PEP 508 style URL in the format /// `file:///#subdirectory=`. url: VerbatimUrl, @@ -379,6 +383,7 @@ impl RequirementSource { ParsedUrl::Directory(directory) => RequirementSource::Directory { install_path: directory.install_path.clone(), editable: directory.editable, + r#virtual: directory.r#virtual, url, }, ParsedUrl::Git(git) => RequirementSource::Git { @@ -435,11 +440,13 @@ impl RequirementSource { Self::Directory { install_path, editable, + r#virtual, url, } => Some(VerbatimParsedUrl { parsed_url: ParsedUrl::Directory(ParsedDirectoryUrl::from_source( install_path.clone(), *editable, + *r#virtual, url.to_url(), )), verbatim: url.clone(), @@ -515,11 +522,14 @@ impl RequirementSource { RequirementSource::Directory { install_path, editable, + r#virtual, url, + .. } => Ok(Self::Directory { install_path: relative_to(&install_path, path) .or_else(|_| std::path::absolute(install_path))?, editable, + r#virtual, url, }), } @@ -582,6 +592,8 @@ enum RequirementSourceWire { Directory { directory: PortablePathBuf }, /// Ex) `source = { editable = "/home/ferris/iniconfig" }` Editable { editable: PortablePathBuf }, + /// Ex) `source = { editable = "/home/ferris/iniconfig" }` + Virtual { r#virtual: PortablePathBuf }, /// Ex) `source = { specifier = "foo >1,<2" }` Registry { #[serde(skip_serializing_if = "VersionSpecifiers::is_empty", default)] @@ -668,12 +680,17 @@ impl From for RequirementSourceWire { RequirementSource::Directory { install_path, editable, + r#virtual, url: _, } => { if editable { Self::Editable { editable: PortablePathBuf::from(install_path), } + } else if r#virtual { + Self::Virtual { + r#virtual: PortablePathBuf::from(install_path), + } } else { Self::Directory { directory: PortablePathBuf::from(install_path), @@ -760,6 +777,7 @@ impl TryFrom for RequirementSource { Ok(Self::Directory { install_path: directory, editable: false, + r#virtual: false, url, }) } @@ -769,6 +787,17 @@ impl TryFrom for RequirementSource { Ok(Self::Directory { install_path: editable, editable: true, + r#virtual: false, + url, + }) + } + RequirementSourceWire::Virtual { r#virtual } => { + let r#virtual = PathBuf::from(r#virtual); + let url = VerbatimUrl::from_path(&r#virtual, &*CWD)?; + Ok(Self::Directory { + install_path: r#virtual, + editable: false, + r#virtual: true, url, }) } @@ -825,6 +854,7 @@ mod tests { source: RequirementSource::Directory { install_path: PathBuf::from(path), editable: false, + r#virtual: false, url: VerbatimUrl::from_absolute_path(path).unwrap(), }, origin: None, diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-unix-bare-url.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-unix-bare-url.txt.snap index e3a0a6bf60384..c74fdedd8fdf6 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-unix-bare-url.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-unix-bare-url.txt.snap @@ -23,6 +23,7 @@ RequirementsTxt { }, install_path: "/scripts/packages/black_editable", editable: false, + virtual: false, }, ), verbatim: VerbatimUrl { @@ -72,6 +73,7 @@ RequirementsTxt { }, install_path: "/scripts/packages/black_editable", editable: false, + virtual: false, }, ), verbatim: VerbatimUrl { @@ -125,6 +127,7 @@ RequirementsTxt { }, install_path: "/scripts/packages/black_editable", editable: false, + virtual: false, }, ), verbatim: VerbatimUrl { diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-unix-editable.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-unix-editable.txt.snap index 6eba9d2928f2a..48cc8b4022dce 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-unix-editable.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-unix-editable.txt.snap @@ -25,6 +25,7 @@ RequirementsTxt { }, install_path: "/editable", editable: true, + virtual: false, }, ), verbatim: VerbatimUrl { @@ -81,6 +82,7 @@ RequirementsTxt { }, install_path: "/editable", editable: true, + virtual: false, }, ), verbatim: VerbatimUrl { @@ -137,6 +139,7 @@ RequirementsTxt { }, install_path: "/editable", editable: true, + virtual: false, }, ), verbatim: VerbatimUrl { @@ -193,6 +196,7 @@ RequirementsTxt { }, install_path: "/editable", editable: true, + virtual: false, }, ), verbatim: VerbatimUrl { @@ -249,6 +253,7 @@ RequirementsTxt { }, install_path: "/editable", editable: true, + virtual: false, }, ), verbatim: VerbatimUrl { @@ -298,6 +303,7 @@ RequirementsTxt { }, install_path: "/editable[d", editable: true, + virtual: false, }, ), verbatim: VerbatimUrl { diff --git a/crates/uv-distribution/src/index/cached_wheel.rs b/crates/uv-distribution/src/index/cached_wheel.rs index 2e7caa5088b24..9b80db305389e 100644 --- a/crates/uv-distribution/src/index/cached_wheel.rs +++ b/crates/uv-distribution/src/index/cached_wheel.rs @@ -55,6 +55,7 @@ impl CachedWheel { url, path: self.entry.into_path_buf(), editable: false, + r#virtual: false, hashes: self.hashes, } } @@ -66,6 +67,19 @@ impl CachedWheel { url, path: self.entry.into_path_buf(), editable: true, + r#virtual: false, + hashes: self.hashes, + } + } + + /// Convert a [`CachedWheel`] into an editable [`CachedDirectUrlDist`]. + pub fn into_virtual(self, url: VerbatimUrl) -> CachedDirectUrlDist { + CachedDirectUrlDist { + filename: self.filename, + url, + path: self.entry.into_path_buf(), + editable: false, + r#virtual: true, hashes: self.hashes, } } diff --git a/crates/uv-distribution/src/metadata/lowering.rs b/crates/uv-distribution/src/metadata/lowering.rs index 26f465d23611b..5deaf520f1e6f 100644 --- a/crates/uv-distribution/src/metadata/lowering.rs +++ b/crates/uv-distribution/src/metadata/lowering.rs @@ -12,7 +12,7 @@ use pypi_types::{ParsedUrlError, Requirement, RequirementSource, VerbatimParsedU use uv_git::GitReference; use uv_normalize::PackageName; use uv_warnings::warn_user_once; -use uv_workspace::pyproject::Source; +use uv_workspace::pyproject::{PyProjectToml, Source}; use uv_workspace::Workspace; #[derive(Debug, Clone)] @@ -148,10 +148,21 @@ impl LoweredRequirement { "Invalid path in file URL", )) })?; - RequirementSource::Directory { - install_path, - url, - editable: true, + + if member.pyproject_toml().is_package() { + RequirementSource::Directory { + install_path, + url, + editable: true, + r#virtual: false, + } + } else { + RequirementSource::Directory { + install_path, + url, + editable: false, + r#virtual: true, + } } } Source::CatchAll { .. } => { @@ -372,17 +383,41 @@ fn path_source( "Invalid path in file URL", )) })?; + let is_dir = if let Ok(metadata) = install_path.metadata() { metadata.is_dir() } else { install_path.extension().is_none() }; if is_dir { - Ok(RequirementSource::Directory { - install_path, - url, - editable, - }) + if editable { + Ok(RequirementSource::Directory { + install_path, + url, + editable, + r#virtual: false, + }) + } else { + // Determine whether the project is a package or virtual. + let is_package = { + let pyproject_path = install_path.join("pyproject.toml"); + fs_err::read_to_string(&pyproject_path) + .ok() + .and_then(|contents| PyProjectToml::from_string(contents).ok()) + .map(|pyproject_toml| pyproject_toml.is_package()) + .unwrap_or(true) + }; + + // If a project is not a package, treat it as a virtual dependency. + let r#virtual = !is_package; + + Ok(RequirementSource::Directory { + install_path, + url, + editable, + r#virtual, + }) + } } else { if editable { return Err(LoweringError::EditableFile(url.to_string())); diff --git a/crates/uv-installer/src/plan.rs b/crates/uv-installer/src/plan.rs index e0340cf25977c..0bbe89fa2f96e 100644 --- a/crates/uv-installer/src/plan.rs +++ b/crates/uv-installer/src/plan.rs @@ -264,6 +264,7 @@ impl<'a> Planner<'a> { } RequirementSource::Directory { + r#virtual, url, editable, install_path, @@ -284,6 +285,7 @@ impl<'a> Planner<'a> { url: url.clone(), install_path, editable: *editable, + r#virtual: *r#virtual, }; // Find the most-compatible wheel from the cache, since we don't know diff --git a/crates/uv-installer/src/satisfies.rs b/crates/uv-installer/src/satisfies.rs index 500fc079873d6..c91f95d01c3ca 100644 --- a/crates/uv-installer/src/satisfies.rs +++ b/crates/uv-installer/src/satisfies.rs @@ -197,6 +197,7 @@ impl RequirementSatisfaction { RequirementSource::Directory { install_path: requested_path, editable: requested_editable, + r#virtual: _, url: _, } => { let InstalledDist::Url(InstalledDirectUrlDist { direct_url, .. }) = &distribution diff --git a/crates/uv-requirements/src/lookahead.rs b/crates/uv-requirements/src/lookahead.rs index f703581e07843..3415e37070301 100644 --- a/crates/uv-requirements/src/lookahead.rs +++ b/crates/uv-requirements/src/lookahead.rs @@ -288,6 +288,7 @@ fn required_dist(requirement: &Requirement) -> Result, distribution } => Dist::from_file_url(requirement.name.clone(), url.clone(), install_path, *ext)?, RequirementSource::Directory { install_path, + r#virtual, url, editable, } => Dist::from_directory_url( @@ -295,6 +296,7 @@ fn required_dist(requirement: &Requirement) -> Result, distribution url.clone(), install_path, *editable, + *r#virtual, )?, })) } diff --git a/crates/uv-resolver/src/lock.rs b/crates/uv-resolver/src/lock.rs index 0da9f4a7a6c47..fa443fc19218d 100644 --- a/crates/uv-resolver/src/lock.rs +++ b/crates/uv-resolver/src/lock.rs @@ -1394,6 +1394,11 @@ impl Package { source_type: "editable", } .into()), + Source::Virtual(_) => Err(LockErrorKind::InvalidWheelSource { + id: self.id.clone(), + source_type: "virtual", + } + .into()), }; }; @@ -1431,6 +1436,7 @@ impl Package { url: verbatim_url(workspace_root.join(path), &self.id)?, install_path: workspace_root.join(path), editable: false, + r#virtual: false, }; distribution_types::SourceDist::Directory(dir_dist) } @@ -1440,6 +1446,17 @@ impl Package { url: verbatim_url(workspace_root.join(path), &self.id)?, install_path: workspace_root.join(path), editable: true, + r#virtual: false, + }; + distribution_types::SourceDist::Directory(dir_dist) + } + Source::Virtual(path) => { + let dir_dist = DirectorySourceDist { + name: self.id.name.clone(), + url: verbatim_url(workspace_root.join(path), &self.id)?, + install_path: workspace_root.join(path), + editable: false, + r#virtual: true, }; distribution_types::SourceDist::Directory(dir_dist) } @@ -1985,6 +2002,8 @@ enum Source { Directory(PathBuf), /// A path to a local directory that should be installed as editable. Editable(PathBuf), + /// A path to a local directory that should not be built or installed. + Virtual(PathBuf), } impl Source { @@ -2093,6 +2112,8 @@ impl Source { .map_err(LockErrorKind::DistributionRelativePath)?; if directory_dist.editable { Ok(Source::Editable(path)) + } else if directory_dist.r#virtual { + Ok(Source::Virtual(path)) } else { Ok(Source::Directory(path)) } @@ -2182,6 +2203,9 @@ impl Source { Value::from(PortablePath::from(path).to_string()), ); } + Source::Virtual(ref path) => { + source_table.insert("virtual", Value::from(PortablePath::from(path).to_string())); + } } table.insert("source", value(source_table)); } @@ -2198,7 +2222,8 @@ impl std::fmt::Display for Source { Source::Registry(RegistrySource::Path(path)) | Source::Path(path) | Source::Directory(path) - | Source::Editable(path) => { + | Source::Editable(path) + | Source::Virtual(path) => { write!(f, "{}+{}", self.name(), PortablePath::from(path)) } } @@ -2214,6 +2239,7 @@ impl Source { Self::Path(..) => "path", Self::Directory(..) => "directory", Self::Editable(..) => "editable", + Self::Virtual(..) => "virtual", } } @@ -2228,7 +2254,9 @@ impl Source { match *self { Self::Registry(..) => None, Self::Direct(..) | Self::Path(..) => Some(true), - Self::Git(..) | Self::Directory(..) | Self::Editable(..) => Some(false), + Self::Git(..) | Self::Directory(..) | Self::Editable(..) | Self::Virtual(..) => { + Some(false) + } } } } @@ -2256,6 +2284,9 @@ enum SourceWire { Editable { editable: PortablePathBuf, }, + Virtual { + r#virtual: PortablePathBuf, + }, } impl TryFrom for Source { @@ -2292,6 +2323,7 @@ impl TryFrom for Source { Path { path } => Ok(Source::Path(path.into())), Directory { directory } => Ok(Source::Directory(directory.into())), Editable { editable } => Ok(Source::Editable(editable.into())), + Virtual { r#virtual } => Ok(Source::Virtual(r#virtual.into())), } } } @@ -3280,6 +3312,7 @@ fn normalize_requirement( RequirementSource::Directory { install_path, editable, + r#virtual, url: _, } => { let install_path = uv_fs::normalize_path(&workspace.install_path().join(&install_path)); @@ -3293,6 +3326,7 @@ fn normalize_requirement( source: RequirementSource::Directory { install_path, editable, + r#virtual, url, }, origin: None, diff --git a/crates/uv-resolver/src/pubgrub/dependencies.rs b/crates/uv-resolver/src/pubgrub/dependencies.rs index 4aecef952d800..8b5a4c7155675 100644 --- a/crates/uv-resolver/src/pubgrub/dependencies.rs +++ b/crates/uv-resolver/src/pubgrub/dependencies.rs @@ -145,12 +145,14 @@ impl PubGrubRequirement { } RequirementSource::Directory { editable, + r#virtual, url, install_path, } => { let parsed_url = ParsedUrl::Directory(ParsedDirectoryUrl::from_source( install_path.clone(), *editable, + *r#virtual, url.to_url(), )); (url, parsed_url) diff --git a/crates/uv-workspace/src/workspace.rs b/crates/uv-workspace/src/workspace.rs index bb0987dacf954..3ef58426952a7 100644 --- a/crates/uv-workspace/src/workspace.rs +++ b/crates/uv-workspace/src/workspace.rs @@ -281,10 +281,20 @@ impl Workspace { name: project.name.clone(), extras, marker: MarkerTree::TRUE, - source: RequirementSource::Directory { - install_path: member.root.clone(), - editable: true, - url, + source: if member.pyproject_toml.is_package() { + RequirementSource::Directory { + install_path: member.root.clone(), + editable: true, + r#virtual: false, + url, + } + } else { + RequirementSource::Directory { + install_path: member.root.clone(), + editable: false, + r#virtual: true, + url, + } }, origin: None, }) diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 645d128879e9c..294bb78eb8a4d 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -1,8 +1,7 @@ use anyhow::{Context, Result}; use itertools::Itertools; -use rustc_hash::FxHashSet; -use distribution_types::Name; +use distribution_types::{Dist, ResolvedDist, SourceDist}; use pep508_rs::MarkerTree; use uv_auth::store_credentials_from_url; use uv_cache::Cache; @@ -199,7 +198,7 @@ pub(super) async fn do_sync( let resolution = lock.to_resolution(project, &markers, tags, extras, &dev)?; // Always skip virtual projects, which shouldn't be built or installed. - let resolution = apply_no_virtual_project(resolution, project); + let resolution = apply_no_virtual_project(resolution); // Filter resolution based on install-specific options. let resolution = install_options.filter_resolution(resolution, project); @@ -299,28 +298,20 @@ pub(super) async fn do_sync( /// Filter out any virtual workspace members. fn apply_no_virtual_project( resolution: distribution_types::Resolution, - project: &VirtualProject, ) -> distribution_types::Resolution { - let VirtualProject::Project(project) = project else { - // If the project is _only_ a virtual workspace root, we don't need to filter it out. - return resolution; - }; + resolution.filter(|dist| { + let ResolvedDist::Installable(dist) = dist else { + return true; + }; + + let Dist::Source(dist) = dist else { + return true; + }; - let virtual_members = project - .workspace() - .packages() - .iter() - .filter_map(|(name, package)| { - // A project is a package if it's explicitly marked as such, _or_ if a build system is - // present. - if package.pyproject_toml().is_package() { - None - } else { - Some(name) - } - }) - .collect::>(); + let SourceDist::Directory(dist) = dist else { + return true; + }; - // Remove any virtual members from the resolution. - resolution.filter(|dist| !virtual_members.contains(dist.name())) + !dist.r#virtual + }) } diff --git a/crates/uv/tests/edit.rs b/crates/uv/tests/edit.rs index ff3db080f8d89..e56978056c9af 100644 --- a/crates/uv/tests/edit.rs +++ b/crates/uv/tests/edit.rs @@ -3541,7 +3541,7 @@ fn add_non_project() -> Result<()> { ----- stdout ----- ----- stderr ----- - error: Found a non-project workspace root; optional dependencies are unsupported (instead, use: `uv add --dev`) + error: Project is missing a `[project]` table; add a `[project]` table to support optional dependencies, or use `uv add --dev` "###); // Adding `iniconfig` as a dev dependency should succeed. diff --git a/crates/uv/tests/lock.rs b/crates/uv/tests/lock.rs index d016fba15fe63..a2bd288794515 100644 --- a/crates/uv/tests/lock.rs +++ b/crates/uv/tests/lock.rs @@ -3853,6 +3853,10 @@ fn lock_python_version_marker_complement() -> Result<()> { "typing-extensions ; python_full_version > '3.10'", "typing-extensions ; python_full_version <= '3.10'", ] + + [build-system] + requires = ["setuptools>=42", "wheel"] + build-backend = "setuptools.build_meta" "#, )?; @@ -5206,6 +5210,10 @@ fn lock_same_version_multiple_urls() -> Result<()> { "dependency @ {} ; sys_platform == 'darwin'", "dependency @ {} ; sys_platform != 'darwin'", ] + + [build-system] + requires = ["setuptools>=42", "wheel"] + build-backend = "setuptools.build_meta" "#, Url::from_file_path(context.temp_dir.join("v1")).unwrap(), Url::from_file_path(context.temp_dir.join("v2")).unwrap(), @@ -7795,12 +7803,12 @@ fn lock_editable() -> Result<()> { ] [package.metadata] - requires-dist = [{ name = "library", directory = "library" }] + requires-dist = [{ name = "library", virtual = "library" }] [[package]] name = "library" version = "0.1.0" - source = { directory = "library" } + source = { virtual = "library" } [[package]] name = "workspace" @@ -7811,7 +7819,7 @@ fn lock_editable() -> Result<()> { ] [package.metadata] - requires-dist = [{ name = "library", directory = "library" }] + requires-dist = [{ name = "library", virtual = "library" }] "### ); }); @@ -8817,7 +8825,7 @@ fn lock_remove_member() -> Result<()> { [[package]] name = "project" version = "0.1.0" - source = { editable = "." } + source = { virtual = "." } "### ); }); @@ -11349,7 +11357,7 @@ fn lock_explicit_virtual_project() -> Result<()> { [[package]] name = "project" version = "0.1.0" - source = { editable = "." } + source = { virtual = "." } dependencies = [ { name = "black" }, ] @@ -11421,7 +11429,7 @@ fn lock_explicit_virtual_project() -> Result<()> { Ok(()) } -/// Lock a project that is implicitly virtual (by way of omitting `build-system`0> +/// Lock a project that is implicitly virtual (by way of omitting `build-system`). #[test] fn lock_implicit_virtual_project() -> Result<()> { let context = TestContext::new("3.12"); @@ -11566,7 +11574,7 @@ fn lock_implicit_virtual_project() -> Result<()> { [[package]] name = "project" version = "0.1.0" - source = { editable = "." } + source = { virtual = "." } dependencies = [ { name = "black" }, ] @@ -11637,3 +11645,164 @@ fn lock_implicit_virtual_project() -> Result<()> { Ok(()) } + +/// Lock a project that has a path dependency that is implicitly virtual (by way of omitting +/// `build-system`). +#[test] +fn lock_implicit_virtual_path() -> 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", "child"] + + [tool.uv.sources] + child = { path = "./child" } + "#, + )?; + + let child = context.temp_dir.child("child"); + child.child("pyproject.toml").write_str( + r#" + [project] + name = "child" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig >1"] + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 6 packages in [TIME] + "###); + + let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "anyio" + version = "4.3.0" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6", size = 159642 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8", size = 85584 }, + ] + + [[package]] + name = "child" + version = "0.1.0" + source = { virtual = "child" } + dependencies = [ + { name = "iniconfig" }, + ] + + [package.metadata] + requires-dist = [{ name = "iniconfig", specifier = ">1" }] + + [[package]] + name = "idna" + version = "3.6" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 }, + ] + + [[package]] + name = "iniconfig" + version = "2.0.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "anyio" }, + { name = "child" }, + ] + + [package.metadata] + requires-dist = [ + { name = "anyio", specifier = ">3" }, + { name = "child", virtual = "child" }, + ] + + [[package]] + name = "sniffio" + version = "1.3.1" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, + ] + "### + ); + }); + + // Re-run with `--locked`. + uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 6 packages in [TIME] + "###); + + // Re-run with `--offline`. We shouldn't need a network connection to validate an + // already-correct lockfile with immutable metadata. + uv_snapshot!(context.filters(), context.lock().arg("--locked").arg("--offline").arg("--no-cache"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 6 packages in [TIME] + "###); + + // Install from the lockfile. The virtual project should _not_ be installed. + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Prepared 4 packages in [TIME] + Installed 4 packages in [TIME] + + anyio==4.3.0 + + idna==3.6 + + iniconfig==2.0.0 + + sniffio==1.3.1 + "###); + + Ok(()) +} diff --git a/crates/uv/tests/lock_scenarios.rs b/crates/uv/tests/lock_scenarios.rs index f10c1c9cf3527..2eb4fb93247fa 100644 --- a/crates/uv/tests/lock_scenarios.rs +++ b/crates/uv/tests/lock_scenarios.rs @@ -103,7 +103,7 @@ fn fork_allows_non_conflicting_non_overlapping_dependencies() -> Result<()> { [[package]] name = "project" version = "0.1.0" - source = { editable = "." } + source = { virtual = "." } dependencies = [ { name = "package-a", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] @@ -215,7 +215,7 @@ fn fork_allows_non_conflicting_repeated_dependencies() -> Result<()> { [[package]] name = "project" version = "0.1.0" - source = { editable = "." } + source = { virtual = "." } dependencies = [ { name = "package-a" }, ] @@ -334,7 +334,7 @@ fn fork_basic() -> Result<()> { [[package]] name = "project" version = "0.1.0" - source = { editable = "." } + source = { virtual = "." } dependencies = [ { name = "package-a", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'darwin'" }, { name = "package-a", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'linux'" }, @@ -667,7 +667,7 @@ fn fork_filter_sibling_dependencies() -> Result<()> { [[package]] name = "project" version = "0.1.0" - source = { editable = "." } + source = { virtual = "." } dependencies = [ { name = "package-a", version = "4.3.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'darwin'" }, { name = "package-a", version = "4.4.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'linux'" }, @@ -789,7 +789,7 @@ fn fork_upgrade() -> Result<()> { [[package]] name = "project" version = "0.1.0" - source = { editable = "." } + source = { virtual = "." } dependencies = [ { name = "package-foo" }, ] @@ -938,7 +938,7 @@ fn fork_incomplete_markers() -> Result<()> { [[package]] name = "project" version = "0.1.0" - source = { editable = "." } + source = { virtual = "." } dependencies = [ { name = "package-a", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "python_full_version < '3.10'" }, { name = "package-a", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "python_full_version >= '3.11'" }, @@ -1074,7 +1074,7 @@ fn fork_marker_accrue() -> Result<()> { [[package]] name = "project" version = "0.1.0" - source = { editable = "." } + source = { virtual = "." } dependencies = [ { name = "package-a", marker = "implementation_name == 'cpython'" }, { name = "package-b", marker = "implementation_name == 'pypy'" }, @@ -1317,7 +1317,7 @@ fn fork_marker_inherit_combined_allowed() -> Result<()> { [[package]] name = "project" version = "0.1.0" - source = { editable = "." } + source = { virtual = "." } dependencies = [ { name = "package-a", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'darwin'" }, { name = "package-a", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'linux'" }, @@ -1485,7 +1485,7 @@ fn fork_marker_inherit_combined_disallowed() -> Result<()> { [[package]] name = "project" version = "0.1.0" - source = { editable = "." } + source = { virtual = "." } dependencies = [ { name = "package-a", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'darwin'" }, { name = "package-a", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'linux'" }, @@ -1654,7 +1654,7 @@ fn fork_marker_inherit_combined() -> Result<()> { [[package]] name = "project" version = "0.1.0" - source = { editable = "." } + source = { virtual = "." } dependencies = [ { name = "package-a", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'darwin'" }, { name = "package-a", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'linux'" }, @@ -1796,7 +1796,7 @@ fn fork_marker_inherit_isolated() -> Result<()> { [[package]] name = "project" version = "0.1.0" - source = { editable = "." } + source = { virtual = "." } dependencies = [ { name = "package-a", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'darwin'" }, { name = "package-a", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'linux'" }, @@ -1956,7 +1956,7 @@ fn fork_marker_inherit_transitive() -> Result<()> { [[package]] name = "project" version = "0.1.0" - source = { editable = "." } + source = { virtual = "." } dependencies = [ { name = "package-a", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'darwin'" }, { name = "package-a", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'linux'" }, @@ -2088,7 +2088,7 @@ fn fork_marker_inherit() -> Result<()> { [[package]] name = "project" version = "0.1.0" - source = { editable = "." } + source = { virtual = "." } dependencies = [ { name = "package-a", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'darwin'" }, { name = "package-a", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'linux'" }, @@ -2247,7 +2247,7 @@ fn fork_marker_limited_inherit() -> Result<()> { [[package]] name = "project" version = "0.1.0" - source = { editable = "." } + source = { virtual = "." } dependencies = [ { name = "package-a", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'darwin'" }, { name = "package-a", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'linux'" }, @@ -2390,7 +2390,7 @@ fn fork_marker_selection() -> Result<()> { [[package]] name = "project" version = "0.1.0" - source = { editable = "." } + source = { virtual = "." } dependencies = [ { name = "package-a" }, { name = "package-b", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'darwin'" }, @@ -2557,7 +2557,7 @@ fn fork_marker_track() -> Result<()> { [[package]] name = "project" version = "0.1.0" - source = { editable = "." } + source = { virtual = "." } dependencies = [ { name = "package-a" }, { name = "package-b", version = "2.7", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'darwin'" }, @@ -2692,7 +2692,7 @@ fn fork_non_fork_marker_transitive() -> Result<()> { [[package]] name = "project" version = "0.1.0" - source = { editable = "." } + source = { virtual = "." } dependencies = [ { name = "package-a" }, { name = "package-b" }, @@ -2972,7 +2972,7 @@ fn fork_overlapping_markers_basic() -> Result<()> { [[package]] name = "project" version = "0.1.0" - source = { editable = "." } + source = { virtual = "." } dependencies = [ { name = "package-a" }, ] @@ -3216,7 +3216,7 @@ fn preferences_dependent_forking_bistable() -> Result<()> { [[package]] name = "project" version = "0.1.0" - source = { editable = "." } + source = { virtual = "." } dependencies = [ { name = "package-cleaver" }, ] @@ -3635,7 +3635,7 @@ fn preferences_dependent_forking_tristable() -> Result<()> { [[package]] name = "project" version = "0.1.0" - source = { editable = "." } + source = { virtual = "." } dependencies = [ { name = "package-bar", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform != 'linux'" }, { name = "package-bar", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'linux'" }, @@ -3836,7 +3836,7 @@ fn preferences_dependent_forking() -> Result<()> { [[package]] name = "project" version = "0.1.0" - source = { editable = "." } + source = { virtual = "." } dependencies = [ { name = "package-bar", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform != 'linux'" }, { name = "package-bar", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'linux'" }, @@ -4021,7 +4021,7 @@ fn fork_remaining_universe_partitioning() -> Result<()> { [[package]] name = "project" version = "0.1.0" - source = { editable = "." } + source = { virtual = "." } dependencies = [ { name = "package-a", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'illumos'" }, { name = "package-a", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'windows'" }, @@ -4112,7 +4112,7 @@ fn fork_requires_python_full_prerelease() -> Result<()> { [[package]] name = "project" version = "0.1.0" - source = { editable = "." } + source = { virtual = "." } [package.metadata] requires-dist = [{ name = "package-a", marker = "python_full_version == '3.9'", specifier = "==1.0.0" }] @@ -4196,7 +4196,7 @@ fn fork_requires_python_full() -> Result<()> { [[package]] name = "project" version = "0.1.0" - source = { editable = "." } + source = { virtual = "." } [package.metadata] requires-dist = [{ name = "package-a", marker = "python_full_version == '3.9'", specifier = "==1.0.0" }] @@ -4293,7 +4293,7 @@ fn fork_requires_python_patch_overlap() -> Result<()> { [[package]] name = "project" version = "0.1.0" - source = { editable = "." } + source = { virtual = "." } dependencies = [ { name = "package-a", marker = "python_full_version < '3.11'" }, ] @@ -4377,7 +4377,7 @@ fn fork_requires_python() -> Result<()> { [[package]] name = "project" version = "0.1.0" - source = { editable = "." } + source = { virtual = "." } [package.metadata] requires-dist = [{ name = "package-a", marker = "python_full_version == '3.9.*'", specifier = "==1.0.0" }] diff --git a/crates/uv/tests/snapshots/workflow__jax_instability-2.snap b/crates/uv/tests/snapshots/workflow__jax_instability-2.snap index 2457bbd3eb048..28f3f0c16eefb 100644 --- a/crates/uv/tests/snapshots/workflow__jax_instability-2.snap +++ b/crates/uv/tests/snapshots/workflow__jax_instability-2.snap @@ -162,7 +162,7 @@ wheels = [ [[package]] name = "uv-lock-instability" version = "0.1.0" -source = { editable = "." } +source = { virtual = "." } dependencies = [ { name = "jax" }, ] diff --git a/crates/uv/tests/workflow.rs b/crates/uv/tests/workflow.rs index fbff72598faae..c7ce5949198c6 100644 --- a/crates/uv/tests/workflow.rs +++ b/crates/uv/tests/workflow.rs @@ -466,7 +466,7 @@ fn jax_instability() -> Result<()> { +[[package]] name = "uv-lock-instability" version = "0.1.0" - source = { editable = "." } + source = { virtual = "." } dependencies = [ { name = "jax" }, + { name = "tzdata" }, @@ -519,7 +519,7 @@ fn jax_instability() -> Result<()> { -[[package]] name = "uv-lock-instability" version = "0.1.0" - source = { editable = "." } + source = { virtual = "." } dependencies = [ { name = "jax" }, - { name = "tzdata" },