Tweak some of the tool.uv.sources error messages for consistency (#3364)

This commit is contained in:
Charlie Marsh 2024-05-03 22:38:47 -04:00 committed by GitHub
parent 100935f4f1
commit 363e808724
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 47 additions and 43 deletions

View file

@ -146,9 +146,9 @@ pub enum RequirementSource {
/// `<scheme>://<domain>/<path>#subdirectory=<subdirectory>`. /// `<scheme>://<domain>/<path>#subdirectory=<subdirectory>`.
url: VerbatimUrl, url: VerbatimUrl,
}, },
/// A remote git repository, either over HTTPS or over SSH. /// A remote Git repository, over either HTTPS or SSH.
Git { Git {
/// The repository URL (without `git+` prefix). /// The repository URL (without the `git+` prefix).
repository: Url, repository: Url,
/// Optionally, the revision, tag, or branch to use. /// Optionally, the revision, tag, or branch to use.
reference: GitReference, reference: GitReference,

View file

@ -31,7 +31,7 @@ use crate::ExtrasSpecification;
pub enum Pep621Error { pub enum Pep621Error {
#[error(transparent)] #[error(transparent)]
Pep508(#[from] pep508_rs::Pep508Error), Pep508(#[from] pep508_rs::Pep508Error),
#[error("You need to specify a `[project]` section to use `[tool.uv.sources]`")] #[error("Must specify a `[project]` section alongside `[tool.uv.sources]`")]
MissingProjectSection, MissingProjectSection,
#[error("pyproject.toml section is declared as dynamic, but must be static: `{0}`")] #[error("pyproject.toml section is declared as dynamic, but must be static: `{0}`")]
CantBeDynamic(&'static str), CantBeDynamic(&'static str),
@ -47,32 +47,33 @@ pub enum LoweringError {
DirectUrl(#[from] Box<ParsedUrlError>), DirectUrl(#[from] Box<ParsedUrlError>),
#[error("Unsupported path (can't convert to URL): `{}`", _0.user_display())] #[error("Unsupported path (can't convert to URL): `{}`", _0.user_display())]
PathToUrl(PathBuf), PathToUrl(PathBuf),
#[error("The package is not included as workspace package in `tool.uv.workspace`")] #[error("Package is not included as workspace package in `tool.uv.workspace`")]
UndeclaredWorkspacePackage, UndeclaredWorkspacePackage,
#[error("You need to specify a version constraint")] #[error("Must specify a version constraint")]
UnconstrainedVersion, UnconstrainedVersion,
#[error("You can only use one of rev, tag or branch")] #[error("Can only specify one of rev, tag, or branch")]
MoreThanOneGitRef, MoreThanOneGitRef,
#[error("You can't combine these options in `tool.uv.sources`")] #[error("Unable to combine options in `tool.uv.sources`")]
InvalidEntry, InvalidEntry,
#[error(transparent)] #[error(transparent)]
InvalidUrl(#[from] url::ParseError), InvalidUrl(#[from] url::ParseError),
#[error("You can't combine a url in `project` with `tool.uv.sources`")] #[error("Can't combine URLs from both `project.dependencies` and `tool.uv.sources`")]
ConflictingUrls, ConflictingUrls,
/// Note: Infallible on unix and windows.
#[error("Could not normalize path: `{0}`")] #[error("Could not normalize path: `{0}`")]
AbsolutizeError(String, #[source] io::Error), AbsolutizeError(String, #[source] io::Error),
#[error("Fragments are not allowed in URLs: `{0}`")] #[error("Fragments are not allowed in URLs: `{0}`")]
ForbiddenFragment(Url), ForbiddenFragment(Url),
#[error("`workspace = false` is not yet supported")]
WorkspaceFalse,
} }
/// A `pyproject.toml` as specified in PEP 517. /// A `pyproject.toml` as specified in PEP 517.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub struct PyProjectToml { pub struct PyProjectToml {
/// Project metadata /// PEP 621-compliant project metadata.
pub project: Option<Project>, pub project: Option<Project>,
/// Uv additions /// Proprietary additions.
pub tool: Option<Tool>, pub tool: Option<Tool>,
} }
@ -149,17 +150,18 @@ impl Deref for SerdePattern {
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged, deny_unknown_fields)] #[serde(untagged, deny_unknown_fields)]
pub enum Source { pub enum Source {
/// A remote git repository, either over HTTPS or over SSH. /// A remote Git repository, available over HTTPS or SSH.
/// ///
/// Example: /// Example:
/// ```toml /// ```toml
/// flask = { git = "https://github.com/pallets/flask", tag = "3.0.0" } /// flask = { git = "https://github.com/pallets/flask", tag = "3.0.0" }
/// ``` /// ```
Git { Git {
/// The repository URL (without the `git+` prefix).
git: Url, git: Url,
/// The path to the directory with the `pyproject.toml` if it is not in the archive root. /// The path to the directory with the `pyproject.toml`, if it's not in the archive root.
subdirectory: Option<String>, subdirectory: Option<String>,
// Only one of the three may be used, we validate this later for a better error message. // 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>,
branch: Option<String>, branch: Option<String>,
@ -173,33 +175,32 @@ pub enum Source {
/// ``` /// ```
Url { Url {
url: Url, url: Url,
/// For source distributions, the path to the directory with the `pyproject.toml` if it is /// 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<String>, subdirectory: Option<String>,
}, },
/// The path to a dependency. It can either be a wheel (a `.whl` file), a source distribution /// The path to a dependency, either a wheel (a `.whl` file), source distribution (a `.zip` or
/// as archive (a `.zip` or `.tag.gz` file) or a source distribution as directory (a directory /// `.tag.gz` file), or source tree (i.e., a directory containing a `pyproject.toml` or
/// with a pyproject.toml in, or a legacy directory with only a setup.py but non pyproject.toml /// `setup.py` file in the root).
/// in it).
Path { Path {
path: String, path: String,
/// `false` by default. /// `false` by default.
editable: Option<bool>, editable: Option<bool>,
}, },
/// When using a version as requirement, you can optionally pin the requirement to an index /// A dependency pinned to a specific index, e.g., `torch` after setting `torch` to `https://download.pytorch.org/whl/cu118`.
/// you defined, e.g. `torch` after configuring `torch` to
/// `https://download.pytorch.org/whl/cu118`.
Registry { Registry {
// TODO(konstin): The string is more-or-less a placeholder // TODO(konstin): The string is more-or-less a placeholder
index: String, index: String,
}, },
/// A dependency on another package in the workspace. /// A dependency on another package in the workspace.
Workspace { Workspace {
/// When set to `false`, the package will be fetched from the remote index, rather than
/// included as a workspace package.
workspace: bool, workspace: bool,
/// `true` by default. /// `true` by default.
editable: Option<bool>, editable: Option<bool>,
}, },
/// Show a better error message for invalid combinations of options. /// A catch-all variant used to emit precise error messages when deserializing.
CatchAll { CatchAll {
git: String, git: String,
subdirectory: Option<String>, subdirectory: Option<String>,
@ -365,7 +366,7 @@ pub(crate) fn lower_requirements(
}) })
} }
/// Combine `project.dependencies`/`project.optional-dependencies` with `tool.uv.sources`. /// Combine `project.dependencies` or `project.optional-dependencies` with `tool.uv.sources`.
pub(crate) fn lower_requirement( pub(crate) fn lower_requirement(
requirement: pep508_rs::Requirement, requirement: pep508_rs::Requirement,
project_name: &PackageName, project_name: &PackageName,
@ -391,7 +392,8 @@ pub(crate) fn lower_requirement(
} }
let Some(source) = source else { let Some(source) = source else {
// Support recursive editable inclusions. TODO(konsti): This is a workspace feature. // Support recursive editable inclusions.
// TODO(konsti): This is a workspace feature.
return if requirement.version_or_url.is_none() && &requirement.name != project_name { return if requirement.version_or_url.is_none() && &requirement.name != project_name {
Err(LoweringError::UnconstrainedVersion) Err(LoweringError::UnconstrainedVersion)
} else { } else {
@ -482,12 +484,12 @@ pub(crate) fn lower_requirement(
workspace, workspace,
editable, editable,
} => { } => {
if !workspace {
return Err(LoweringError::WorkspaceFalse);
}
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);
} }
if !workspace {
todo!()
}
let path = workspace_packages let path = workspace_packages
.get(&requirement.name) .get(&requirement.name)
.ok_or(LoweringError::UndeclaredWorkspacePackage)? .ok_or(LoweringError::UndeclaredWorkspacePackage)?
@ -495,7 +497,7 @@ pub(crate) fn lower_requirement(
path_source(path, project_dir, editable)? path_source(path, project_dir, editable)?
} }
Source::CatchAll { .. } => { Source::CatchAll { .. } => {
// This is better than a serde error about not matching any enum variant // Emit a dedicated error message, which is an improvement over Serde's default error.
return Err(LoweringError::InvalidEntry); return Err(LoweringError::InvalidEntry);
} }
}; };
@ -526,8 +528,8 @@ fn path_source(
}) })
} }
/// Given an extra in a project that may contain references to the project /// Given an extra in a project that may contain references to the project itself, flatten it into
/// itself, flatten it into a list of requirements. /// a list of requirements.
/// ///
/// For example: /// For example:
/// ```toml /// ```toml
@ -665,7 +667,7 @@ mod test {
assert_snapshot!(format_err(input), @r###" assert_snapshot!(format_err(input), @r###"
error: Failed to parse `pyproject.toml` error: Failed to parse `pyproject.toml`
Caused by: Failed to parse entry for: `tqdm` Caused by: Failed to parse entry for: `tqdm`
Caused by: You can't combine a url in `project` with `tool.uv.sources` Caused by: Can't combine URLs from both `project.dependencies` and `tool.uv.sources`
"###); "###);
} }
@ -686,7 +688,7 @@ mod test {
assert_snapshot!(format_err(input), @r###" assert_snapshot!(format_err(input), @r###"
error: Failed to parse `pyproject.toml` error: Failed to parse `pyproject.toml`
Caused by: Failed to parse entry for: `tqdm` Caused by: Failed to parse entry for: `tqdm`
Caused by: You can only use one of rev, tag or branch Caused by: Can only specify one of rev, tag, or branch
"###); "###);
} }
@ -756,7 +758,7 @@ mod test {
assert_snapshot!(format_err(input), @r###" assert_snapshot!(format_err(input), @r###"
error: Failed to parse `pyproject.toml` error: Failed to parse `pyproject.toml`
Caused by: Failed to parse entry for: `tqdm` Caused by: Failed to parse entry for: `tqdm`
Caused by: You need to specify a version constraint Caused by: Must specify a version constraint
"###); "###);
} }
@ -828,7 +830,7 @@ mod test {
assert_snapshot!(format_err(input), @r###" assert_snapshot!(format_err(input), @r###"
error: Failed to parse `pyproject.toml` error: Failed to parse `pyproject.toml`
Caused by: Failed to parse entry for: `tqdm` Caused by: Failed to parse entry for: `tqdm`
Caused by: You can't combine a url in `project` with `tool.uv.sources` Caused by: Can't combine URLs from both `project.dependencies` and `tool.uv.sources`
"###); "###);
} }
@ -849,7 +851,7 @@ mod test {
assert_snapshot!(format_err(input), @r###" assert_snapshot!(format_err(input), @r###"
error: Failed to parse `pyproject.toml` error: Failed to parse `pyproject.toml`
Caused by: Failed to parse entry for: `tqdm` Caused by: Failed to parse entry for: `tqdm`
Caused by: The package is not included as workspace package in `tool.uv.workspace` Caused by: Package is not included as workspace package in `tool.uv.workspace`
"###); "###);
} }
@ -882,7 +884,7 @@ mod test {
assert_snapshot!(format_err(input), @r###" assert_snapshot!(format_err(input), @r###"
error: Failed to parse `pyproject.toml` error: Failed to parse `pyproject.toml`
Caused by: You need to specify a `[project]` section to use `[tool.uv.sources]` Caused by: Must specify a `[project]` section alongside `[tool.uv.sources]`
"###); "###);
} }
} }

