mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 10:58:28 +00:00
Improve static metadata extraction for Poetry projects (#4182)
## Summary Adds handling for a few cases to improve interoperability with Poetry: - If the `project` schema is invalid, we now raise a hard error, rather than treating the metadata as dynamic and then falling back to the build backend. This could cause problems, I'm not sure. It's stricter than before. - If the project contains `tool.poetry` but omits `project.dependencies`, we now treat it as dynamic. We could go even further and treat _any_ Poetry project as dynamic, but then we'd be ignoring user-declared dependencies, which is also confusing. Closes https://github.com/astral-sh/uv/issues/4142.
This commit is contained in:
parent
c6da4f15b7
commit
125a4b220e
4 changed files with 166 additions and 14 deletions
|
@ -60,6 +60,8 @@ pub enum MetadataError {
|
||||||
UnsupportedMetadataVersion(String),
|
UnsupportedMetadataVersion(String),
|
||||||
#[error("The following field was marked as dynamic: {0}")]
|
#[error("The following field was marked as dynamic: {0}")]
|
||||||
DynamicField(&'static str),
|
DynamicField(&'static str),
|
||||||
|
#[error("The project uses Poetry's syntax to declare its dependencies, despite including a `project` table in `pyproject.toml`")]
|
||||||
|
PoetrySyntax,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Pep508Error<VerbatimParsedUrl>> for MetadataError {
|
impl From<Pep508Error<VerbatimParsedUrl>> for MetadataError {
|
||||||
|
@ -210,6 +212,15 @@ impl Metadata23 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If dependencies are declared with Poetry, and `project.dependencies` is omitted, treat
|
||||||
|
// the dependencies as dynamic. The inclusion of a `project` table without defining
|
||||||
|
// `project.dependencies` is almost certainly an error.
|
||||||
|
if project.dependencies.is_none()
|
||||||
|
&& pyproject_toml.tool.and_then(|tool| tool.poetry).is_some()
|
||||||
|
{
|
||||||
|
return Err(MetadataError::PoetrySyntax);
|
||||||
|
}
|
||||||
|
|
||||||
let name = project.name;
|
let name = project.name;
|
||||||
let version = project
|
let version = project
|
||||||
.version
|
.version
|
||||||
|
@ -257,11 +268,11 @@ impl Metadata23 {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A `pyproject.toml` as specified in PEP 517.
|
/// A `pyproject.toml` as specified in PEP 517.
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
#[derive(Deserialize, Debug)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
struct PyProjectToml {
|
struct PyProjectToml {
|
||||||
/// Project metadata
|
|
||||||
project: Option<Project>,
|
project: Option<Project>,
|
||||||
|
tool: Option<Tool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// PEP 621 project metadata.
|
/// PEP 621 project metadata.
|
||||||
|
@ -270,7 +281,7 @@ struct PyProjectToml {
|
||||||
/// relevant for dependency resolution.
|
/// relevant for dependency resolution.
|
||||||
///
|
///
|
||||||
/// See <https://packaging.python.org/en/latest/specifications/pyproject-toml>.
|
/// See <https://packaging.python.org/en/latest/specifications/pyproject-toml>.
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
#[derive(Deserialize, Debug)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
struct Project {
|
struct Project {
|
||||||
/// The name of the project
|
/// The name of the project
|
||||||
|
@ -288,6 +299,17 @@ struct Project {
|
||||||
dynamic: Option<Vec<String>>,
|
dynamic: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
struct Tool {
|
||||||
|
poetry: Option<ToolPoetry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
#[allow(clippy::empty_structs_with_brackets)]
|
||||||
|
struct ToolPoetry {}
|
||||||
|
|
||||||
/// Python Package Metadata 1.0 and later as specified in
|
/// Python Package Metadata 1.0 and later as specified in
|
||||||
/// <https://peps.python.org/pep-0241/>.
|
/// <https://peps.python.org/pep-0241/>.
|
||||||
///
|
///
|
||||||
|
@ -369,6 +391,15 @@ impl RequiresDist {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If dependencies are declared with Poetry, and `project.dependencies` is omitted, treat
|
||||||
|
// the dependencies as dynamic. The inclusion of a `project` table without defining
|
||||||
|
// `project.dependencies` is almost certainly an error.
|
||||||
|
if project.dependencies.is_none()
|
||||||
|
&& pyproject_toml.tool.and_then(|tool| tool.poetry).is_some()
|
||||||
|
{
|
||||||
|
return Err(MetadataError::PoetrySyntax);
|
||||||
|
}
|
||||||
|
|
||||||
let name = project.name;
|
let name = project.name;
|
||||||
|
|
||||||
// Extract the requirements.
|
// Extract the requirements.
|
||||||
|
|
|
@ -70,12 +70,12 @@ pub enum Error {
|
||||||
NotFound(PathBuf),
|
NotFound(PathBuf),
|
||||||
#[error("The source distribution is missing a `PKG-INFO` file")]
|
#[error("The source distribution is missing a `PKG-INFO` file")]
|
||||||
MissingPkgInfo,
|
MissingPkgInfo,
|
||||||
#[error("The source distribution does not support static metadata in `PKG-INFO`")]
|
#[error("Failed to extract static metadata from `PKG-INFO`")]
|
||||||
DynamicPkgInfo(#[source] pypi_types::MetadataError),
|
PkgInfo(#[source] pypi_types::MetadataError),
|
||||||
#[error("The source distribution is missing a `pyproject.toml` file")]
|
#[error("The source distribution is missing a `pyproject.toml` file")]
|
||||||
MissingPyprojectToml,
|
MissingPyprojectToml,
|
||||||
#[error("The source distribution does not support static metadata in `pyproject.toml`")]
|
#[error("Failed to extract static metadata from `pyproject.toml`")]
|
||||||
DynamicPyprojectToml(#[source] pypi_types::MetadataError),
|
PyprojectToml(#[source] pypi_types::MetadataError),
|
||||||
#[error("Unsupported scheme in URL: {0}")]
|
#[error("Unsupported scheme in URL: {0}")]
|
||||||
UnsupportedScheme(String),
|
UnsupportedScheme(String),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
|
|
|
@ -1411,7 +1411,16 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
||||||
|
|
||||||
return Ok(Some(metadata));
|
return Ok(Some(metadata));
|
||||||
}
|
}
|
||||||
Err(err @ (Error::MissingPkgInfo | Error::DynamicPkgInfo(_))) => {
|
Err(
|
||||||
|
err @ (Error::MissingPkgInfo
|
||||||
|
| Error::PkgInfo(
|
||||||
|
pypi_types::MetadataError::Pep508Error(_)
|
||||||
|
| pypi_types::MetadataError::DynamicField(_)
|
||||||
|
| pypi_types::MetadataError::FieldNotFound(_)
|
||||||
|
| pypi_types::MetadataError::UnsupportedMetadataVersion(_)
|
||||||
|
| pypi_types::MetadataError::PoetrySyntax,
|
||||||
|
)),
|
||||||
|
) => {
|
||||||
debug!("No static `PKG-INFO` available for: {source} ({err:?})");
|
debug!("No static `PKG-INFO` available for: {source} ({err:?})");
|
||||||
}
|
}
|
||||||
Err(err) => return Err(err),
|
Err(err) => return Err(err),
|
||||||
|
@ -1427,7 +1436,16 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
||||||
|
|
||||||
return Ok(Some(metadata));
|
return Ok(Some(metadata));
|
||||||
}
|
}
|
||||||
Err(err @ (Error::MissingPyprojectToml | Error::DynamicPyprojectToml(_))) => {
|
Err(
|
||||||
|
err @ (Error::MissingPyprojectToml
|
||||||
|
| Error::PyprojectToml(
|
||||||
|
pypi_types::MetadataError::Pep508Error(_)
|
||||||
|
| pypi_types::MetadataError::DynamicField(_)
|
||||||
|
| pypi_types::MetadataError::FieldNotFound(_)
|
||||||
|
| pypi_types::MetadataError::UnsupportedMetadataVersion(_)
|
||||||
|
| pypi_types::MetadataError::PoetrySyntax,
|
||||||
|
)),
|
||||||
|
) => {
|
||||||
debug!("No static `pyproject.toml` available for: {source} ({err:?})");
|
debug!("No static `pyproject.toml` available for: {source} ({err:?})");
|
||||||
}
|
}
|
||||||
Err(err) => return Err(err),
|
Err(err) => return Err(err),
|
||||||
|
@ -1602,7 +1620,7 @@ async fn read_pkg_info(
|
||||||
};
|
};
|
||||||
|
|
||||||
// Parse the metadata.
|
// Parse the metadata.
|
||||||
let metadata = Metadata23::parse_pkg_info(&content).map_err(Error::DynamicPkgInfo)?;
|
let metadata = Metadata23::parse_pkg_info(&content).map_err(Error::PkgInfo)?;
|
||||||
|
|
||||||
Ok(metadata)
|
Ok(metadata)
|
||||||
}
|
}
|
||||||
|
@ -1627,8 +1645,7 @@ async fn read_pyproject_toml(
|
||||||
};
|
};
|
||||||
|
|
||||||
// Parse the metadata.
|
// Parse the metadata.
|
||||||
let metadata =
|
let metadata = Metadata23::parse_pyproject_toml(&content).map_err(Error::PyprojectToml)?;
|
||||||
Metadata23::parse_pyproject_toml(&content).map_err(Error::DynamicPyprojectToml)?;
|
|
||||||
|
|
||||||
Ok(metadata)
|
Ok(metadata)
|
||||||
}
|
}
|
||||||
|
@ -1646,8 +1663,8 @@ async fn read_requires_dist(project_root: &Path) -> Result<pypi_types::RequiresD
|
||||||
};
|
};
|
||||||
|
|
||||||
// Parse the metadata.
|
// Parse the metadata.
|
||||||
let requires_dist = pypi_types::RequiresDist::parse_pyproject_toml(&content)
|
let requires_dist =
|
||||||
.map_err(Error::DynamicPyprojectToml)?;
|
pypi_types::RequiresDist::parse_pyproject_toml(&content).map_err(Error::PyprojectToml)?;
|
||||||
|
|
||||||
Ok(requires_dist)
|
Ok(requires_dist)
|
||||||
}
|
}
|
||||||
|
|
|
@ -615,6 +615,110 @@ build-backend = "poetry.core.masonry.api"
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Compile a `pyproject.toml` file with a `poetry` section and a `project` section without a
|
||||||
|
/// `dependencies` field, which should be treated as an empty list.
|
||||||
|
#[test]
|
||||||
|
fn compile_pyproject_toml_poetry_empty_dependencies() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||||
|
pyproject_toml.write_str(
|
||||||
|
r#"[project]
|
||||||
|
name = "poetry-editable"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = ""
|
||||||
|
authors = ["Astral Software Inc. <hey@astral.sh>"]
|
||||||
|
|
||||||
|
[tool.poetry]
|
||||||
|
name = "poetry-editable"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = ""
|
||||||
|
authors = ["Astral Software Inc. <hey@astral.sh>"]
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = "^3.10"
|
||||||
|
anyio = "^3"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.compile()
|
||||||
|
.arg("pyproject.toml"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
# This file was autogenerated by uv via the following command:
|
||||||
|
# uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z pyproject.toml
|
||||||
|
anyio==3.7.1
|
||||||
|
# via poetry-editable (pyproject.toml)
|
||||||
|
idna==3.6
|
||||||
|
# via anyio
|
||||||
|
sniffio==1.3.1
|
||||||
|
# via anyio
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 3 packages in [TIME]
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compile a `pyproject.toml` file with a `poetry` section and a `project` section with an invalid
|
||||||
|
/// `dependencies` field.
|
||||||
|
#[test]
|
||||||
|
fn compile_pyproject_toml_poetry_invalid_dependencies() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||||
|
pyproject_toml.write_str(
|
||||||
|
r#"[project]
|
||||||
|
name = "poetry-editable"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = ""
|
||||||
|
authors = ["Astral Software Inc. <hey@astral.sh>"]
|
||||||
|
|
||||||
|
[tool.poetry]
|
||||||
|
name = "poetry-editable"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = ""
|
||||||
|
authors = ["Astral Software Inc. <hey@astral.sh>"]
|
||||||
|
|
||||||
|
[project.dependencies]
|
||||||
|
python = "^3.12"
|
||||||
|
msgspec = "^0.18.4"
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = "^3.10"
|
||||||
|
anyio = "^3"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.compile()
|
||||||
|
.arg("pyproject.toml"), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
error: Failed to extract static metadata from `pyproject.toml`
|
||||||
|
Caused by: TOML parse error at line 13, column 1
|
||||||
|
|
|
||||||
|
13 | [project.dependencies]
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
invalid type: map, expected a sequence
|
||||||
|
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Compile a `pyproject.toml` file that uses setuptools as the build backend.
|
/// Compile a `pyproject.toml` file that uses setuptools as the build backend.
|
||||||
#[test]
|
#[test]
|
||||||
fn compile_pyproject_toml_setuptools() -> Result<()> {
|
fn compile_pyproject_toml_setuptools() -> Result<()> {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue