uv_pypi_types/metadata/
mod.rs

1mod build_requires;
2mod metadata10;
3mod metadata23;
4mod metadata_resolver;
5mod pyproject_toml;
6mod requires_dist;
7mod requires_txt;
8
9use std::str::Utf8Error;
10
11use mailparse::{MailHeaderMap, MailParseError};
12use thiserror::Error;
13
14use uv_normalize::InvalidNameError;
15use uv_pep440::{VersionParseError, VersionSpecifiersParseError};
16use uv_pep508::Pep508Error;
17
18use crate::VerbatimParsedUrl;
19
20pub use build_requires::BuildRequires;
21pub use metadata_resolver::ResolutionMetadata;
22pub use metadata10::Metadata10;
23pub use metadata23::Metadata23;
24pub use pyproject_toml::PyProjectToml;
25pub use requires_dist::RequiresDist;
26pub use requires_txt::RequiresTxt;
27
28/// <https://github.com/PyO3/python-pkginfo-rs/blob/d719988323a0cfea86d4737116d7917f30e819e2/src/error.rs>
29///
30/// The error type
31#[derive(Error, Debug)]
32pub enum MetadataError {
33    #[error(transparent)]
34    MailParse(#[from] MailParseError),
35    #[error("Invalid `pyproject.toml`")]
36    InvalidPyprojectTomlSyntax(#[source] toml_edit::TomlError),
37    #[error(transparent)]
38    InvalidPyprojectTomlSchema(toml_edit::de::Error),
39    #[error(
40        "`pyproject.toml` is using the `[project]` table, but the required `project.name` field is not set"
41    )]
42    MissingName,
43    #[error("Metadata field {0} not found")]
44    FieldNotFound(&'static str),
45    #[error("Invalid version: {0}")]
46    Pep440VersionError(VersionParseError),
47    #[error(transparent)]
48    Pep440Error(#[from] VersionSpecifiersParseError),
49    #[error(transparent)]
50    Pep508Error(#[from] Box<Pep508Error<VerbatimParsedUrl>>),
51    #[error(transparent)]
52    InvalidName(#[from] InvalidNameError),
53    #[error("Invalid `Metadata-Version` field: {0}")]
54    InvalidMetadataVersion(String),
55    #[error("Reading metadata from `PKG-INFO` requires Metadata 2.2 or later (found: {0})")]
56    UnsupportedMetadataVersion(String),
57    #[error("The following field was marked as dynamic: {0}")]
58    DynamicField(&'static str),
59    #[error(
60        "The project uses Poetry's syntax to declare its dependencies, despite including a `project` table in `pyproject.toml`"
61    )]
62    PoetrySyntax,
63    #[error("Failed to read `requires.txt` contents")]
64    RequiresTxtContents(#[from] std::io::Error),
65    #[error("The description is not valid utf-8")]
66    DescriptionEncoding(#[source] Utf8Error),
67}
68
69impl From<Pep508Error<VerbatimParsedUrl>> for MetadataError {
70    fn from(error: Pep508Error<VerbatimParsedUrl>) -> Self {
71        Self::Pep508Error(Box::new(error))
72    }
73}
74
75/// The headers of a distribution metadata file.
76#[derive(Debug)]
77struct Headers<'a> {
78    headers: Vec<mailparse::MailHeader<'a>>,
79    body_start: usize,
80}
81
82impl<'a> Headers<'a> {
83    /// Parse the headers from the given metadata file content.
84    fn parse(content: &'a [u8]) -> Result<Self, MailParseError> {
85        let (headers, body_start) = mailparse::parse_headers(content)?;
86        Ok(Self {
87            headers,
88            body_start,
89        })
90    }
91
92    /// Return the first value associated with the header with the given name.
93    fn get_first_value(&self, name: &str) -> Option<String> {
94        self.headers.get_first_header(name).and_then(|header| {
95            let value = header.get_value();
96            if value == "UNKNOWN" {
97                None
98            } else {
99                Some(value)
100            }
101        })
102    }
103
104    /// Return all values associated with the header with the given name.
105    fn get_all_values(&self, name: &str) -> impl Iterator<Item = String> {
106        self.headers
107            .get_all_values(name)
108            .into_iter()
109            .filter(|value| value != "UNKNOWN")
110    }
111}
112
113/// Parse a `Metadata-Version` field into a (major, minor) tuple.
114fn parse_version(metadata_version: &str) -> Result<(u8, u8), MetadataError> {
115    let (major, minor) =
116        metadata_version
117            .split_once('.')
118            .ok_or(MetadataError::InvalidMetadataVersion(
119                metadata_version.to_string(),
120            ))?;
121    let major = major
122        .parse::<u8>()
123        .map_err(|_| MetadataError::InvalidMetadataVersion(metadata_version.to_string()))?;
124    let minor = minor
125        .parse::<u8>()
126        .map_err(|_| MetadataError::InvalidMetadataVersion(metadata_version.to_string()))?;
127    Ok((major, minor))
128}