14
uv.schema.json generated
View file

@ -596,7 +596,7 @@
"description": "A `tool.uv.sources` value.", "description": "A `tool.uv.sources` value.",
"anyOf": [ "anyOf": [
{ {
"description": "A remote git repository, either over HTTPS or over SSH.\n\nExample: ```toml flask = { git = \"https://github.com/pallets/flask\", tag = \"3.0.0\" } ```", "description": "A remote Git repository, available over HTTPS or SSH.\n\nExample: ```toml flask = { git = \"https://github.com/pallets/flask\", tag = \"3.0.0\" } ```",
"type": "object", "type": "object",
"required": [ "required": [
"git" "git"
@ -609,6 +609,7 @@
] ]
}, },
"git": { "git": {
"description": "The repository URL (without the `git+` prefix).",
"type": "string", "type": "string",
"format": "uri" "format": "uri"
}, },
@ -619,7 +620,7 @@
] ]
}, },
"subdirectory": { "subdirectory": {
"description": "The path to the directory with the `pyproject.toml` if it is not in the archive root.", "description": "The path to the directory with the `pyproject.toml`, if it's not in the archive root.",
"type": [ "type": [
"string", "string",
"null" "null"
@ -642,7 +643,7 @@
], ],
"properties": { "properties": {
"subdirectory": { "subdirectory": {
"description": "For source distributions, the path to the directory with the `pyproject.toml` if it is 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": [ "type": [
"string", "string",
"null" "null"
@ -656,7 +657,7 @@
"additionalProperties": false "additionalProperties": false
}, },
{ {
"description": "The path to a dependency. It can either be a wheel (a `.whl` file), a source distribution as archive (a `.zip` or `.tag.gz` file) or a source distribution as directory (a directory with a pyproject.toml in, or a legacy directory with only a setup.py but non pyproject.toml in it).", "description": "The path to a dependency, either a wheel (a `.whl` file), source distribution (a `.zip` or `.tag.gz` file), or source tree (i.e., a directory containing a `pyproject.toml` or `setup.py` file in the root).",
"type": "object", "type": "object",
"required": [ "required": [
"path" "path"
@ -676,7 +677,7 @@
"additionalProperties": false "additionalProperties": false
}, },
{ {
"description": "When using a version as requirement, you can optionally pin the requirement to an index you defined, e.g. `torch` after configuring `torch` to `https://download.pytorch.org/whl/cu118`.", "description": "A dependency pinned to a specific index, e.g., `torch` after setting `torch` to `https://download.pytorch.org/whl/cu118`.",
"type": "object", "type": "object",
"required": [ "required": [
"index" "index"
@ -703,13 +704,14 @@
] ]
}, },
"workspace": { "workspace": {
"description": "When set to `false`, the package will be fetched from the remote index, rather than included as a workspace package.",
"type": "boolean" "type": "boolean"
} }
}, },
"additionalProperties": false "additionalProperties": false
}, },
{ {
"description": "Show a better error message for invalid combinations of options.", "description": "A catch-all variant used to emit precise error messages when deserializing.",
"type": "object", "type": "object",
"required": [ "required": [
"git", "git",