From c97427d530ad69e6ff60ecee6f6c6e574cdd06e1 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 5 Jun 2024 13:39:37 -0400 Subject: [PATCH] Reduce visibility of `lowering` (#4055) ## Summary This makes `lowering.rs` internal to the metadata package. --- crates/uv-distribution/src/lib.rs | 2 - crates/uv-distribution/src/metadata.rs | 137 ------ .../lowering.rs} | 262 ------------ crates/uv-distribution/src/metadata/mod.rs | 401 ++++++++++++++++++ crates/uv-distribution/src/pyproject.rs | 8 - 5 files changed, 401 insertions(+), 409 deletions(-) delete mode 100644 crates/uv-distribution/src/metadata.rs rename crates/uv-distribution/src/{requirement_lowering.rs => metadata/lowering.rs} (54%) create mode 100644 crates/uv-distribution/src/metadata/mod.rs diff --git a/crates/uv-distribution/src/lib.rs b/crates/uv-distribution/src/lib.rs index 3b0e08ea7..51261e98d 100644 --- a/crates/uv-distribution/src/lib.rs +++ b/crates/uv-distribution/src/lib.rs @@ -4,7 +4,6 @@ pub use error::Error; pub use index::{BuiltWheelIndex, RegistryWheelIndex}; pub use metadata::{ArchiveMetadata, Metadata}; pub use reporter::Reporter; -use requirement_lowering::LoweringError; pub use workspace::{ProjectWorkspace, Workspace, WorkspaceError, WorkspaceMember}; mod archive; @@ -16,6 +15,5 @@ mod locks; mod metadata; pub mod pyproject; mod reporter; -mod requirement_lowering; mod source; mod workspace; diff --git a/crates/uv-distribution/src/metadata.rs b/crates/uv-distribution/src/metadata.rs deleted file mode 100644 index d9f810ed0..000000000 --- a/crates/uv-distribution/src/metadata.rs +++ /dev/null @@ -1,137 +0,0 @@ -use std::collections::BTreeMap; -use std::path::Path; - -use thiserror::Error; - -use pep440_rs::{Version, VersionSpecifiers}; -use pypi_types::{HashDigest, Metadata23}; -use uv_configuration::PreviewMode; -use uv_normalize::{ExtraName, PackageName}; - -use crate::requirement_lowering::{lower_requirement, LoweringError}; -use crate::{ProjectWorkspace, WorkspaceError}; - -#[derive(Debug, Error)] -pub enum MetadataLoweringError { - #[error(transparent)] - Workspace(#[from] WorkspaceError), - #[error("Failed to parse entry for: `{0}`")] - LoweringError(PackageName, #[source] LoweringError), -} - -#[derive(Debug, Clone)] -pub struct Metadata { - // Mandatory fields - pub name: PackageName, - pub version: Version, - // Optional fields - pub requires_dist: Vec, - pub requires_python: Option, - pub provides_extras: Vec, -} - -impl Metadata { - /// Lower without considering `tool.uv` in `pyproject.toml`, used for index and other archive - /// dependencies. - pub fn from_metadata23(metadata: Metadata23) -> Self { - Self { - name: metadata.name, - version: metadata.version, - requires_dist: metadata - .requires_dist - .into_iter() - .map(pypi_types::Requirement::from) - .collect(), - requires_python: metadata.requires_python, - provides_extras: metadata.provides_extras, - } - } - - /// Lower by considering `tool.uv` in `pyproject.toml` if present, used for Git and directory - /// dependencies. - pub async fn from_workspace( - metadata: Metadata23, - project_root: &Path, - preview_mode: PreviewMode, - ) -> Result { - // TODO(konsti): Limit discovery for Git checkouts to Git root. - // TODO(konsti): Cache workspace discovery. - let Some(project_workspace) = - ProjectWorkspace::from_maybe_project_root(project_root, None).await? - else { - return Ok(Self::from_metadata23(metadata)); - }; - - Self::from_project_workspace(metadata, &project_workspace, preview_mode) - } - - pub fn from_project_workspace( - metadata: Metadata23, - project_workspace: &ProjectWorkspace, - preview_mode: PreviewMode, - ) -> Result { - let empty = BTreeMap::default(); - let sources = project_workspace - .current_project() - .pyproject_toml() - .tool - .as_ref() - .and_then(|tool| tool.uv.as_ref()) - .and_then(|uv| uv.sources.as_ref()) - .unwrap_or(&empty); - - let requires_dist = metadata - .requires_dist - .into_iter() - .map(|requirement| { - let requirement_name = requirement.name.clone(); - lower_requirement( - requirement, - &metadata.name, - project_workspace.project_root(), - sources, - project_workspace.workspace(), - preview_mode, - ) - .map_err(|err| MetadataLoweringError::LoweringError(requirement_name.clone(), err)) - }) - .collect::>()?; - - Ok(Self { - name: metadata.name, - version: metadata.version, - requires_dist, - requires_python: metadata.requires_python, - provides_extras: metadata.provides_extras, - }) - } -} - -/// The metadata associated with an archive. -#[derive(Debug, Clone)] -pub struct ArchiveMetadata { - /// The [`Metadata`] for the underlying distribution. - pub metadata: Metadata, - /// The hashes of the source or built archive. - pub hashes: Vec, -} - -impl ArchiveMetadata { - /// Lower without considering `tool.uv` in `pyproject.toml`, used for index and other archive - /// dependencies. - pub fn from_metadata23(metadata: Metadata23) -> Self { - Self { - metadata: Metadata::from_metadata23(metadata), - hashes: vec![], - } - } -} - -impl From for ArchiveMetadata { - fn from(metadata: Metadata) -> Self { - Self { - metadata, - hashes: vec![], - } - } -} diff --git a/crates/uv-distribution/src/requirement_lowering.rs b/crates/uv-distribution/src/metadata/lowering.rs similarity index 54% rename from crates/uv-distribution/src/requirement_lowering.rs rename to crates/uv-distribution/src/metadata/lowering.rs index e5dbc9e6c..e50968d0e 100644 --- a/crates/uv-distribution/src/requirement_lowering.rs +++ b/crates/uv-distribution/src/metadata/lowering.rs @@ -239,265 +239,3 @@ fn path_source( editable, }) } - -#[cfg(test)] -mod test { - use anyhow::Context; - use std::path::Path; - - use indoc::indoc; - use insta::assert_snapshot; - - use pypi_types::Metadata23; - use uv_configuration::PreviewMode; - - use crate::metadata::Metadata; - use crate::pyproject::PyProjectToml; - use crate::ProjectWorkspace; - - async fn metadata_from_pyproject_toml(contents: &str) -> anyhow::Result { - let pyproject_toml: PyProjectToml = toml::from_str(contents)?; - let path = Path::new("pyproject.toml"); - let project_workspace = ProjectWorkspace::from_project( - path, - pyproject_toml - .project - .as_ref() - .context("metadata field project not found")?, - &pyproject_toml, - Some(path), - ) - .await?; - let metadata = Metadata23::parse_pyproject_toml(contents)?; - Ok(Metadata::from_project_workspace( - metadata, - &project_workspace, - PreviewMode::Enabled, - )?) - } - - async fn format_err(input: &str) -> String { - let err = metadata_from_pyproject_toml(input).await.unwrap_err(); - let mut causes = err.chain(); - let mut message = String::new(); - message.push_str(&format!("error: {}\n", causes.next().unwrap())); - for err in causes { - message.push_str(&format!(" Caused by: {err}\n")); - } - message - } - - #[tokio::test] - async fn conflict_project_and_sources() { - let input = indoc! {r#" - [project] - name = "foo" - version = "0.0.0" - dependencies = [ - "tqdm @ git+https://github.com/tqdm/tqdm", - ] - [tool.uv.sources] - tqdm = { url = "https://files.pythonhosted.org/packages/a5/d6/502a859bac4ad5e274255576cd3e15ca273cdb91731bc39fb840dd422ee9/tqdm-4.66.0-py3-none-any.whl" } - "#}; - - assert_snapshot!(format_err(input).await, @r###" - error: Failed to parse entry for: `tqdm` - Caused by: Can't combine URLs from both `project.dependencies` and `tool.uv.sources` - "###); - } - - #[tokio::test] - async fn too_many_git_specs() { - let input = indoc! {r#" - [project] - name = "foo" - version = "0.0.0" - dependencies = [ - "tqdm", - ] - [tool.uv.sources] - tqdm = { git = "https://github.com/tqdm/tqdm", rev = "baaaaaab", tag = "v1.0.0" } - "#}; - - assert_snapshot!(format_err(input).await, @r###" - error: Failed to parse entry for: `tqdm` - Caused by: Can only specify one of: `rev`, `tag`, or `branch` - "###); - } - - #[tokio::test] - async fn too_many_git_typo() { - let input = indoc! {r#" - [project] - name = "foo" - version = "0.0.0" - dependencies = [ - "tqdm", - ] - [tool.uv.sources] - tqdm = { git = "https://github.com/tqdm/tqdm", ref = "baaaaaab" } - "#}; - - // TODO(konsti): This should tell you the set of valid fields - assert_snapshot!(format_err(input).await, @r###" - error: TOML parse error at line 8, column 8 - | - 8 | tqdm = { git = "https://github.com/tqdm/tqdm", ref = "baaaaaab" } - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - data did not match any variant of untagged enum Source - - "###); - } - - #[tokio::test] - async fn you_cant_mix_those() { - let input = indoc! {r#" - [project] - name = "foo" - version = "0.0.0" - dependencies = [ - "tqdm", - ] - [tool.uv.sources] - tqdm = { path = "tqdm", index = "torch" } - "#}; - - // TODO(konsti): This should tell you the set of valid fields - assert_snapshot!(format_err(input).await, @r###" - error: TOML parse error at line 8, column 8 - | - 8 | tqdm = { path = "tqdm", index = "torch" } - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - data did not match any variant of untagged enum Source - - "###); - } - - #[tokio::test] - async fn missing_constraint() { - let input = indoc! {r#" - [project] - name = "foo" - version = "0.0.0" - dependencies = [ - "tqdm", - ] - "#}; - assert!(metadata_from_pyproject_toml(input).await.is_ok()); - } - - #[tokio::test] - async fn invalid_syntax() { - let input = indoc! {r#" - [project] - name = "foo" - version = "0.0.0" - dependencies = [ - "tqdm ==4.66.0", - ] - [tool.uv.sources] - tqdm = { url = invalid url to tqdm-4.66.0-py3-none-any.whl" } - "#}; - - assert_snapshot!(format_err(input).await, @r###" - error: TOML parse error at line 8, column 16 - | - 8 | tqdm = { url = invalid url to tqdm-4.66.0-py3-none-any.whl" } - | ^ - invalid string - expected `"`, `'` - - "###); - } - - #[tokio::test] - async fn invalid_url() { - let input = indoc! {r#" - [project] - name = "foo" - version = "0.0.0" - dependencies = [ - "tqdm ==4.66.0", - ] - [tool.uv.sources] - tqdm = { url = "§invalid#+#*Ä" } - "#}; - - assert_snapshot!(format_err(input).await, @r###" - error: TOML parse error at line 8, column 8 - | - 8 | tqdm = { url = "§invalid#+#*Ä" } - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - data did not match any variant of untagged enum Source - - "###); - } - - #[tokio::test] - async fn workspace_and_url_spec() { - let input = indoc! {r#" - [project] - name = "foo" - version = "0.0.0" - dependencies = [ - "tqdm @ git+https://github.com/tqdm/tqdm", - ] - [tool.uv.sources] - tqdm = { workspace = true } - "#}; - - assert_snapshot!(format_err(input).await, @r###" - error: Failed to parse entry for: `tqdm` - Caused by: Can't combine URLs from both `project.dependencies` and `tool.uv.sources` - "###); - } - - #[tokio::test] - async fn missing_workspace_package() { - let input = indoc! {r#" - [project] - name = "foo" - version = "0.0.0" - dependencies = [ - "tqdm ==4.66.0", - ] - [tool.uv.sources] - tqdm = { workspace = true } - "#}; - - assert_snapshot!(format_err(input).await, @r###" - error: Failed to parse entry for: `tqdm` - Caused by: Package is not included as workspace package in `tool.uv.workspace` - "###); - } - - #[tokio::test] - async fn cant_be_dynamic() { - let input = indoc! {r#" - [project] - name = "foo" - version = "0.0.0" - dynamic = [ - "dependencies" - ] - [tool.uv.sources] - tqdm = { workspace = true } - "#}; - - assert_snapshot!(format_err(input).await, @r###" - error: The following field was marked as dynamic: dependencies - "###); - } - - #[tokio::test] - async fn missing_project_section() { - let input = indoc! {" - [tool.uv.sources] - tqdm = { workspace = true } - "}; - - assert_snapshot!(format_err(input).await, @r###" - error: metadata field project not found - "###); - } -} diff --git a/crates/uv-distribution/src/metadata/mod.rs b/crates/uv-distribution/src/metadata/mod.rs new file mode 100644 index 000000000..52303e2c9 --- /dev/null +++ b/crates/uv-distribution/src/metadata/mod.rs @@ -0,0 +1,401 @@ +mod lowering; + +use std::collections::BTreeMap; +use std::path::Path; + +use thiserror::Error; + +use pep440_rs::{Version, VersionSpecifiers}; +use pypi_types::{HashDigest, Metadata23}; +use uv_configuration::PreviewMode; +use uv_normalize::{ExtraName, PackageName}; + +use crate::metadata::lowering::{lower_requirement, LoweringError}; +use crate::{ProjectWorkspace, WorkspaceError}; + +#[derive(Debug, Error)] +pub enum MetadataLoweringError { + #[error(transparent)] + Workspace(#[from] WorkspaceError), + #[error("Failed to parse entry for: `{0}`")] + LoweringError(PackageName, #[source] LoweringError), +} + +#[derive(Debug, Clone)] +pub struct Metadata { + // Mandatory fields + pub name: PackageName, + pub version: Version, + // Optional fields + pub requires_dist: Vec, + pub requires_python: Option, + pub provides_extras: Vec, +} + +impl Metadata { + /// Lower without considering `tool.uv` in `pyproject.toml`, used for index and other archive + /// dependencies. + pub fn from_metadata23(metadata: Metadata23) -> Self { + Self { + name: metadata.name, + version: metadata.version, + requires_dist: metadata + .requires_dist + .into_iter() + .map(pypi_types::Requirement::from) + .collect(), + requires_python: metadata.requires_python, + provides_extras: metadata.provides_extras, + } + } + + /// Lower by considering `tool.uv` in `pyproject.toml` if present, used for Git and directory + /// dependencies. + pub async fn from_workspace( + metadata: Metadata23, + project_root: &Path, + preview_mode: PreviewMode, + ) -> Result { + // TODO(konsti): Limit discovery for Git checkouts to Git root. + // TODO(konsti): Cache workspace discovery. + let Some(project_workspace) = + ProjectWorkspace::from_maybe_project_root(project_root, None).await? + else { + return Ok(Self::from_metadata23(metadata)); + }; + + Self::from_project_workspace(metadata, &project_workspace, preview_mode) + } + + fn from_project_workspace( + metadata: Metadata23, + project_workspace: &ProjectWorkspace, + preview_mode: PreviewMode, + ) -> Result { + let empty = BTreeMap::default(); + let sources = project_workspace + .current_project() + .pyproject_toml() + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.sources.as_ref()) + .unwrap_or(&empty); + + let requires_dist = metadata + .requires_dist + .into_iter() + .map(|requirement| { + let requirement_name = requirement.name.clone(); + lower_requirement( + requirement, + &metadata.name, + project_workspace.project_root(), + sources, + project_workspace.workspace(), + preview_mode, + ) + .map_err(|err| MetadataLoweringError::LoweringError(requirement_name.clone(), err)) + }) + .collect::>()?; + + Ok(Self { + name: metadata.name, + version: metadata.version, + requires_dist, + requires_python: metadata.requires_python, + provides_extras: metadata.provides_extras, + }) + } +} + +/// The metadata associated with an archive. +#[derive(Debug, Clone)] +pub struct ArchiveMetadata { + /// The [`Metadata`] for the underlying distribution. + pub metadata: Metadata, + /// The hashes of the source or built archive. + pub hashes: Vec, +} + +impl ArchiveMetadata { + /// Lower without considering `tool.uv` in `pyproject.toml`, used for index and other archive + /// dependencies. + pub fn from_metadata23(metadata: Metadata23) -> Self { + Self { + metadata: Metadata::from_metadata23(metadata), + hashes: vec![], + } + } +} + +impl From for ArchiveMetadata { + fn from(metadata: Metadata) -> Self { + Self { + metadata, + hashes: vec![], + } + } +} + +#[cfg(test)] +mod test { + use anyhow::Context; + use std::path::Path; + + use indoc::indoc; + use insta::assert_snapshot; + + use pypi_types::Metadata23; + use uv_configuration::PreviewMode; + + use crate::metadata::Metadata; + use crate::pyproject::PyProjectToml; + use crate::ProjectWorkspace; + + async fn metadata_from_pyproject_toml(contents: &str) -> anyhow::Result { + let pyproject_toml: PyProjectToml = toml::from_str(contents)?; + let path = Path::new("pyproject.toml"); + let project_workspace = ProjectWorkspace::from_project( + path, + pyproject_toml + .project + .as_ref() + .context("metadata field project not found")?, + &pyproject_toml, + Some(path), + ) + .await?; + let metadata = Metadata23::parse_pyproject_toml(contents)?; + Ok(Metadata::from_project_workspace( + metadata, + &project_workspace, + PreviewMode::Enabled, + )?) + } + + async fn format_err(input: &str) -> String { + let err = metadata_from_pyproject_toml(input).await.unwrap_err(); + let mut causes = err.chain(); + let mut message = String::new(); + message.push_str(&format!("error: {}\n", causes.next().unwrap())); + for err in causes { + message.push_str(&format!(" Caused by: {err}\n")); + } + message + } + + #[tokio::test] + async fn conflict_project_and_sources() { + let input = indoc! {r#" + [project] + name = "foo" + version = "0.0.0" + dependencies = [ + "tqdm @ git+https://github.com/tqdm/tqdm", + ] + [tool.uv.sources] + tqdm = { url = "https://files.pythonhosted.org/packages/a5/d6/502a859bac4ad5e274255576cd3e15ca273cdb91731bc39fb840dd422ee9/tqdm-4.66.0-py3-none-any.whl" } + "#}; + + assert_snapshot!(format_err(input).await, @r###" + error: Failed to parse entry for: `tqdm` + Caused by: Can't combine URLs from both `project.dependencies` and `tool.uv.sources` + "###); + } + + #[tokio::test] + async fn too_many_git_specs() { + let input = indoc! {r#" + [project] + name = "foo" + version = "0.0.0" + dependencies = [ + "tqdm", + ] + [tool.uv.sources] + tqdm = { git = "https://github.com/tqdm/tqdm", rev = "baaaaaab", tag = "v1.0.0" } + "#}; + + assert_snapshot!(format_err(input).await, @r###" + error: Failed to parse entry for: `tqdm` + Caused by: Can only specify one of: `rev`, `tag`, or `branch` + "###); + } + + #[tokio::test] + async fn too_many_git_typo() { + let input = indoc! {r#" + [project] + name = "foo" + version = "0.0.0" + dependencies = [ + "tqdm", + ] + [tool.uv.sources] + tqdm = { git = "https://github.com/tqdm/tqdm", ref = "baaaaaab" } + "#}; + + // TODO(konsti): This should tell you the set of valid fields + assert_snapshot!(format_err(input).await, @r###" + error: TOML parse error at line 8, column 8 + | + 8 | tqdm = { git = "https://github.com/tqdm/tqdm", ref = "baaaaaab" } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + data did not match any variant of untagged enum Source + + "###); + } + + #[tokio::test] + async fn you_cant_mix_those() { + let input = indoc! {r#" + [project] + name = "foo" + version = "0.0.0" + dependencies = [ + "tqdm", + ] + [tool.uv.sources] + tqdm = { path = "tqdm", index = "torch" } + "#}; + + // TODO(konsti): This should tell you the set of valid fields + assert_snapshot!(format_err(input).await, @r###" + error: TOML parse error at line 8, column 8 + | + 8 | tqdm = { path = "tqdm", index = "torch" } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + data did not match any variant of untagged enum Source + + "###); + } + + #[tokio::test] + async fn missing_constraint() { + let input = indoc! {r#" + [project] + name = "foo" + version = "0.0.0" + dependencies = [ + "tqdm", + ] + "#}; + assert!(metadata_from_pyproject_toml(input).await.is_ok()); + } + + #[tokio::test] + async fn invalid_syntax() { + let input = indoc! {r#" + [project] + name = "foo" + version = "0.0.0" + dependencies = [ + "tqdm ==4.66.0", + ] + [tool.uv.sources] + tqdm = { url = invalid url to tqdm-4.66.0-py3-none-any.whl" } + "#}; + + assert_snapshot!(format_err(input).await, @r###" + error: TOML parse error at line 8, column 16 + | + 8 | tqdm = { url = invalid url to tqdm-4.66.0-py3-none-any.whl" } + | ^ + invalid string + expected `"`, `'` + + "###); + } + + #[tokio::test] + async fn invalid_url() { + let input = indoc! {r#" + [project] + name = "foo" + version = "0.0.0" + dependencies = [ + "tqdm ==4.66.0", + ] + [tool.uv.sources] + tqdm = { url = "§invalid#+#*Ä" } + "#}; + + assert_snapshot!(format_err(input).await, @r###" + error: TOML parse error at line 8, column 8 + | + 8 | tqdm = { url = "§invalid#+#*Ä" } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + data did not match any variant of untagged enum Source + + "###); + } + + #[tokio::test] + async fn workspace_and_url_spec() { + let input = indoc! {r#" + [project] + name = "foo" + version = "0.0.0" + dependencies = [ + "tqdm @ git+https://github.com/tqdm/tqdm", + ] + [tool.uv.sources] + tqdm = { workspace = true } + "#}; + + assert_snapshot!(format_err(input).await, @r###" + error: Failed to parse entry for: `tqdm` + Caused by: Can't combine URLs from both `project.dependencies` and `tool.uv.sources` + "###); + } + + #[tokio::test] + async fn missing_workspace_package() { + let input = indoc! {r#" + [project] + name = "foo" + version = "0.0.0" + dependencies = [ + "tqdm ==4.66.0", + ] + [tool.uv.sources] + tqdm = { workspace = true } + "#}; + + assert_snapshot!(format_err(input).await, @r###" + error: Failed to parse entry for: `tqdm` + Caused by: Package is not included as workspace package in `tool.uv.workspace` + "###); + } + + #[tokio::test] + async fn cant_be_dynamic() { + let input = indoc! {r#" + [project] + name = "foo" + version = "0.0.0" + dynamic = [ + "dependencies" + ] + [tool.uv.sources] + tqdm = { workspace = true } + "#}; + + assert_snapshot!(format_err(input).await, @r###" + error: The following field was marked as dynamic: dependencies + "###); + } + + #[tokio::test] + async fn missing_project_section() { + let input = indoc! {" + [tool.uv.sources] + tqdm = { workspace = true } + "}; + + assert_snapshot!(format_err(input).await, @r###" + error: metadata field project not found + "###); + } +} diff --git a/crates/uv-distribution/src/pyproject.rs b/crates/uv-distribution/src/pyproject.rs index 63a12baf7..e961c1aa7 100644 --- a/crates/uv-distribution/src/pyproject.rs +++ b/crates/uv-distribution/src/pyproject.rs @@ -19,18 +19,10 @@ use pep508_rs::Pep508Error; use pypi_types::VerbatimParsedUrl; use uv_normalize::{ExtraName, PackageName}; -use crate::LoweringError; - #[derive(Debug, Error)] pub enum Pep621Error { #[error(transparent)] Pep508(#[from] Box>), - #[error("Must specify a `[project]` section alongside `[tool.uv.sources]`")] - MissingProjectSection, - #[error("pyproject.toml section is declared as dynamic, but must be static: `{0}`")] - DynamicNotAllowed(&'static str), - #[error("Failed to parse entry for: `{0}`")] - LoweringError(PackageName, #[source] LoweringError), } impl From> for Pep621Error {