diff --git a/Cargo.lock b/Cargo.lock index 7612c9ce15cf..71edd66e8d69 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4863,6 +4863,7 @@ dependencies = [ "fs2", "junction", "path-slash", + "schemars", "serde", "tempfile", "tokio", diff --git a/crates/uv-distribution/src/metadata/lowering.rs b/crates/uv-distribution/src/metadata/lowering.rs index 5deaf520f1e6..378277546222 100644 --- a/crates/uv-distribution/src/metadata/lowering.rs +++ b/crates/uv-distribution/src/metadata/lowering.rs @@ -88,20 +88,20 @@ impl LoweredRequirement { if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) { 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 } => { if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) { return Err(LoweringError::ConflictingUrls); } - url_source(url, subdirectory)? + url_source(url, subdirectory.map(PathBuf::from))? } Source::Path { path, editable } => { if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) { return Err(LoweringError::ConflictingUrls); } path_source( - path, + PathBuf::from(path), origin, project_dir, workspace.install_path(), @@ -203,19 +203,25 @@ impl LoweredRequirement { if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) { 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 } => { if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) { return Err(LoweringError::ConflictingUrls); } - url_source(url, subdirectory)? + url_source(url, subdirectory.map(PathBuf::from))? } Source::Path { path, editable } => { if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) { 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::Workspace { .. } => { diff --git a/crates/uv-fs/Cargo.toml b/crates/uv-fs/Cargo.toml index 3ce6f917982d..c7aa1cb1a20f 100644 --- a/crates/uv-fs/Cargo.toml +++ b/crates/uv-fs/Cargo.toml @@ -22,6 +22,7 @@ encoding_rs_io = { workspace = true } fs-err = { workspace = true } fs2 = { workspace = true } path-slash = { workspace = true } +schemars = { workspace = true, optional = true } serde = { workspace = true, optional = true } tokio = { workspace = true, optional = true} tempfile = { workspace = true } diff --git a/crates/uv-fs/src/path.rs b/crates/uv-fs/src/path.rs index b010a3576a2d..4e5e8049eb25 100644 --- a/crates/uv-fs/src/path.rs +++ b/crates/uv-fs/src/path.rs @@ -301,6 +301,17 @@ pub struct PortablePath<'a>(&'a Path); #[derive(Debug, Clone, PartialEq, Eq)] 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 for PortablePath<'_> { fn as_ref(&self) -> &Path { self.0 diff --git a/crates/uv-workspace/Cargo.toml b/crates/uv-workspace/Cargo.toml index 5ddaabbf5995..d77df0f98e61 100644 --- a/crates/uv-workspace/Cargo.toml +++ b/crates/uv-workspace/Cargo.toml @@ -16,7 +16,7 @@ workspace = true pep440_rs = { workspace = true } pep508_rs = { workspace = true } pypi-types = { workspace = true } -uv-fs = { workspace = true, features = ["tokio"] } +uv-fs = { workspace = true, features = ["tokio", "schemars"] } uv-git = { workspace = true } uv-macros = { workspace = true } uv-normalize = { workspace = true } diff --git a/crates/uv-workspace/src/pyproject.rs b/crates/uv-workspace/src/pyproject.rs index faf8a65ce877..368a9b9cb312 100644 --- a/crates/uv-workspace/src/pyproject.rs +++ b/crates/uv-workspace/src/pyproject.rs @@ -17,7 +17,7 @@ use url::Url; use pep440_rs::{Version, VersionSpecifiers}; use pypi_types::{RequirementSource, SupportedEnvironments, VerbatimParsedUrl}; -use uv_fs::relative_to; +use uv_fs::{relative_to, PortablePathBuf}; use uv_git::GitReference; use uv_macros::OptionsMetadata; use uv_normalize::{ExtraName, PackageName}; @@ -413,7 +413,7 @@ pub enum Source { /// The repository URL (without the `git+` prefix). git: Url, /// The path to the directory with the `pyproject.toml`, if it's not in the archive root. - subdirectory: Option, + subdirectory: Option, // Only one of the three may be used; we'll validate this later and emit a custom error. rev: Option, tag: Option, @@ -430,13 +430,13 @@ pub enum Source { url: Url, /// For source distributions, the path to the directory with the `pyproject.toml`, if it's /// not in the archive root. - subdirectory: Option, + subdirectory: Option, }, /// 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 /// `setup.py` file in the root). Path { - path: PathBuf, + path: PortablePathBuf, /// `false` by default. editable: Option, }, @@ -454,12 +454,12 @@ pub enum Source { /// A catch-all variant used to emit precise error messages when deserializing. CatchAll { git: String, - subdirectory: Option, + subdirectory: Option, rev: Option, tag: Option, branch: Option, url: String, - path: PathBuf, + path: PortablePathBuf, index: String, workspace: bool, }, @@ -534,15 +534,17 @@ impl Source { RequirementSource::Path { install_path, .. } | RequirementSource::Directory { install_path, .. } => Source::Path { editable, - path: relative_to(&install_path, root) - .or_else(|_| std::path::absolute(&install_path)) - .map_err(SourceError::Absolute)?, + path: PortablePathBuf::from( + relative_to(&install_path, root) + .or_else(|_| std::path::absolute(&install_path)) + .map_err(SourceError::Absolute)?, + ), }, RequirementSource::Url { subdirectory, url, .. } => Source::Url { url: url.to_url(), - subdirectory, + subdirectory: subdirectory.map(PortablePathBuf::from), }, RequirementSource::Git { repository, @@ -566,7 +568,7 @@ impl Source { tag, branch, git: repository, - subdirectory, + subdirectory: subdirectory.map(PortablePathBuf::from), } } else { Source::Git { @@ -574,7 +576,7 @@ impl Source { tag, branch, git: repository, - subdirectory, + subdirectory: subdirectory.map(PortablePathBuf::from), } } } diff --git a/crates/uv/tests/edit.rs b/crates/uv/tests/edit.rs index ba074ac6fba3..e1c186ed8a97 100644 --- a/crates/uv/tests/edit.rs +++ b/crates/uv/tests/edit.rs @@ -5,6 +5,7 @@ use assert_cmd::assert::OutputAssertExt; use assert_fs::prelude::*; use indoc::indoc; use insta::assert_snapshot; +use std::path::Path; use crate::common::{decode_token, packse_index_url}; use common::{uv_snapshot, TestContext}; @@ -2019,7 +2020,7 @@ fn add_path() -> Result<()> { 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#" [project] name = "child" @@ -2032,7 +2033,7 @@ fn add_path() -> Result<()> { 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 exit_code: 0 ----- stdout ----- @@ -2043,7 +2044,7 @@ fn add_path() -> Result<()> { Resolved 2 packages in [TIME] Prepared 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) "###); @@ -2067,7 +2068,7 @@ fn add_path() -> Result<()> { build-backend = "setuptools.build_meta" [tool.uv.sources] - child = { path = "child" } + child = { path = "packages/child" } "### ); }); @@ -2089,7 +2090,7 @@ fn add_path() -> Result<()> { [[package]] name = "child" version = "0.1.0" - source = { directory = "child" } + source = { directory = "packages/child" } [[package]] name = "parent" @@ -2100,7 +2101,7 @@ fn add_path() -> Result<()> { ] [package.metadata] - requires-dist = [{ name = "child", directory = "child" }] + requires-dist = [{ name = "child", directory = "packages/child" }] "### ); }); diff --git a/uv.schema.json b/uv.schema.json index cafce3caea2a..15cdc4729ca4 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -1230,9 +1230,13 @@ }, "subdirectory": { "description": "The path to the directory with the `pyproject.toml`, if it's not in the archive root.", - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/String" + }, + { + "type": "null" + } ] }, "tag": { @@ -1253,9 +1257,13 @@ "properties": { "subdirectory": { "description": "For source distributions, the path to the directory with the `pyproject.toml`, if it's not in the archive root.", - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/String" + }, + { + "type": "null" + } ] }, "url": { @@ -1280,7 +1288,7 @@ ] }, "path": { - "type": "string" + "$ref": "#/definitions/String" } }, "additionalProperties": false @@ -1336,7 +1344,7 @@ "type": "string" }, "path": { - "type": "string" + "$ref": "#/definitions/String" }, "rev": { "type": [ @@ -1345,9 +1353,13 @@ ] }, "subdirectory": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/String" + }, + { + "type": "null" + } ] }, "tag": {