Use portable paths when serializing sources (#7504)

## Summary

Closes https://github.com/astral-sh/uv/issues/7493.
This commit is contained in:
Charlie Marsh 2024-09-18 14:51:14 -04:00 committed by GitHub
parent 1379b530f6
commit e36cc99b0d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 70 additions and 36 deletions

1
Cargo.lock generated
View file

@ -4863,6 +4863,7 @@ dependencies = [
"fs2", "fs2",
"junction", "junction",
"path-slash", "path-slash",
"schemars",
"serde", "serde",
"tempfile", "tempfile",
"tokio", "tokio",

View file

@ -88,20 +88,20 @@ impl LoweredRequirement {
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) { if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
return Err(LoweringError::ConflictingUrls); return Err(LoweringError::ConflictingUrls);
} }
git_source(&git, subdirectory, rev, tag, branch)? git_source(&git, subdirectory.map(PathBuf::from), rev, tag, branch)?
} }
Source::Url { url, subdirectory } => { Source::Url { url, subdirectory } => {
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) { if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
return Err(LoweringError::ConflictingUrls); return Err(LoweringError::ConflictingUrls);
} }
url_source(url, subdirectory)? url_source(url, subdirectory.map(PathBuf::from))?
} }
Source::Path { path, editable } => { Source::Path { path, editable } => {
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) { if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
return Err(LoweringError::ConflictingUrls); return Err(LoweringError::ConflictingUrls);
} }
path_source( path_source(
path, PathBuf::from(path),
origin, origin,
project_dir, project_dir,
workspace.install_path(), workspace.install_path(),
@ -203,19 +203,25 @@ impl LoweredRequirement {
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) { if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
return Err(LoweringError::ConflictingUrls); return Err(LoweringError::ConflictingUrls);
} }
git_source(&git, subdirectory, rev, tag, branch)? git_source(&git, subdirectory.map(PathBuf::from), rev, tag, branch)?
} }
Source::Url { url, subdirectory } => { Source::Url { url, subdirectory } => {
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) { if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
return Err(LoweringError::ConflictingUrls); return Err(LoweringError::ConflictingUrls);
} }
url_source(url, subdirectory)? url_source(url, subdirectory.map(PathBuf::from))?
} }
Source::Path { path, editable } => { Source::Path { path, editable } => {
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) { if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
return Err(LoweringError::ConflictingUrls); return Err(LoweringError::ConflictingUrls);
} }
path_source(path, Origin::Project, dir, dir, editable.unwrap_or(false))? path_source(
PathBuf::from(path),
Origin::Project,
dir,
dir,
editable.unwrap_or(false),
)?
} }
Source::Registry { index } => registry_source(&requirement, index)?, Source::Registry { index } => registry_source(&requirement, index)?,
Source::Workspace { .. } => { Source::Workspace { .. } => {

View file

@ -22,6 +22,7 @@ encoding_rs_io = { workspace = true }
fs-err = { workspace = true } fs-err = { workspace = true }
fs2 = { workspace = true } fs2 = { workspace = true }
path-slash = { workspace = true } path-slash = { workspace = true }
schemars = { workspace = true, optional = true }
serde = { workspace = true, optional = true } serde = { workspace = true, optional = true }
tokio = { workspace = true, optional = true} tokio = { workspace = true, optional = true}
tempfile = { workspace = true } tempfile = { workspace = true }

View file

@ -301,6 +301,17 @@ pub struct PortablePath<'a>(&'a Path);
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct PortablePathBuf(PathBuf); pub struct PortablePathBuf(PathBuf);
#[cfg(feature = "schemars")]
impl schemars::JsonSchema for PortablePathBuf {
fn schema_name() -> String {
PathBuf::schema_name()
}
fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
PathBuf::json_schema(_gen)
}
}
impl AsRef<Path> for PortablePath<'_> { impl AsRef<Path> for PortablePath<'_> {
fn as_ref(&self) -> &Path { fn as_ref(&self) -> &Path {
self.0 self.0

View file

@ -16,7 +16,7 @@ workspace = true
pep440_rs = { workspace = true } pep440_rs = { workspace = true }
pep508_rs = { workspace = true } pep508_rs = { workspace = true }
pypi-types = { workspace = true } pypi-types = { workspace = true }
uv-fs = { workspace = true, features = ["tokio"] } uv-fs = { workspace = true, features = ["tokio", "schemars"] }
uv-git = { workspace = true } uv-git = { workspace = true }
uv-macros = { workspace = true } uv-macros = { workspace = true }
uv-normalize = { workspace = true } uv-normalize = { workspace = true }

View file

@ -17,7 +17,7 @@ use url::Url;
use pep440_rs::{Version, VersionSpecifiers}; use pep440_rs::{Version, VersionSpecifiers};
use pypi_types::{RequirementSource, SupportedEnvironments, VerbatimParsedUrl}; use pypi_types::{RequirementSource, SupportedEnvironments, VerbatimParsedUrl};
use uv_fs::relative_to; use uv_fs::{relative_to, PortablePathBuf};
use uv_git::GitReference; use uv_git::GitReference;
use uv_macros::OptionsMetadata; use uv_macros::OptionsMetadata;
use uv_normalize::{ExtraName, PackageName}; use uv_normalize::{ExtraName, PackageName};
@ -413,7 +413,7 @@ pub enum Source {
/// The repository URL (without the `git+` prefix). /// The repository URL (without the `git+` prefix).
git: Url, git: Url,
/// The path to the directory with the `pyproject.toml`, if it's not in the archive root. /// The path to the directory with the `pyproject.toml`, if it's not in the archive root.
subdirectory: Option<PathBuf>, subdirectory: Option<PortablePathBuf>,
// Only one of the three may be used; we'll validate this later and emit a custom error. // Only one of the three may be used; we'll validate this later and emit a custom error.
rev: Option<String>, rev: Option<String>,
tag: Option<String>, tag: Option<String>,
@ -430,13 +430,13 @@ pub enum Source {
url: Url, url: Url,
/// For source distributions, the path to the directory with the `pyproject.toml`, if it's /// For source distributions, the path to the directory with the `pyproject.toml`, if it's
/// not in the archive root. /// not in the archive root.
subdirectory: Option<PathBuf>, subdirectory: Option<PortablePathBuf>,
}, },
/// The path to a dependency, either a wheel (a `.whl` file), source distribution (a `.zip` or /// The path to a dependency, either a wheel (a `.whl` file), source distribution (a `.zip` or
/// `.tar.gz` file), or source tree (i.e., a directory containing a `pyproject.toml` or /// `.tar.gz` file), or source tree (i.e., a directory containing a `pyproject.toml` or
/// `setup.py` file in the root). /// `setup.py` file in the root).
Path { Path {
path: PathBuf, path: PortablePathBuf,
/// `false` by default. /// `false` by default.
editable: Option<bool>, editable: Option<bool>,
}, },
@ -454,12 +454,12 @@ pub enum Source {
/// A catch-all variant used to emit precise error messages when deserializing. /// A catch-all variant used to emit precise error messages when deserializing.
CatchAll { CatchAll {
git: String, git: String,
subdirectory: Option<PathBuf>, subdirectory: Option<PortablePathBuf>,
rev: Option<String>, rev: Option<String>,
tag: Option<String>, tag: Option<String>,
branch: Option<String>, branch: Option<String>,
url: String, url: String,
path: PathBuf, path: PortablePathBuf,
index: String, index: String,
workspace: bool, workspace: bool,
}, },
@ -534,15 +534,17 @@ impl Source {
RequirementSource::Path { install_path, .. } RequirementSource::Path { install_path, .. }
| RequirementSource::Directory { install_path, .. } => Source::Path { | RequirementSource::Directory { install_path, .. } => Source::Path {
editable, editable,
path: relative_to(&install_path, root) path: PortablePathBuf::from(
.or_else(|_| std::path::absolute(&install_path)) relative_to(&install_path, root)
.map_err(SourceError::Absolute)?, .or_else(|_| std::path::absolute(&install_path))
.map_err(SourceError::Absolute)?,
),
}, },
RequirementSource::Url { RequirementSource::Url {
subdirectory, url, .. subdirectory, url, ..
} => Source::Url { } => Source::Url {
url: url.to_url(), url: url.to_url(),
subdirectory, subdirectory: subdirectory.map(PortablePathBuf::from),
}, },
RequirementSource::Git { RequirementSource::Git {
repository, repository,
@ -566,7 +568,7 @@ impl Source {
tag, tag,
branch, branch,
git: repository, git: repository,
subdirectory, subdirectory: subdirectory.map(PortablePathBuf::from),
} }
} else { } else {
Source::Git { Source::Git {
@ -574,7 +576,7 @@ impl Source {
tag, tag,
branch, branch,
git: repository, git: repository,
subdirectory, subdirectory: subdirectory.map(PortablePathBuf::from),
} }
} }
} }

View file

@ -5,6 +5,7 @@ use assert_cmd::assert::OutputAssertExt;
use assert_fs::prelude::*; use assert_fs::prelude::*;
use indoc::indoc; use indoc::indoc;
use insta::assert_snapshot; use insta::assert_snapshot;
use std::path::Path;
use crate::common::{decode_token, packse_index_url}; use crate::common::{decode_token, packse_index_url};
use common::{uv_snapshot, TestContext}; use common::{uv_snapshot, TestContext};
@ -2019,7 +2020,7 @@ fn add_path() -> Result<()> {
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
"#})?; "#})?;
let child = workspace.child("child"); let child = workspace.child("packages").child("child");
child.child("pyproject.toml").write_str(indoc! {r#" child.child("pyproject.toml").write_str(indoc! {r#"
[project] [project]
name = "child" name = "child"
@ -2032,7 +2033,7 @@ fn add_path() -> Result<()> {
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
"#})?; "#})?;
uv_snapshot!(context.filters(), context.add().arg("./child").current_dir(workspace.path()), @r###" uv_snapshot!(context.filters(), context.add().arg(Path::new("packages").join("child")).current_dir(workspace.path()), @r###"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
@ -2043,7 +2044,7 @@ fn add_path() -> Result<()> {
Resolved 2 packages in [TIME] Resolved 2 packages in [TIME]
Prepared 2 packages in [TIME] Prepared 2 packages in [TIME]
Installed 2 packages in [TIME] Installed 2 packages in [TIME]
+ child==0.1.0 (from file://[TEMP_DIR]/workspace/child) + child==0.1.0 (from file://[TEMP_DIR]/workspace/packages/child)
+ parent==0.1.0 (from file://[TEMP_DIR]/workspace) + parent==0.1.0 (from file://[TEMP_DIR]/workspace)
"###); "###);
@ -2067,7 +2068,7 @@ fn add_path() -> Result<()> {
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[tool.uv.sources] [tool.uv.sources]
child = { path = "child" } child = { path = "packages/child" }
"### "###
); );
}); });
@ -2089,7 +2090,7 @@ fn add_path() -> Result<()> {
[[package]] [[package]]
name = "child" name = "child"
version = "0.1.0" version = "0.1.0"
source = { directory = "child" } source = { directory = "packages/child" }
[[package]] [[package]]
name = "parent" name = "parent"
@ -2100,7 +2101,7 @@ fn add_path() -> Result<()> {
] ]
[package.metadata] [package.metadata]
requires-dist = [{ name = "child", directory = "child" }] requires-dist = [{ name = "child", directory = "packages/child" }]
"### "###
); );
}); });

