Only use relative paths in lockfile (#6490)

For users who were using absolute paths in the `pyproject.toml`
previously, this is a behavior change: We now convert all absolute paths
in `path` entries to relative paths. Since i assume that no-one relies
on absolute path in their lockfiles - they are intended to be portable -
I'm tagging this as a bugfix.

Closes https://github.com/astral-sh/uv/pull/6438
Fixes https://github.com/astral-sh/uv/issues/6371
This commit is contained in:
Charlie Marsh 2024-08-23 22:19:10 -04:00 committed by GitHub
parent 611a9003c9
commit f7835243c5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 329 additions and 383 deletions

1
Cargo.lock generated
View file

@ -4828,7 +4828,6 @@ dependencies = [
"insta",
"install-wheel-rs",
"nanoid",
"path-absolutize",
"pep440_rs",
"pep508_rs",
"platform-tags",

View file

@ -172,7 +172,6 @@ impl<'a> From<&'a PathSourceDist> for PathSourceUrl<'a> {
pub struct DirectorySourceUrl<'a> {
pub url: &'a Url,
pub install_path: Cow<'a, Path>,
pub lock_path: Cow<'a, Path>,
pub editable: bool,
}
@ -187,7 +186,6 @@ impl<'a> From<&'a DirectorySourceDist> for DirectorySourceUrl<'a> {
Self {
url: &dist.url,
install_path: Cow::Borrowed(&dist.install_path),
lock_path: Cow::Borrowed(&dist.lock_path),
editable: dist.editable,
}
}

View file

@ -122,8 +122,7 @@ impl CachedDist {
.map_err(|()| anyhow!("Invalid path in file URL"))?;
Ok(Some(ParsedUrl::Directory(ParsedDirectoryUrl {
url: dist.url.raw().clone(),
install_path: path.clone(),
lock_path: path,
install_path: path,
editable: dist.editable,
})))
} else {

View file

@ -33,6 +33,7 @@
//!
//! Since we read this information from [`direct_url.json`](https://packaging.python.org/en/latest/specifications/direct-url-data-structure/), it doesn't match the information [`Dist`] exactly.
use std::borrow::Cow;
use std::path;
use std::path::{Path, PathBuf};
use std::str::FromStr;
@ -237,10 +238,6 @@ pub struct PathBuiltDist {
pub filename: WheelFilename,
/// The absolute path to the wheel which we use for installing.
pub install_path: PathBuf,
/// The absolute path or path relative to the workspace root pointing to the wheel
/// which we use for locking. Unlike `given` on the verbatim URL all environment variables
/// are resolved, and unlike the install path, we did not yet join it on the base directory.
pub lock_path: PathBuf,
/// The URL as it was provided by the user.
pub url: VerbatimUrl,
}
@ -298,10 +295,6 @@ pub struct PathSourceDist {
pub name: PackageName,
/// The absolute path to the distribution which we use for installing.
pub install_path: PathBuf,
/// The absolute path or path relative to the workspace root pointing to the distribution
/// which we use for locking. Unlike `given` on the verbatim URL all environment variables
/// are resolved, and unlike the install path, we did not yet join it on the base directory.
pub lock_path: PathBuf,
/// The file extension, e.g. `tar.gz`, `zip`, etc.
pub ext: SourceDistExtension,
/// The URL as it was provided by the user.
@ -314,10 +307,6 @@ pub struct DirectorySourceDist {
pub name: PackageName,
/// The absolute path to the distribution which we use for installing.
pub install_path: PathBuf,
/// The absolute path or path relative to the workspace root pointing to the distribution
/// which we use for locking. Unlike `given` on the verbatim URL all environment variables
/// are resolved, and unlike the install path, we did not yet join it on the base directory.
pub lock_path: PathBuf,
/// Whether the package should be installed in editable mode.
pub editable: bool,
/// The URL as it was provided by the user.
@ -369,11 +358,10 @@ impl Dist {
name: PackageName,
url: VerbatimUrl,
install_path: &Path,
lock_path: &Path,
ext: DistExtension,
) -> Result<Dist, Error> {
// Convert to an absolute path.
let install_path = std::path::absolute(install_path)?;
let install_path = path::absolute(install_path)?;
// Normalize the path.
let install_path = normalize_absolute_path(&install_path)?;
@ -398,14 +386,12 @@ impl Dist {
Ok(Self::Built(BuiltDist::Path(PathBuiltDist {
filename,
install_path,
lock_path: lock_path.to_path_buf(),
url,
})))
}
DistExtension::Source(ext) => Ok(Self::Source(SourceDist::Path(PathSourceDist {
name,
install_path,
lock_path: lock_path.to_path_buf(),
ext,
url,
}))),
@ -417,11 +403,10 @@ impl Dist {
name: PackageName,
url: VerbatimUrl,
install_path: &Path,
lock_path: &Path,
editable: bool,
) -> Result<Dist, Error> {
// Convert to an absolute path.
let install_path = std::path::absolute(install_path)?;
let install_path = path::absolute(install_path)?;
// Normalize the path.
let install_path = normalize_absolute_path(&install_path)?;
@ -435,7 +420,6 @@ impl Dist {
Ok(Self::Source(SourceDist::Directory(DirectorySourceDist {
name,
install_path,
lock_path: lock_path.to_path_buf(),
editable,
url,
})))
@ -466,18 +450,13 @@ impl Dist {
archive.subdirectory,
archive.ext,
),
ParsedUrl::Path(file) => Self::from_file_url(
name,
url.verbatim,
&file.install_path,
&file.lock_path,
file.ext,
),
ParsedUrl::Path(file) => {
Self::from_file_url(name, url.verbatim, &file.install_path, file.ext)
}
ParsedUrl::Directory(directory) => Self::from_directory_url(
name,
url.verbatim,
&directory.install_path,
&directory.lock_path,
directory.editable,
),
ParsedUrl::Git(git) => {

View file

@ -185,7 +185,6 @@ impl From<&ResolvedDist> for Requirement {
}
Dist::Built(BuiltDist::Path(wheel)) => RequirementSource::Path {
install_path: wheel.install_path.clone(),
lock_path: wheel.lock_path.clone(),
url: wheel.url.clone(),
ext: DistExtension::Wheel,
},
@ -214,13 +213,11 @@ impl From<&ResolvedDist> for Requirement {
},
Dist::Source(SourceDist::Path(sdist)) => RequirementSource::Path {
install_path: sdist.install_path.clone(),
lock_path: sdist.lock_path.clone(),
url: sdist.url.clone(),
ext: DistExtension::Source(sdist.ext),
},
Dist::Source(SourceDist::Directory(sdist)) => RequirementSource::Directory {
install_path: sdist.install_path.clone(),
lock_path: sdist.lock_path.clone(),
url: sdist.url.clone(),
editable: sdist.editable,
},

View file

@ -71,14 +71,12 @@ impl UnnamedRequirementUrl for VerbatimParsedUrl {
ParsedUrl::Directory(ParsedDirectoryUrl {
url: verbatim.to_url(),
install_path: verbatim.as_path()?,
lock_path: path.as_ref().to_path_buf(),
editable: false,
})
} else {
ParsedUrl::Path(ParsedPathUrl {
url: verbatim.to_url(),
install_path: verbatim.as_path()?,
lock_path: path.as_ref().to_path_buf(),
ext: DistExtension::from_path(&path).map_err(|err| {
ParsedUrlError::MissingExtensionPath(path.as_ref().to_path_buf(), err)
})?,
@ -102,14 +100,12 @@ impl UnnamedRequirementUrl for VerbatimParsedUrl {
ParsedUrl::Directory(ParsedDirectoryUrl {
url: verbatim.to_url(),
install_path: verbatim.as_path()?,
lock_path: path.as_ref().to_path_buf(),
editable: false,
})
} else {
ParsedUrl::Path(ParsedPathUrl {
url: verbatim.to_url(),
install_path: verbatim.as_path()?,
lock_path: path.as_ref().to_path_buf(),
ext: DistExtension::from_path(&path).map_err(|err| {
ParsedUrlError::MissingExtensionPath(path.as_ref().to_path_buf(), err)
})?,
@ -187,26 +183,16 @@ pub struct ParsedPathUrl {
pub url: Url,
/// The absolute path to the distribution which we use for installing.
pub install_path: PathBuf,
/// The absolute path or path relative to the workspace root pointing to the distribution
/// which we use for locking. Unlike `given` on the verbatim URL all environment variables
/// are resolved, and unlike the install path, we did not yet join it on the base directory.
pub lock_path: PathBuf,
/// The file extension, e.g. `tar.gz`, `zip`, etc.
pub ext: DistExtension,
}
impl ParsedPathUrl {
/// Construct a [`ParsedPathUrl`] from a path requirement source.
pub fn from_source(
install_path: PathBuf,
lock_path: PathBuf,
ext: DistExtension,
url: Url,
) -> Self {
pub fn from_source(install_path: PathBuf, ext: DistExtension, url: Url) -> Self {
Self {
url,
install_path,
lock_path,
ext,
}
}
@ -221,25 +207,15 @@ pub struct ParsedDirectoryUrl {
pub url: Url,
/// The absolute path to the distribution which we use for installing.
pub install_path: PathBuf,
/// The absolute path or path relative to the workspace root pointing to the distribution
/// which we use for locking. Unlike `given` on the verbatim URL all environment variables
/// are resolved, and unlike the install path, we did not yet join it on the base directory.
pub lock_path: PathBuf,
pub editable: bool,
}
impl ParsedDirectoryUrl {
/// Construct a [`ParsedDirectoryUrl`] from a path requirement source.
pub fn from_source(
install_path: PathBuf,
lock_path: PathBuf,
editable: bool,
url: Url,
) -> Self {
pub fn from_source(install_path: PathBuf, editable: bool, url: Url) -> Self {
Self {
url,
install_path,
lock_path,
editable,
}
}
@ -393,7 +369,6 @@ impl TryFrom<Url> for ParsedUrl {
Ok(Self::Directory(ParsedDirectoryUrl {
url,
install_path: path.clone(),
lock_path: path,
editable: false,
}))
} else {
@ -402,7 +377,6 @@ impl TryFrom<Url> for ParsedUrl {
ext: DistExtension::from_path(&path)
.map_err(|err| ParsedUrlError::MissingExtensionPath(path.clone(), err))?,
install_path: path.clone(),
lock_path: path,
}))
}
} else {

View file

@ -1,4 +1,5 @@
use std::fmt::{Display, Formatter};
use std::io;
use std::path::{Path, PathBuf};
use std::str::FromStr;
@ -10,7 +11,7 @@ use pep440_rs::VersionSpecifiers;
use pep508_rs::{
marker, MarkerEnvironment, MarkerTree, RequirementOrigin, VerbatimUrl, VersionOrUrl,
};
use uv_fs::{PortablePathBuf, CWD};
use uv_fs::{relative_to, PortablePathBuf, CWD};
use uv_git::{GitReference, GitSha, GitUrl};
use uv_normalize::{ExtraName, PackageName};
@ -105,6 +106,14 @@ impl Requirement {
_ => self,
}
}
/// Convert the requirement to a [`Requirement`] relative to the given path.
pub fn relative_to(self, path: &Path) -> Result<Self, io::Error> {
Ok(Self {
source: self.source.relative_to(path)?,
..self
})
}
}
impl From<Requirement> for pep508_rs::Requirement<VerbatimUrl> {
@ -175,28 +184,24 @@ impl From<Requirement> for pep508_rs::Requirement<VerbatimParsedUrl> {
}
RequirementSource::Path {
install_path,
lock_path,
ext,
url,
} => Some(VersionOrUrl::Url(VerbatimParsedUrl {
parsed_url: ParsedUrl::Path(ParsedPathUrl {
url: url.to_url(),
install_path,
lock_path,
ext,
}),
verbatim: url,
})),
RequirementSource::Directory {
install_path,
lock_path,
editable,
url,
} => Some(VersionOrUrl::Url(VerbatimParsedUrl {
parsed_url: ParsedUrl::Directory(ParsedDirectoryUrl {
url: url.to_url(),
install_path,
lock_path,
editable,
}),
verbatim: url,
@ -342,10 +347,6 @@ pub enum RequirementSource {
Path {
/// The absolute path to the distribution which we use for installing.
install_path: PathBuf,
/// The absolute path or path relative to the workspace root pointing to the distribution
/// which we use for locking. Unlike `given` on the verbatim URL all environment variables
/// are resolved, and unlike the install path, we did not yet join it on the base directory.
lock_path: PathBuf,
/// The file extension, e.g. `tar.gz`, `zip`, etc.
ext: DistExtension,
/// The PEP 508 style URL in the format
@ -357,10 +358,6 @@ pub enum RequirementSource {
Directory {
/// The absolute path to the distribution which we use for installing.
install_path: PathBuf,
/// The absolute path or path relative to the workspace root pointing to the distribution
/// which we use for locking. Unlike `given` on the verbatim URL all environment variables
/// are resolved, and unlike the install path, we did not yet join it on the base directory.
lock_path: PathBuf,
/// For a source tree (a directory), whether to install as an editable.
editable: bool,
/// The PEP 508 style URL in the format
@ -376,13 +373,11 @@ impl RequirementSource {
match parsed_url {
ParsedUrl::Path(local_file) => RequirementSource::Path {
install_path: local_file.install_path.clone(),
lock_path: local_file.lock_path.clone(),
ext: local_file.ext,
url,
},
ParsedUrl::Directory(directory) => RequirementSource::Directory {
install_path: directory.install_path.clone(),
lock_path: directory.lock_path.clone(),
editable: directory.editable,
url,
},
@ -427,13 +422,11 @@ impl RequirementSource {
}),
Self::Path {
install_path,
lock_path,
ext,
url,
} => Some(VerbatimParsedUrl {
parsed_url: ParsedUrl::Path(ParsedPathUrl::from_source(
install_path.clone(),
lock_path.clone(),
*ext,
url.to_url(),
)),
@ -441,13 +434,11 @@ impl RequirementSource {
}),
Self::Directory {
install_path,
lock_path,
editable,
url,
} => Some(VerbatimParsedUrl {
parsed_url: ParsedUrl::Directory(ParsedDirectoryUrl::from_source(
install_path.clone(),
lock_path.clone(),
*editable,
url.to_url(),
)),
@ -504,6 +495,33 @@ impl RequirementSource {
| RequirementSource::Directory { .. } => None,
}
}
/// Convert the source to a [`RequirementSource`] relative to the given path.
pub fn relative_to(self, path: &Path) -> Result<Self, io::Error> {
match self {
RequirementSource::Registry { .. }
| RequirementSource::Url { .. }
| RequirementSource::Git { .. } => Ok(self),
RequirementSource::Path {
install_path,
ext,
url,
} => Ok(Self::Path {
install_path: relative_to(&install_path, path)?,
ext,
url,
}),
RequirementSource::Directory {
install_path,
editable,
url,
} => Ok(Self::Directory {
install_path: relative_to(&install_path, path)?,
editable,
url,
}),
}
}
}
impl Display for RequirementSource {
@ -639,26 +657,24 @@ impl From<RequirementSource> for RequirementSourceWire {
}
}
RequirementSource::Path {
install_path: _,
lock_path,
install_path,
ext: _,
url: _,
} => Self::Path {
path: PortablePathBuf::from(lock_path),
path: PortablePathBuf::from(install_path),
},
RequirementSource::Directory {
install_path: _,
lock_path,
install_path,
editable,
url: _,
} => {
if editable {
Self::Editable {
editable: PortablePathBuf::from(lock_path),
editable: PortablePathBuf::from(install_path),
}
} else {
Self::Directory {
directory: PortablePathBuf::from(lock_path),
directory: PortablePathBuf::from(install_path),
}
}
}
@ -732,8 +748,7 @@ impl TryFrom<RequirementSourceWire> for RequirementSource {
Ok(Self::Path {
ext: DistExtension::from_path(path.as_path())
.map_err(|err| ParsedUrlError::MissingExtensionPath(path.clone(), err))?,
install_path: path.clone(),
lock_path: path,
install_path: path,
url,
})
}
@ -741,8 +756,7 @@ impl TryFrom<RequirementSourceWire> for RequirementSource {
let directory = PathBuf::from(directory);
let url = VerbatimUrl::parse_path(&directory, &*CWD)?;
Ok(Self::Directory {
install_path: directory.clone(),
lock_path: directory,
install_path: directory,
editable: false,
url,
})
@ -751,8 +765,7 @@ impl TryFrom<RequirementSourceWire> for RequirementSource {
let editable = PathBuf::from(editable);
let url = VerbatimUrl::parse_path(&editable, &*CWD)?;
Ok(Self::Directory {
install_path: editable.clone(),
lock_path: editable,
install_path: editable,
editable: true,
url,
})
@ -809,7 +822,6 @@ mod tests {
marker: MarkerTree::TRUE,
source: RequirementSource::Directory {
install_path: PathBuf::from(path),
lock_path: PathBuf::from(path),
editable: false,
url: VerbatimUrl::from_path(Path::new(path)).unwrap(),
},

View file

@ -1845,7 +1845,6 @@ mod test {
fragment: None,
},
install_path: "/foo/bar",
lock_path: "/foo/bar",
editable: true,
},
),

View file

@ -22,7 +22,6 @@ RequirementsTxt {
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/scripts/packages/black_editable",
lock_path: "./scripts/packages/black_editable",
editable: false,
},
),
@ -72,7 +71,6 @@ RequirementsTxt {
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/scripts/packages/black_editable",
lock_path: "./scripts/packages/black_editable",
editable: false,
},
),
@ -126,7 +124,6 @@ RequirementsTxt {
fragment: None,
},
install_path: "/scripts/packages/black_editable",
lock_path: "/scripts/packages/black_editable",
editable: false,
},
),

View file

@ -24,7 +24,6 @@ RequirementsTxt {
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/editable",
lock_path: "./editable",
editable: true,
},
),
@ -81,7 +80,6 @@ RequirementsTxt {
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/editable",
lock_path: "./editable",
editable: true,
},
),
@ -138,7 +136,6 @@ RequirementsTxt {
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/editable",
lock_path: "./editable",
editable: true,
},
),
@ -195,7 +192,6 @@ RequirementsTxt {
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/editable",
lock_path: "./editable",
editable: true,
},
),
@ -252,7 +248,6 @@ RequirementsTxt {
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/editable",
lock_path: "./editable",
editable: true,
},
),
@ -302,7 +297,6 @@ RequirementsTxt {
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/editable[d",
lock_path: "./editable[d",
editable: true,
},
),

View file

@ -22,7 +22,6 @@ RequirementsTxt {
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/scripts/packages/black_editable",
lock_path: "./scripts/packages/black_editable",
editable: false,
},
),
@ -72,7 +71,6 @@ RequirementsTxt {
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/scripts/packages/black_editable",
lock_path: "./scripts/packages/black_editable",
editable: false,
},
),
@ -126,7 +124,6 @@ RequirementsTxt {
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/scripts/packages/black_editable",
lock_path: "scripts/packages/black_editable",
editable: false,
},
),

View file

@ -24,7 +24,6 @@ RequirementsTxt {
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/editable",
lock_path: "./editable",
editable: true,
},
),
@ -81,7 +80,6 @@ RequirementsTxt {
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/editable",
lock_path: "./editable",
editable: true,
},
),
@ -138,7 +136,6 @@ RequirementsTxt {
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/editable",
lock_path: "./editable",
editable: true,
},
),
@ -195,7 +192,6 @@ RequirementsTxt {
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/editable",
lock_path: "./editable",
editable: true,
},
),
@ -252,7 +248,6 @@ RequirementsTxt {
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/editable",
lock_path: "./editable",
editable: true,
},
),
@ -302,7 +297,6 @@ RequirementsTxt {
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/editable[d",
lock_path: "./editable[d",
editable: true,
},
),

View file

@ -35,7 +35,6 @@ anyhow = { workspace = true }
fs-err = { workspace = true }
futures = { workspace = true }
nanoid = { workspace = true }
path-absolutize = { workspace = true }
reqwest = { workspace = true }
reqwest-middleware = { workspace = true }
rmp-serde = { workspace = true }

View file

@ -2,14 +2,14 @@ use std::collections::BTreeMap;
use std::io;
use std::path::{Path, PathBuf};
use thiserror::Error;
use url::Url;
use distribution_filename::DistExtension;
use path_absolutize::Absolutize;
use pep440_rs::VersionSpecifiers;
use pep508_rs::{VerbatimUrl, VersionOrUrl};
use pypi_types::{ParsedUrlError, Requirement, RequirementSource, VerbatimParsedUrl};
use thiserror::Error;
use url::Url;
use uv_fs::{relative_to, Simplified};
use uv_fs::Simplified;
use uv_git::GitReference;
use uv_normalize::PackageName;
use uv_warnings::warn_user_once;
@ -142,14 +142,15 @@ impl LoweredRequirement {
// relative to workspace: `packages/current_project`
// workspace lock root: `../current_workspace`
// relative to main workspace: `../current_workspace/packages/current_project`
let relative_to_workspace = relative_to(member.root(), workspace.install_path())
.map_err(LoweringError::RelativeTo)?;
let relative_to_main_workspace = workspace.lock_path().join(relative_to_workspace);
let url = VerbatimUrl::parse_absolute_path(member.root())?
.with_given(relative_to_main_workspace.to_string_lossy());
let url = VerbatimUrl::parse_absolute_path(member.root())?;
let install_path = url.to_file_path().map_err(|()| {
LoweringError::RelativeTo(io::Error::new(
io::ErrorKind::Other,
"Invalid path in file URL",
))
})?;
RequirementSource::Directory {
install_path: member.root().clone(),
lock_path: relative_to_main_workspace,
install_path,
url,
editable: true,
}
@ -360,28 +361,20 @@ fn path_source(
Origin::Workspace => workspace_root,
};
let url = VerbatimUrl::parse_path(path, base)?.with_given(path.to_string_lossy());
let absolute_path = path
.to_path_buf()
.absolutize_from(base)
.map_err(|err| LoweringError::Absolutize(path.to_path_buf(), err))?
.to_path_buf();
let relative_to_workspace = if path.is_relative() {
// Relative paths in a project are relative to the project root, but the lockfile is
// relative to the workspace root.
relative_to(&absolute_path, workspace_root).map_err(LoweringError::RelativeTo)?
} else {
// If the user gave us an absolute path, we respect that.
path.to_path_buf()
};
let is_dir = if let Ok(metadata) = absolute_path.metadata() {
let install_path = url.to_file_path().map_err(|()| {
LoweringError::RelativeTo(io::Error::new(
io::ErrorKind::Other,
"Invalid path in file URL",
))
})?;
let is_dir = if let Ok(metadata) = install_path.metadata() {
metadata.is_dir()
} else {
absolute_path.extension().is_none()
install_path.extension().is_none()
};
if is_dir {
Ok(RequirementSource::Directory {
install_path: absolute_path,
lock_path: relative_to_workspace,
install_path,
url,
editable,
})
@ -390,10 +383,9 @@ fn path_source(
return Err(LoweringError::EditableFile(url.to_string()));
}
Ok(RequirementSource::Path {
install_path: absolute_path,
lock_path: relative_to_workspace,
ext: DistExtension::from_path(path)
ext: DistExtension::from_path(&install_path)
.map_err(|err| ParsedUrlError::MissingExtensionPath(path.to_path_buf(), err))?,
install_path,
url,
})
}

View file

@ -59,7 +59,6 @@ impl Metadata {
pub async fn from_workspace(
metadata: Metadata23,
install_path: &Path,
lock_path: &Path,
sources: SourceStrategy,
) -> Result<Self, MetadataError> {
// Lower the requirements.
@ -75,7 +74,6 @@ impl Metadata {
provides_extras: metadata.provides_extras,
},
install_path,
lock_path,
sources,
)
.await?;

View file

@ -37,7 +37,6 @@ impl RequiresDist {
pub async fn from_project_maybe_workspace(
metadata: pypi_types::RequiresDist,
install_path: &Path,
lock_path: &Path,
sources: SourceStrategy,
) -> Result<Self, MetadataError> {
match sources {
@ -46,7 +45,6 @@ impl RequiresDist {
// TODO(konsti): Cache workspace discovery.
let Some(project_workspace) = ProjectWorkspace::from_maybe_project_root(
install_path,
lock_path,
&DiscoveryOptions::default(),
)
.await?
@ -160,7 +158,6 @@ mod test {
let pyproject_toml = PyProjectToml::from_string(contents.to_string())?;
let path = Path::new("pyproject.toml");
let project_workspace = ProjectWorkspace::from_project(
path,
path,
pyproject_toml
.project

View file

@ -425,7 +425,6 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
let requires_dist = RequiresDist::from_project_maybe_workspace(
requires_dist,
project_root,
project_root,
self.build_context.sources(),
)
.await?;
@ -1009,7 +1008,6 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
Metadata::from_workspace(
metadata,
resource.install_path.as_ref(),
resource.lock_path.as_ref(),
self.build_context.sources(),
)
.await?,
@ -1024,7 +1022,6 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
Metadata::from_workspace(
metadata,
resource.install_path.as_ref(),
resource.lock_path.as_ref(),
self.build_context.sources(),
)
.await?,
@ -1049,7 +1046,6 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
Metadata::from_workspace(
metadata,
resource.install_path.as_ref(),
resource.lock_path.as_ref(),
self.build_context.sources(),
)
.await?,
@ -1081,7 +1077,6 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
Metadata::from_workspace(
metadata,
resource.install_path.as_ref(),
resource.lock_path.as_ref(),
self.build_context.sources(),
)
.await?,
@ -1252,8 +1247,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
Self::read_static_metadata(source, fetch.path(), resource.subdirectory).await?
{
return Ok(ArchiveMetadata::from(
Metadata::from_workspace(metadata, &path, &path, self.build_context.sources())
.await?,
Metadata::from_workspace(metadata, &path, self.build_context.sources()).await?,
));
}
@ -1276,8 +1270,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
debug!("Using cached metadata for: {source}");
return Ok(ArchiveMetadata::from(
Metadata::from_workspace(metadata, &path, &path, self.build_context.sources())
.await?,
Metadata::from_workspace(metadata, &path, self.build_context.sources()).await?,
));
}
}
@ -1297,8 +1290,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
.map_err(Error::CacheWrite)?;
return Ok(ArchiveMetadata::from(
Metadata::from_workspace(metadata, &path, &path, self.build_context.sources())
.await?,
Metadata::from_workspace(metadata, &path, self.build_context.sources()).await?,
));
}
@ -1324,13 +1316,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
.map_err(Error::CacheWrite)?;
Ok(ArchiveMetadata::from(
Metadata::from_workspace(
metadata,
fetch.path(),
fetch.path(),
self.build_context.sources(),
)
.await?,
Metadata::from_workspace(metadata, fetch.path(), self.build_context.sources()).await?,
))
}

View file

@ -266,7 +266,6 @@ impl<'a> Planner<'a> {
url,
editable,
install_path,
lock_path,
} => {
// Convert to an absolute path.
let install_path = std::path::absolute(install_path)?;
@ -283,7 +282,6 @@ impl<'a> Planner<'a> {
name: requirement.name.clone(),
url: url.clone(),
install_path,
lock_path: lock_path.clone(),
editable: *editable,
};
@ -305,7 +303,6 @@ impl<'a> Planner<'a> {
ext,
url,
install_path,
lock_path,
} => {
// Convert to an absolute path.
let install_path = std::path::absolute(install_path)?;
@ -335,21 +332,20 @@ impl<'a> Planner<'a> {
filename,
url: url.clone(),
install_path,
lock_path: lock_path.clone(),
};
if !wheel.filename.is_compatible(tags) {
bail!(
"A path dependency is incompatible with the current platform: {}",
wheel.lock_path.user_display()
);
"A path dependency is incompatible with the current platform: {}",
wheel.install_path.user_display()
);
}
if no_binary {
bail!(
"A path dependency points to a wheel which conflicts with `--no-binary`: {}",
wheel.url
);
"A path dependency points to a wheel which conflicts with `--no-binary`: {}",
wheel.url
);
}
// Find the exact wheel from the cache, since we know the filename in
@ -387,7 +383,6 @@ impl<'a> Planner<'a> {
name: requirement.name.clone(),
url: url.clone(),
install_path,
lock_path: lock_path.clone(),
ext: *ext,
};

View file

@ -150,7 +150,6 @@ impl RequirementSatisfaction {
}
RequirementSource::Path {
install_path: requested_path,
lock_path: _,
ext: _,
url: _,
} => {
@ -197,7 +196,6 @@ impl RequirementSatisfaction {
}
RequirementSource::Directory {
install_path: requested_path,
lock_path: _,
editable: requested_editable,
url: _,
} => {

View file

@ -283,26 +283,17 @@ fn required_dist(requirement: &Requirement) -> Result<Option<Dist>, distribution
}
RequirementSource::Path {
install_path,
lock_path,
ext,
url,
} => Dist::from_file_url(
requirement.name.clone(),
url.clone(),
install_path,
lock_path,
*ext,
)?,
} => Dist::from_file_url(requirement.name.clone(), url.clone(), install_path, *ext)?,
RequirementSource::Directory {
install_path,
lock_path,
url,
editable,
} => Dist::from_directory_url(
requirement.name.clone(),
url.clone(),
install_path,
lock_path,
*editable,
)?,
}))

View file

@ -171,7 +171,6 @@ impl<'a, Context: BuildContext> SourceTreeResolver<'a, Context> {
let source = SourceUrl::Directory(DirectorySourceUrl {
url: &url,
install_path: Cow::Borrowed(source_tree),
lock_path: Cow::Borrowed(source_tree),
editable: false,
});

View file

@ -256,7 +256,6 @@ impl<'a, Context: BuildContext> NamedRequirementsResolver<'a, Context> {
SourceUrl::Directory(DirectorySourceUrl {
url: &requirement.url.verbatim,
install_path: Cow::Borrowed(&parsed_directory_url.install_path),
lock_path: Cow::Borrowed(&parsed_directory_url.lock_path),
editable: parsed_directory_url.editable,
})
}

View file

@ -2,6 +2,7 @@ use std::borrow::Cow;
use std::collections::{BTreeMap, BTreeSet, VecDeque};
use std::convert::Infallible;
use std::fmt::{Debug, Display};
use std::io;
use std::path::{Path, PathBuf};
use std::str::FromStr;
@ -30,7 +31,7 @@ use pypi_types::{
};
use uv_configuration::ExtrasSpecification;
use uv_distribution::DistributionDatabase;
use uv_fs::{relative_to, PortablePath, PortablePathBuf, Simplified};
use uv_fs::{relative_to, PortablePath, PortablePathBuf};
use uv_git::{GitReference, GitSha, RepositoryReference, ResolvedRepositoryReference};
use uv_normalize::{ExtraName, GroupName, PackageName};
use uv_types::BuildContext;
@ -75,7 +76,7 @@ pub struct Lock {
impl Lock {
/// Initialize a [`Lock`] from a [`ResolutionGraph`].
pub fn from_resolution_graph(graph: &ResolutionGraph) -> Result<Self, LockError> {
pub fn from_resolution_graph(graph: &ResolutionGraph, root: &Path) -> Result<Self, LockError> {
let mut locked_dists = BTreeMap::new();
// Lock all base packages.
@ -88,7 +89,7 @@ impl Lock {
.fork_markers(dist.name(), &dist.version, dist.dist.version_or_url().url())
.cloned()
.unwrap_or_default();
let mut locked_dist = Package::from_annotated_dist(dist, fork_markers)?;
let mut locked_dist = Package::from_annotated_dist(dist, fork_markers, root)?;
// Add all dependencies
for edge in graph.petgraph.edges(node_index) {
@ -97,7 +98,7 @@ impl Lock {
continue;
};
let marker = edge.weight().clone();
locked_dist.add_dependency(dependency_dist, marker);
locked_dist.add_dependency(dependency_dist, marker, root)?;
}
let id = locked_dist.id.clone();
if let Some(locked_dist) = locked_dists.insert(id, locked_dist) {
@ -115,7 +116,7 @@ impl Lock {
continue;
};
if let Some(extra) = dist.extra.as_ref() {
let id = PackageId::from_annotated_dist(dist);
let id = PackageId::from_annotated_dist(dist, root)?;
let Some(locked_dist) = locked_dists.get_mut(&id) else {
return Err(LockErrorKind::MissingExtraBase {
id,
@ -129,11 +130,16 @@ impl Lock {
continue;
};
let marker = edge.weight().clone();
locked_dist.add_optional_dependency(extra.clone(), dependency_dist, marker);
locked_dist.add_optional_dependency(
extra.clone(),
dependency_dist,
marker,
root,
)?;
}
}
if let Some(group) = dist.dev.as_ref() {
let id = PackageId::from_annotated_dist(dist);
let id = PackageId::from_annotated_dist(dist, root)?;
let Some(locked_dist) = locked_dists.get_mut(&id) else {
return Err(LockErrorKind::MissingDevBase {
id,
@ -147,7 +153,7 @@ impl Lock {
continue;
};
let marker = edge.weight().clone();
locked_dist.add_dev_dependency(group.clone(), dependency_dist, marker);
locked_dist.add_dev_dependency(group.clone(), dependency_dist, marker, root)?;
}
}
}
@ -981,6 +987,8 @@ pub struct ResolverManifest {
}
impl ResolverManifest {
/// Initialize a [`ResolverManifest`] with the given members, requirements, constraints, and
/// overrides.
pub fn new(
members: impl IntoIterator<Item = PackageName>,
requirements: impl IntoIterator<Item = Requirement>,
@ -994,6 +1002,28 @@ impl ResolverManifest {
overrides: overrides.into_iter().collect(),
}
}
/// Convert the manifest to a relative form using the given workspace.
pub fn relative_to(self, workspace: &Workspace) -> Result<Self, io::Error> {
Ok(Self {
members: self.members,
requirements: self
.requirements
.into_iter()
.map(|requirement| requirement.relative_to(workspace.install_path()))
.collect::<Result<BTreeSet<_>, _>>()?,
constraints: self
.constraints
.into_iter()
.map(|requirement| requirement.relative_to(workspace.install_path()))
.collect::<Result<BTreeSet<_>, _>>()?,
overrides: self
.overrides
.into_iter()
.map(|requirement| requirement.relative_to(workspace.install_path()))
.collect::<Result<BTreeSet<_>, _>>()?,
})
}
}
#[derive(Clone, Debug, serde::Deserialize)]
@ -1094,8 +1124,9 @@ impl Package {
fn from_annotated_dist(
annotated_dist: &AnnotatedDist,
fork_markers: Vec<MarkerTree>,
root: &Path,
) -> Result<Self, LockError> {
let id = PackageId::from_annotated_dist(annotated_dist);
let id = PackageId::from_annotated_dist(annotated_dist, root)?;
let sdist = SourceDist::from_annotated_dist(&id, annotated_dist)?;
let wheels = Wheel::from_annotated_dist(annotated_dist)?;
let requires_dist = if id.source.is_immutable() {
@ -1106,7 +1137,9 @@ impl Package {
.requires_dist
.iter()
.cloned()
.collect()
.map(|requirement| requirement.relative_to(root))
.collect::<Result<_, _>>()
.map_err(LockErrorKind::RequirementRelativePath)?
};
let requires_dev = if id.source.is_immutable() {
BTreeMap::default()
@ -1115,8 +1148,16 @@ impl Package {
.metadata
.dev_dependencies
.iter()
.map(|(k, v)| (k.clone(), v.iter().cloned().collect()))
.collect()
.map(|(group, requirements)| {
let requirements = requirements
.iter()
.cloned()
.map(|requirement| requirement.relative_to(root))
.collect::<Result<_, _>>()
.map_err(LockErrorKind::RequirementRelativePath)?;
Ok::<_, LockError>((group.clone(), requirements))
})
.collect::<Result<_, _>>()?
};
Ok(Package {
id,
@ -1134,17 +1175,24 @@ impl Package {
}
/// Add the [`AnnotatedDist`] as a dependency of the [`Package`].
fn add_dependency(&mut self, annotated_dist: &AnnotatedDist, marker: MarkerTree) {
let new_dep = Dependency::from_annotated_dist(annotated_dist, marker);
fn add_dependency(
&mut self,
annotated_dist: &AnnotatedDist,
marker: MarkerTree,
root: &Path,
) -> Result<(), LockError> {
let new_dep = Dependency::from_annotated_dist(annotated_dist, marker, root)?;
for existing_dep in &mut self.dependencies {
if existing_dep.package_id == new_dep.package_id
&& existing_dep.marker == new_dep.marker
{
existing_dep.extra.extend(new_dep.extra);
return;
return Ok(());
}
}
self.dependencies.push(new_dep);
Ok(())
}
/// Add the [`AnnotatedDist`] as an optional dependency of the [`Package`].
@ -1153,16 +1201,19 @@ impl Package {
extra: ExtraName,
annotated_dist: &AnnotatedDist,
marker: MarkerTree,
) {
let dep = Dependency::from_annotated_dist(annotated_dist, marker);
root: &Path,
) -> Result<(), LockError> {
let dep = Dependency::from_annotated_dist(annotated_dist, marker, root)?;
let optional_deps = self.optional_dependencies.entry(extra).or_default();
for existing_dep in &mut *optional_deps {
if existing_dep.package_id == dep.package_id && existing_dep.marker == dep.marker {
existing_dep.extra.extend(dep.extra);
return;
return Ok(());
}
}
optional_deps.push(dep);
Ok(())
}
/// Add the [`AnnotatedDist`] as a development dependency of the [`Package`].
@ -1171,16 +1222,19 @@ impl Package {
dev: GroupName,
annotated_dist: &AnnotatedDist,
marker: MarkerTree,
) {
let dep = Dependency::from_annotated_dist(annotated_dist, marker);
root: &Path,
) -> Result<(), LockError> {
let dep = Dependency::from_annotated_dist(annotated_dist, marker, root)?;
let dev_deps = self.dev_dependencies.entry(dev).or_default();
for existing_dep in &mut *dev_deps {
if existing_dep.package_id == dep.package_id && existing_dep.marker == dep.marker {
existing_dep.extra.extend(dep.extra);
return;
return Ok(());
}
}
dev_deps.push(dep);
Ok(())
}
/// Convert the [`Package`] to a [`Dist`] that can be used in installation.
@ -1206,7 +1260,6 @@ impl Package {
filename,
url: verbatim_url(workspace_root.join(path), &self.id)?,
install_path: workspace_root.join(path),
lock_path: path.clone(),
};
let built_dist = BuiltDist::Path(path_dist);
Ok(Dist::Built(built_dist))
@ -1268,7 +1321,6 @@ impl Package {
name: self.id.name.clone(),
url: verbatim_url(workspace_root.join(path), &self.id)?,
install_path: workspace_root.join(path),
lock_path: path.clone(),
ext: SourceDistExtension::from_path(path)?,
};
distribution_types::SourceDist::Path(path_dist)
@ -1278,7 +1330,6 @@ impl Package {
name: self.id.name.clone(),
url: verbatim_url(workspace_root.join(path), &self.id)?,
install_path: workspace_root.join(path),
lock_path: path.clone(),
editable: false,
};
distribution_types::SourceDist::Directory(dir_dist)
@ -1288,7 +1339,6 @@ impl Package {
name: self.id.name.clone(),
url: verbatim_url(workspace_root.join(path), &self.id)?,
install_path: workspace_root.join(path),
lock_path: path.clone(),
editable: true,
};
distribution_types::SourceDist::Directory(dir_dist)
@ -1691,15 +1741,18 @@ pub(crate) struct PackageId {
}
impl PackageId {
fn from_annotated_dist(annotated_dist: &AnnotatedDist) -> PackageId {
fn from_annotated_dist(
annotated_dist: &AnnotatedDist,
root: &Path,
) -> Result<PackageId, LockError> {
let name = annotated_dist.metadata.name.clone();
let version = annotated_dist.metadata.version.clone();
let source = Source::from_resolved_dist(&annotated_dist.dist);
PackageId {
let source = Source::from_resolved_dist(&annotated_dist.dist, root)?;
Ok(Self {
name,
version,
source,
}
})
}
/// Writes this package ID inline into the table given.
@ -1792,43 +1845,50 @@ enum Source {
}
impl Source {
fn from_resolved_dist(resolved_dist: &ResolvedDist) -> Source {
fn from_resolved_dist(resolved_dist: &ResolvedDist, root: &Path) -> Result<Source, LockError> {
match *resolved_dist {
// We pass empty installed packages for locking.
ResolvedDist::Installed(_) => unreachable!(),
ResolvedDist::Installable(ref dist) => Source::from_dist(dist),
ResolvedDist::Installable(ref dist) => Source::from_dist(dist, root),
}
}
fn from_dist(dist: &Dist) -> Source {
fn from_dist(dist: &Dist, root: &Path) -> Result<Source, LockError> {
match *dist {
Dist::Built(ref built_dist) => Source::from_built_dist(built_dist),
Dist::Source(ref source_dist) => Source::from_source_dist(source_dist),
Dist::Built(ref built_dist) => Source::from_built_dist(built_dist, root),
Dist::Source(ref source_dist) => Source::from_source_dist(source_dist, root),
}
}
fn from_built_dist(built_dist: &BuiltDist) -> Source {
fn from_built_dist(built_dist: &BuiltDist, root: &Path) -> Result<Source, LockError> {
match *built_dist {
BuiltDist::Registry(ref reg_dist) => Source::from_registry_built_dist(reg_dist),
BuiltDist::DirectUrl(ref direct_dist) => Source::from_direct_built_dist(direct_dist),
BuiltDist::Path(ref path_dist) => Source::from_path_built_dist(path_dist),
BuiltDist::Registry(ref reg_dist) => Ok(Source::from_registry_built_dist(reg_dist)),
BuiltDist::DirectUrl(ref direct_dist) => {
Ok(Source::from_direct_built_dist(direct_dist))
}
BuiltDist::Path(ref path_dist) => Source::from_path_built_dist(path_dist, root),
}
}
fn from_source_dist(source_dist: &distribution_types::SourceDist) -> Source {
fn from_source_dist(
source_dist: &distribution_types::SourceDist,
root: &Path,
) -> Result<Source, LockError> {
match *source_dist {
distribution_types::SourceDist::Registry(ref reg_dist) => {
Source::from_registry_source_dist(reg_dist)
Ok(Source::from_registry_source_dist(reg_dist))
}
distribution_types::SourceDist::DirectUrl(ref direct_dist) => {
Source::from_direct_source_dist(direct_dist)
Ok(Source::from_direct_source_dist(direct_dist))
}
distribution_types::SourceDist::Git(ref git_dist) => {
Ok(Source::from_git_dist(git_dist))
}
distribution_types::SourceDist::Git(ref git_dist) => Source::from_git_dist(git_dist),
distribution_types::SourceDist::Path(ref path_dist) => {
Source::from_path_source_dist(path_dist)
Source::from_path_source_dist(path_dist, root)
}
distribution_types::SourceDist::Directory(ref directory) => {
Source::from_directory_source_dist(directory)
Source::from_directory_source_dist(directory, root)
}
}
}
@ -1861,22 +1921,29 @@ impl Source {
)
}
fn from_path_built_dist(path_dist: &PathBuiltDist) -> Source {
let path = path_dist.lock_path.simplified().to_path_buf();
Source::Path(path)
fn from_path_built_dist(path_dist: &PathBuiltDist, root: &Path) -> Result<Source, LockError> {
let path = relative_to(&path_dist.install_path, root)
.map_err(LockErrorKind::DistributionRelativePath)?;
Ok(Source::Path(path))
}
fn from_path_source_dist(path_dist: &PathSourceDist) -> Source {
let path = path_dist.install_path.simplified().to_path_buf();
Source::Path(path)
fn from_path_source_dist(path_dist: &PathSourceDist, root: &Path) -> Result<Source, LockError> {
let path = relative_to(&path_dist.install_path, root)
.map_err(LockErrorKind::DistributionRelativePath)?;
Ok(Source::Path(path))
}
fn from_directory_source_dist(directory_dist: &DirectorySourceDist) -> Source {
let path = directory_dist.lock_path.simplified().to_path_buf();
fn from_directory_source_dist(
directory_dist: &DirectorySourceDist,
root: &Path,
) -> Result<Source, LockError> {
let path = relative_to(&directory_dist.install_path, root)
.map_err(LockErrorKind::DistributionRelativePath)?;
if directory_dist.editable {
Source::Editable(path)
Ok(Source::Editable(path))
} else {
Source::Directory(path)
Ok(Source::Directory(path))
}
}
@ -2649,14 +2716,18 @@ struct Dependency {
}
impl Dependency {
fn from_annotated_dist(annotated_dist: &AnnotatedDist, marker: MarkerTree) -> Dependency {
let package_id = PackageId::from_annotated_dist(annotated_dist);
fn from_annotated_dist(
annotated_dist: &AnnotatedDist,
marker: MarkerTree,
root: &Path,
) -> Result<Dependency, LockError> {
let package_id = PackageId::from_annotated_dist(annotated_dist, root)?;
let extra = annotated_dist.extra.iter().cloned().collect();
Dependency {
Ok(Self {
package_id,
extra,
marker,
}
})
}
/// Returns the TOML representation of this dependency.
@ -2838,19 +2909,11 @@ fn normalize_requirement(
})
}
RequirementSource::Path {
install_path: _,
lock_path,
install_path,
ext,
url: _,
} => {
// When a path requirement comes from the lockfile, `install_path` and `lock_path` are
// both relative to the lockfile.
//
// When a path requirement is deserialized from package metadata, `install_path` is
// absolute, and `lock_path` is relative to the lockfile.
let install_path = uv_fs::normalize_path(&workspace.install_path().join(&lock_path));
let lock_path = relative_to(workspace.install_path(), &lock_path)
.map_err(LockErrorKind::RequirementRelativePath)?;
let install_path = uv_fs::normalize_path(&workspace.install_path().join(&install_path));
let url = VerbatimUrl::from_path(&install_path)
.map_err(LockErrorKind::RequirementVerbatimUrl)?;
@ -2860,7 +2923,6 @@ fn normalize_requirement(
marker: requirement.marker,
source: RequirementSource::Path {
install_path,
lock_path,
ext,
url,
},
@ -2868,14 +2930,11 @@ fn normalize_requirement(
})
}
RequirementSource::Directory {
install_path: _,
lock_path,
install_path,
editable,
url: _,
} => {
let install_path = uv_fs::normalize_path(&workspace.install_path().join(&lock_path));
let lock_path = relative_to(workspace.install_path(), &lock_path)
.map_err(LockErrorKind::RequirementRelativePath)?;
let install_path = uv_fs::normalize_path(&workspace.install_path().join(&install_path));
let url = VerbatimUrl::from_path(&install_path)
.map_err(LockErrorKind::RequirementVerbatimUrl)?;
@ -2885,7 +2944,6 @@ fn normalize_requirement(
marker: requirement.marker,
source: RequirementSource::Directory {
install_path,
lock_path,
editable,
url,
},
@ -3072,6 +3130,13 @@ enum LockErrorKind {
#[source]
err: VerbatimUrlError,
},
/// An error that occurs when parsing an existing requirement.
#[error("could not compute relative path between workspace and distribution")]
DistributionRelativePath(
/// The inner error we forward.
#[source]
std::io::Error,
),
/// An error that occurs when an ambiguous `package.dependency` is
/// missing a `version` field.
#[error(

View file

@ -135,11 +135,9 @@ impl PubGrubRequirement {
ext,
url,
install_path,
lock_path,
} => {
let parsed_url = ParsedUrl::Path(ParsedPathUrl::from_source(
install_path.clone(),
lock_path.clone(),
*ext,
url.to_url(),
));
@ -149,11 +147,9 @@ impl PubGrubRequirement {
editable,
url,
install_path,
lock_path,
} => {
let parsed_url = ParsedUrl::Directory(ParsedDirectoryUrl::from_source(
install_path.clone(),
lock_path.clone(),
*editable,
url.to_url(),
));

View file

@ -332,13 +332,13 @@ impl Source {
let source = match source {
RequirementSource::Registry { .. } => return Ok(None),
RequirementSource::Path { lock_path, .. } => Source::Path {
RequirementSource::Path { install_path, .. } => Source::Path {
editable,
path: lock_path.to_string_lossy().into_owned(),
path: install_path.to_string_lossy().into_owned(),
},
RequirementSource::Directory { lock_path, .. } => Source::Path {
RequirementSource::Directory { install_path, .. } => Source::Path {
editable,
path: lock_path.to_string_lossy().into_owned(),
path: install_path.to_string_lossy().into_owned(),
},
RequirementSource::Url {
subdirectory, url, ..

View file

@ -10,7 +10,7 @@ use tracing::{debug, trace, warn};
use pep508_rs::{MarkerTree, RequirementOrigin, VerbatimUrl};
use pypi_types::{Requirement, RequirementSource};
use uv_fs::{absolutize_path, normalize_path, relative_to, Simplified};
use uv_fs::{absolutize_path, Simplified};
use uv_normalize::{GroupName, PackageName, DEV_DEPENDENCIES};
use uv_warnings::warn_user;
@ -62,11 +62,6 @@ pub struct Workspace {
/// The workspace root is the directory containing the top level `pyproject.toml` with
/// the `uv.tool.workspace`, or the `pyproject.toml` in an implicit single workspace project.
install_path: PathBuf,
/// The same path as `install_path`, but relative to the main workspace.
///
/// We use this value to compute relative paths for workspace-to-workspace dependencies. It's an
/// empty path for the main workspace.
lock_path: PathBuf,
/// The members of the workspace.
packages: BTreeMap<PackageName, WorkspaceMember>,
/// The sources table from the workspace `pyproject.toml`.
@ -176,8 +171,6 @@ impl Workspace {
Self::collect_members(
workspace_root.clone(),
// This method supports only absolute paths.
workspace_root,
workspace_definition,
workspace_pyproject_toml,
current_project,
@ -286,11 +279,6 @@ impl Workspace {
marker: MarkerTree::TRUE,
source: RequirementSource::Directory {
install_path: member.root.clone(),
lock_path: member
.root
.strip_prefix(&self.install_path)
.expect("Project must be below workspace root")
.to_path_buf(),
editable: true,
url,
},
@ -421,12 +409,6 @@ impl Workspace {
&self.install_path
}
/// The same path as `install_path()`, but relative to the main workspace. We use this value
/// to compute relative paths for workspace-to-workspace dependencies.
pub fn lock_path(&self) -> &PathBuf {
&self.lock_path
}
/// The path to the workspace virtual environment.
pub fn venv(&self) -> PathBuf {
self.install_path.join(".venv")
@ -480,7 +462,6 @@ impl Workspace {
/// Collect the workspace member projects from the `members` and `excludes` entries.
async fn collect_members(
workspace_root: PathBuf,
lock_path: PathBuf,
workspace_definition: ToolUvWorkspace,
workspace_pyproject_toml: PyProjectToml,
current_project: Option<WorkspaceMember>,
@ -644,7 +625,6 @@ impl Workspace {
Ok(Workspace {
install_path: workspace_root,
lock_path,
packages: workspace_members,
sources: workspace_sources,
pyproject_toml: workspace_pyproject_toml,
@ -817,21 +797,13 @@ impl ProjectWorkspace {
.clone()
.ok_or_else(|| WorkspaceError::MissingProject(pyproject_path.clone()))?;
Self::from_project(
project_root,
Path::new(""),
&project,
&pyproject_toml,
options,
)
.await
Self::from_project(project_root, &project, &pyproject_toml, options).await
}
/// If the current directory contains a `pyproject.toml` with a `project` table, discover the
/// workspace and return it, otherwise it is a dynamic path dependency and we return `Ok(None)`.
pub async fn from_maybe_project_root(
install_path: &Path,
lock_path: &Path,
options: &DiscoveryOptions<'_>,
) -> Result<Option<Self>, WorkspaceError> {
// Read the `pyproject.toml`.
@ -849,8 +821,7 @@ impl ProjectWorkspace {
return Ok(None);
};
match Self::from_project(install_path, lock_path, &project, &pyproject_toml, options).await
{
match Self::from_project(install_path, &project, &pyproject_toml, options).await {
Ok(workspace) => Ok(Some(workspace)),
Err(WorkspaceError::NonWorkspace(_)) => Ok(None),
Err(err) => Err(err),
@ -894,7 +865,6 @@ impl ProjectWorkspace {
/// Find the workspace for a project.
pub async fn from_project(
install_path: &Path,
lock_path: &Path,
project: &Project,
project_pyproject_toml: &PyProjectToml,
options: &DiscoveryOptions<'_>,
@ -954,8 +924,6 @@ impl ProjectWorkspace {
project_name: project.name.clone(),
workspace: Workspace {
install_path: project_path.clone(),
// The workspace and the project are the same, so the relative path is, too.
lock_path: lock_path.to_path_buf(),
packages: current_project_as_members,
// There may be package sources, but we don't need to duplicate them into the
// workspace sources.
@ -970,28 +938,8 @@ impl ProjectWorkspace {
workspace_root.simplified_display()
);
// Say we have:
// ```
// root
// ├── main_workspace <- The reference point
// │ ├── pyproject.toml
// │ └── uv.lock
// └──current_workspace <- We want this relative to the main workspace
// └── packages
// └── current_package <- We have this relative to the main workspace
// └── pyproject.toml
// ```
// The lock path we need: `../current_workspace`
// workspace root: `/root/current_workspace`
// project path: `/root/current_workspace/packages/current_project`
// relative to workspace: `../..`
// lock path: `../current_workspace`
let up_to_root = relative_to(&workspace_root, &project_path)?;
let lock_path = normalize_path(&lock_path.join(up_to_root));
let workspace = Workspace::collect_members(
workspace_root,
lock_path,
workspace_definition,
workspace_pyproject_toml,
Some(current_project),
@ -1270,14 +1218,9 @@ impl VirtualProject {
if let Some(project) = pyproject_toml.project.as_ref() {
// If the `pyproject.toml` contains a `[project]` table, it's a project.
let project = ProjectWorkspace::from_project(
project_root,
Path::new(""),
project,
&pyproject_toml,
options,
)
.await?;
let project =
ProjectWorkspace::from_project(project_root, project, &pyproject_toml, options)
.await?;
Ok(Self::Project(project))
} else if let Some(workspace) = pyproject_toml
.tool
@ -1294,7 +1237,6 @@ impl VirtualProject {
let workspace = Workspace::collect_members(
project_path,
PathBuf::new(),
workspace.clone(),
pyproject_toml,
None,
@ -1452,7 +1394,6 @@ mod tests {
"project_name": "bird-feeder",
"workspace": {
"install_path": "[ROOT]/albatross-in-example/examples/bird-feeder",
"lock_path": "",
"packages": {
"bird-feeder": {
"root": "[ROOT]/albatross-in-example/examples/bird-feeder",
@ -1496,7 +1437,6 @@ mod tests {
"project_name": "bird-feeder",
"workspace": {
"install_path": "[ROOT]/albatross-project-in-excluded/excluded/bird-feeder",
"lock_path": "",
"packages": {
"bird-feeder": {
"root": "[ROOT]/albatross-project-in-excluded/excluded/bird-feeder",
@ -1539,7 +1479,6 @@ mod tests {
"project_name": "albatross",
"workspace": {
"install_path": "[ROOT]/albatross-root-workspace",
"lock_path": "",
"packages": {
"albatross": {
"root": "[ROOT]/albatross-root-workspace",
@ -1624,7 +1563,6 @@ mod tests {
"project_name": "albatross",
"workspace": {
"install_path": "[ROOT]/albatross-virtual-workspace",
"lock_path": "../..",
"packages": {
"albatross": {
"root": "[ROOT]/albatross-virtual-workspace/packages/albatross",
@ -1696,7 +1634,6 @@ mod tests {
"project_name": "albatross",
"workspace": {
"install_path": "[ROOT]/albatross-just-project",
"lock_path": "",
"packages": {
"albatross": {
"root": "[ROOT]/albatross-just-project",

View file

@ -67,7 +67,6 @@ pub(crate) async fn lock(
frozen: bool,
python: Option<String>,
settings: ResolverSettings,
python_preference: PythonPreference,
python_downloads: PythonDownloads,
connectivity: Connectivity,
@ -523,14 +522,12 @@ async fn do_lock(
// Notify the user of any resolution diagnostics.
pip::operations::diagnose_resolution(resolution.diagnostics(), printer)?;
let manifest = ResolverManifest::new(members, requirements, constraints, overrides)
.relative_to(workspace)?;
let previous = existing_lock.map(ValidatedLock::into_lock);
let lock = Lock::from_resolution_graph(&resolution)?
.with_manifest(ResolverManifest::new(
members,
requirements,
constraints,
overrides,
))
let lock = Lock::from_resolution_graph(&resolution, workspace.install_path())?
.with_manifest(manifest)
.with_supported_environments(
environments
.cloned()

View file

@ -276,14 +276,14 @@ fn root_package_splits_transitive_too() -> Result<()> {
[package.metadata]
requires-dist = [
{ name = "b1", marker = "python_full_version < '3.12'", directory = "../b1" },
{ name = "b2", marker = "python_full_version >= '3.12'", directory = "../b2" },
{ name = "b1", marker = "python_full_version < '3.12'", directory = "b1" },
{ name = "b2", marker = "python_full_version >= '3.12'", directory = "b2" },
]
[[package]]
name = "b1"
version = "0.1.0"
source = { directory = "../b1" }
source = { directory = "b1" }
dependencies = [
{ name = "iniconfig", version = "1.1.1", source = { url = "https://files.pythonhosted.org/packages/9b/dd/b3c12c6d707058fa947864b67f0c4e0c39ef8610988d7baea9578f3c48f3/iniconfig-1.1.1-py2.py3-none-any.whl" }, marker = "python_full_version < '3.12'" },
]
@ -294,7 +294,7 @@ fn root_package_splits_transitive_too() -> Result<()> {
[[package]]
name = "b2"
version = "0.1.0"
source = { directory = "../b2" }
source = { directory = "b2" }
dependencies = [
{ name = "iniconfig", version = "2.0.0", source = { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" }, marker = "python_full_version >= '3.12'" },
]
@ -804,12 +804,12 @@ fn dont_pre_visit_url_packages() -> Result<()> {
]
[package.metadata]
requires-dist = [{ name = "c", directory = "../c" }]
requires-dist = [{ name = "c", directory = "c" }]
[[package]]
name = "c"
version = "0.1.0"
source = { directory = "../c" }
source = { directory = "c" }
"###);
Ok(())

View file

@ -4159,7 +4159,7 @@ fn lock_relative_and_absolute_paths() -> Result<()> {
[package.metadata]
requires-dist = [
{ name = "b", directory = "b" },
{ name = "c", directory = "[TEMP_DIR]/c" },
{ name = "c", directory = "c" },
]
[[package]]
@ -4170,7 +4170,7 @@ fn lock_relative_and_absolute_paths() -> Result<()> {
[[package]]
name = "c"
version = "0.1.0"
source = { directory = "[TEMP_DIR]/c" }
source = { directory = "c" }
"###
);
});
@ -5074,7 +5074,7 @@ fn lock_same_version_multiple_urls() -> Result<()> {
[[package]]
name = "dependency"
version = "0.0.1"
source = { directory = "[TEMP_DIR]/v1" }
source = { directory = "v1" }
resolution-markers = [
"sys_platform == 'darwin'",
]
@ -5088,7 +5088,7 @@ fn lock_same_version_multiple_urls() -> Result<()> {
[[package]]
name = "dependency"
version = "0.0.1"
source = { directory = "[TEMP_DIR]/v2" }
source = { directory = "v2" }
resolution-markers = [
"sys_platform != 'darwin'",
]
@ -5113,14 +5113,14 @@ fn lock_same_version_multiple_urls() -> Result<()> {
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "dependency", version = "0.0.1", source = { directory = "[TEMP_DIR]/v1" }, marker = "sys_platform == 'darwin'" },
{ name = "dependency", version = "0.0.1", source = { directory = "[TEMP_DIR]/v2" }, marker = "sys_platform != 'darwin'" },
{ name = "dependency", version = "0.0.1", source = { directory = "v1" }, marker = "sys_platform == 'darwin'" },
{ name = "dependency", version = "0.0.1", source = { directory = "v2" }, marker = "sys_platform != 'darwin'" },
]
[package.metadata]
requires-dist = [
{ name = "dependency", marker = "sys_platform != 'darwin'", directory = "[TEMP_DIR]/v2" },
{ name = "dependency", marker = "sys_platform == 'darwin'", directory = "[TEMP_DIR]/v1" },
{ name = "dependency", marker = "sys_platform != 'darwin'", directory = "v2" },
{ name = "dependency", marker = "sys_platform == 'darwin'", directory = "v1" },
]
[[package]]
@ -7118,7 +7118,7 @@ fn lock_sources_archive() -> Result<()> {
Url::from_file_path(&workspace_archive).unwrap(),
})?;
uv_snapshot!(context.filters(), context.lock(), @r###"
uv_snapshot!( context.lock(), @r###"
success: true
exit_code: 0
----- stdout -----
@ -7129,11 +7129,11 @@ fn lock_sources_archive() -> Result<()> {
let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap();
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
lock, @r###"
// insta::with_settings!({
// filters => context.filters(),
// }, {
assert_snapshot!(
lock, @r###"
version = 1
requires-python = ">=3.12"
@ -7171,7 +7171,7 @@ fn lock_sources_archive() -> Result<()> {
]
[package.metadata]
requires-dist = [{ name = "workspace", path = "[TEMP_DIR]/workspace.zip" }]
requires-dist = [{ name = "workspace", path = "workspace.zip" }]
[[package]]
name = "sniffio"
@ -7185,7 +7185,7 @@ fn lock_sources_archive() -> Result<()> {
[[package]]
name = "workspace"
version = "0.1.0"
source = { path = "[TEMP_DIR]/workspace.zip" }
source = { path = "workspace.zip" }
dependencies = [
{ name = "anyio" },
]
@ -7193,8 +7193,8 @@ fn lock_sources_archive() -> Result<()> {
[package.metadata]
requires-dist = [{ name = "anyio" }]
"###
);
});
);
// });
// Re-run with `--locked`.
uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###"
@ -7293,7 +7293,7 @@ fn lock_sources_source_tree() -> Result<()> {
[[package]]
name = "anyio"
version = "0.1.0"
source = { editable = "[TEMP_DIR]/workspace/anyio" }
source = { editable = "workspace/anyio" }
[[package]]
name = "project"
@ -7304,18 +7304,18 @@ fn lock_sources_source_tree() -> Result<()> {
]
[package.metadata]
requires-dist = [{ name = "workspace", directory = "[TEMP_DIR]/workspace" }]
requires-dist = [{ name = "workspace", directory = "workspace" }]
[[package]]
name = "workspace"
version = "0.1.0"
source = { directory = "[TEMP_DIR]/workspace" }
source = { directory = "workspace" }
dependencies = [
{ name = "anyio" },
]
[package.metadata]
requires-dist = [{ name = "anyio", editable = "[TEMP_DIR]/workspace/anyio" }]
requires-dist = [{ name = "anyio", editable = "workspace/anyio" }]
"###
);
});

View file

@ -1497,3 +1497,61 @@ fn workspace_member_name_shadows_dependencies() -> Result<()> {
Ok(())
}
/// Test that path dependencies with path dependencies resolve paths correctly across workspaces.
///
/// Each package is its own workspace. We put the other projects into a separate directory `libs` so
/// the paths don't line up by accident.
#[test]
fn test_path_hopping() -> Result<()> {
let context = TestContext::new("3.12");
// Build the main project ...
let deps = indoc! {r#"
dependencies = ["foo"]
[tool.uv.sources]
foo = { path = "../libs/foo", editable = true }
"#};
let main_project_dir = context.temp_dir.join("project");
make_project(&main_project_dir, "project", deps)?;
// ... that depends on foo ...
let deps = indoc! {r#"
dependencies = ["bar"]
[tool.uv.sources]
bar = { path = "../../libs/bar", editable = true }
"#};
make_project(&context.temp_dir.join("libs").join("foo"), "foo", deps)?;
// ... that depends on bar, a stub project.
make_project(&context.temp_dir.join("libs").join("bar"), "bar", "")?;
uv_snapshot!(context.filters(), context.lock().arg("--preview").current_dir(&main_project_dir), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
Resolved 3 packages in [TIME]
"###
);
let lock: SourceLock =
toml::from_str(&fs_err::read_to_string(main_project_dir.join("uv.lock"))?)?;
assert_json_snapshot!(lock.sources(), @r###"
{
"bar": {
"editable": "../libs/bar"
},
"foo": {
"editable": "../libs/foo"
},
"project": {
"editable": "."
}
}
"###);
Ok(())
}