34
uv.schema.json generated
View file

@ -1230,9 +1230,13 @@
}, },
"subdirectory": { "subdirectory": {
"description": "The path to the directory with the `pyproject.toml`, if it's not in the archive root.", "description": "The path to the directory with the `pyproject.toml`, if it's not in the archive root.",
"type": [ "anyOf": [
"string", {
"null" "$ref": "#/definitions/String"
},
{
"type": "null"
}
] ]
}, },
"tag": { "tag": {
@ -1253,9 +1257,13 @@
"properties": { "properties": {
"subdirectory": { "subdirectory": {
"description": "For source distributions, the path to the directory with the `pyproject.toml`, if it's not in the archive root.", "description": "For source distributions, the path to the directory with the `pyproject.toml`, if it's not in the archive root.",
"type": [ "anyOf": [
"string", {
"null" "$ref": "#/definitions/String"
},
{
"type": "null"
}
] ]
}, },
"url": { "url": {
@ -1280,7 +1288,7 @@
] ]
}, },
"path": { "path": {
"type": "string" "$ref": "#/definitions/String"
} }
}, },
"additionalProperties": false "additionalProperties": false
@ -1336,7 +1344,7 @@
"type": "string" "type": "string"
}, },
"path": { "path": {
"type": "string" "$ref": "#/definitions/String"
}, },
"rev": { "rev": {
"type": [ "type": [
@ -1345,9 +1353,13 @@
] ]
}, },
"subdirectory": { "subdirectory": {
"type": [ "anyOf": [
"string", {
"null" "$ref": "#/definitions/String"
},
{
"type": "null"
}
] ]
}, },
"tag": { "tag": {