uv_pypi_types/metadata/
metadata23.rs

1//! Vendored from <https://github.com/PyO3/python-pkginfo-rs>
2
3use std::fmt::Display;
4use std::fmt::Write;
5use std::str;
6use std::str::FromStr;
7
8use crate::MetadataError;
9use crate::metadata::Headers;
10
11/// Code Metadata 2.3 as specified in
12/// <https://packaging.python.org/specifications/core-metadata/>.
13#[derive(Debug, Clone, Default, PartialEq, Eq)]
14pub struct Metadata23 {
15    /// Version of the file format; legal values are `1.0`, `1.1`, `1.2`, `2.1`, `2.2`, `2.3` and
16    /// `2.4`.
17    pub metadata_version: String,
18    /// The name of the distribution.
19    pub name: String,
20    /// A string containing the distribution's version number.
21    pub version: String,
22    /// A Platform specification describing an operating system supported by the distribution
23    /// which is not listed in the “Operating System” Trove classifiers.
24    pub platforms: Vec<String>,
25    /// Binary distributions containing a PKG-INFO file will use the Supported-Platform field
26    /// in their metadata to specify the OS and CPU for which the binary distribution was compiled.
27    pub supported_platforms: Vec<String>,
28    /// A one-line summary of what the distribution does.
29    pub summary: Option<String>,
30    /// A longer description of the distribution that can run to several paragraphs.
31    pub description: Option<String>,
32    /// A string stating the markup syntax (if any) used in the distribution's description,
33    /// so that tools can intelligently render the description.
34    ///
35    /// Known values: `text/plain`, `text/markdown` and `text/x-rst`.
36    pub description_content_type: Option<String>,
37    /// A list of additional keywords, separated by commas, to be used to
38    /// assist searching for the distribution in a larger catalog.
39    pub keywords: Option<String>,
40    /// A string containing the URL for the distribution's home page.
41    ///
42    /// Deprecated by PEP 753.
43    pub home_page: Option<String>,
44    /// A string containing the URL from which this version of the distribution can be downloaded.
45    ///
46    /// Deprecated by PEP 753.
47    pub download_url: Option<String>,
48    /// A string containing the author's name at a minimum; additional contact information may be
49    /// provided.
50    pub author: Option<String>,
51    /// A string containing the author's e-mail address. It can contain a name and e-mail address in
52    /// the legal forms for an RFC-822 `From:` header.
53    pub author_email: Option<String>,
54    /// A string containing the maintainer's name at a minimum; additional contact information may
55    /// be provided.
56    ///
57    /// Note that this field is intended for use when a project is being maintained by someone other
58    /// than the original author:
59    /// it should be omitted if it is identical to `author`.
60    pub maintainer: Option<String>,
61    /// A string containing the maintainer's e-mail address.
62    /// It can contain a name and e-mail address in the legal forms for a RFC-822 `From:` header.
63    ///
64    /// Note that this field is intended for use when a project is being maintained by someone other
65    /// than the original author: it should be omitted if it is identical to `author_email`.
66    pub maintainer_email: Option<String>,
67    /// Text indicating the license covering the distribution where the license is not a selection
68    /// from the `License` Trove classifiers or an SPDX license expression.
69    pub license: Option<String>,
70    /// An SPDX expression indicating the license covering the distribution.
71    ///
72    /// Introduced by PEP 639, requires metadata version 2.4.
73    pub license_expression: Option<String>,
74    /// Paths to files containing the text of the licenses covering the distribution.
75    ///
76    /// Introduced by PEP 639, requires metadata version 2.4.
77    pub license_files: Vec<String>,
78    /// Each entry is a string giving a single classification value for the distribution.
79    pub classifiers: Vec<String>,
80    /// Each entry contains a string naming some other distutils project required by this
81    /// distribution.
82    pub requires_dist: Vec<String>,
83    /// Each entry contains a string naming a Distutils project which is contained within this
84    /// distribution.
85    pub provides_dist: Vec<String>,
86    /// Each entry contains a string describing a distutils project's distribution which this
87    /// distribution renders obsolete,
88    /// meaning that the two projects should not be installed at the same time.
89    pub obsoletes_dist: Vec<String>,
90    /// This field specifies the Python version(s) that the distribution is guaranteed to be
91    /// compatible with.
92    pub requires_python: Option<String>,
93    /// Each entry contains a string describing some dependency in the system that the distribution
94    /// is to be used.
95    pub requires_external: Vec<String>,
96    /// A string containing a browsable URL for the project and a label for it, separated by a
97    /// comma.
98    pub project_urls: Vec<String>,
99    /// A string containing the name of an optional feature. Must be a valid Python identifier.
100    /// May be used to make a dependency conditional on whether the optional feature has been
101    /// requested.
102    pub provides_extra: Vec<String>,
103    /// A string containing the name of another core metadata field.
104    pub dynamic: Vec<String>,
105}
106
107impl Metadata23 {
108    /// Parse distribution metadata from metadata `MetadataError`
109    pub fn parse(content: &[u8]) -> Result<Self, MetadataError> {
110        let headers = Headers::parse(content)?;
111
112        let metadata_version = headers
113            .get_first_value("Metadata-Version")
114            .ok_or(MetadataError::FieldNotFound("Metadata-Version"))?;
115        let name = headers
116            .get_first_value("Name")
117            .ok_or(MetadataError::FieldNotFound("Name"))?;
118        let version = headers
119            .get_first_value("Version")
120            .ok_or(MetadataError::FieldNotFound("Version"))?;
121        let platforms = headers.get_all_values("Platform").collect();
122        let supported_platforms = headers.get_all_values("Supported-Platform").collect();
123        let summary = headers.get_first_value("Summary");
124        let body = str::from_utf8(&content[headers.body_start..])
125            .map_err(MetadataError::DescriptionEncoding)?;
126        let description = if body.trim().is_empty() {
127            headers.get_first_value("Description")
128        } else {
129            Some(body.to_string())
130        };
131        let keywords = headers.get_first_value("Keywords");
132        let home_page = headers.get_first_value("Home-Page");
133        let download_url = headers.get_first_value("Download-URL");
134        let author = headers.get_first_value("Author");
135        let author_email = headers.get_first_value("Author-email");
136        let license = headers.get_first_value("License");
137        let license_expression = headers.get_first_value("License-Expression");
138        let license_files = headers.get_all_values("License-File").collect();
139        let classifiers = headers.get_all_values("Classifier").collect();
140        let requires_dist = headers.get_all_values("Requires-Dist").collect();
141        let provides_dist = headers.get_all_values("Provides-Dist").collect();
142        let obsoletes_dist = headers.get_all_values("Obsoletes-Dist").collect();
143        let maintainer = headers.get_first_value("Maintainer");
144        let maintainer_email = headers.get_first_value("Maintainer-email");
145        let requires_python = headers.get_first_value("Requires-Python");
146        let requires_external = headers.get_all_values("Requires-External").collect();
147        let project_urls = headers.get_all_values("Project-URL").collect();
148        let provides_extra = headers.get_all_values("Provides-Extra").collect();
149        let description_content_type = headers.get_first_value("Description-Content-Type");
150        let dynamic = headers.get_all_values("Dynamic").collect();
151        Ok(Self {
152            metadata_version,
153            name,
154            version,
155            platforms,
156            supported_platforms,
157            summary,
158            description,
159            description_content_type,
160            keywords,
161            home_page,
162            download_url,
163            author,
164            author_email,
165            maintainer,
166            maintainer_email,
167            license,
168            license_expression,
169            license_files,
170            classifiers,
171            requires_dist,
172            provides_dist,
173            obsoletes_dist,
174            requires_python,
175            requires_external,
176            project_urls,
177            provides_extra,
178            dynamic,
179        })
180    }
181
182    /// Convert to the pseudo-email format used by Python's METADATA.
183    ///
184    /// > The standard file format for metadata (including in wheels and installed projects) is
185    /// > based on the format of email headers. However, email formats have been revised several
186    /// > times, and exactly which email RFC applies to packaging metadata is not specified. In the
187    /// > absence of a precise definition, the practical standard is set by what the standard
188    /// > library `email.parser` module can parse using the `compat32` policy.
189    /// - <https://packaging.python.org/en/latest/specifications/core-metadata/#core-metadata-specifications>
190    ///
191    /// # Example
192    ///
193    /// ```text
194    /// Metadata-Version: 2.3
195    /// Name: hello-world
196    /// Version: 0.1.0
197    /// License: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
198    ///          INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A [...]
199    /// ```
200    pub fn core_metadata_format(&self) -> String {
201        fn write_str(writer: &mut String, key: &str, value: impl Display) {
202            let value = value.to_string();
203            let mut lines = value.lines();
204            if let Some(line) = lines.next() {
205                let _ = writeln!(writer, "{key}: {line}");
206            } else {
207                // The value is an empty string
208                let _ = writeln!(writer, "{key}: ");
209            }
210            for line in lines {
211                // Python implementations vary
212                // https://github.com/pypa/pyproject-metadata/pull/150/files#diff-7d938dbc255a08c2cfab1b4f1f8d1f6519c9312dd0a39d7793fa778474f1fbd1L135-R141
213                let _ = writeln!(writer, "{}{}", " ".repeat(key.len() + 2), line);
214            }
215        }
216        fn write_opt_str(writer: &mut String, key: &str, value: Option<&impl Display>) {
217            if let Some(value) = value {
218                write_str(writer, key, value);
219            }
220        }
221        fn write_all(
222            writer: &mut String,
223            key: &str,
224            values: impl IntoIterator<Item = impl Display>,
225        ) {
226            for value in values {
227                write_str(writer, key, value);
228            }
229        }
230
231        let mut writer = String::new();
232        write_str(&mut writer, "Metadata-Version", &self.metadata_version);
233        write_str(&mut writer, "Name", &self.name);
234        write_str(&mut writer, "Version", &self.version);
235        write_all(&mut writer, "Platform", &self.platforms);
236        write_all(&mut writer, "Supported-Platform", &self.supported_platforms);
237        write_all(&mut writer, "Summary", &self.summary);
238        write_opt_str(&mut writer, "Keywords", self.keywords.as_ref());
239        write_opt_str(&mut writer, "Home-Page", self.home_page.as_ref());
240        write_opt_str(&mut writer, "Download-URL", self.download_url.as_ref());
241        write_opt_str(&mut writer, "Author", self.author.as_ref());
242        write_opt_str(&mut writer, "Author-email", self.author_email.as_ref());
243        write_opt_str(&mut writer, "License", self.license.as_ref());
244        write_opt_str(
245            &mut writer,
246            "License-Expression",
247            self.license_expression.as_ref(),
248        );
249        write_all(&mut writer, "License-File", &self.license_files);
250        write_all(&mut writer, "Classifier", &self.classifiers);
251        write_all(&mut writer, "Requires-Dist", &self.requires_dist);
252        write_all(&mut writer, "Provides-Dist", &self.provides_dist);
253        write_all(&mut writer, "Obsoletes-Dist", &self.obsoletes_dist);
254        write_opt_str(&mut writer, "Maintainer", self.maintainer.as_ref());
255        write_opt_str(
256            &mut writer,
257            "Maintainer-email",
258            self.maintainer_email.as_ref(),
259        );
260        write_opt_str(
261            &mut writer,
262            "Requires-Python",
263            self.requires_python.as_ref(),
264        );
265        write_all(&mut writer, "Requires-External", &self.requires_external);
266        write_all(&mut writer, "Project-URL", &self.project_urls);
267        write_all(&mut writer, "Provides-Extra", &self.provides_extra);
268        write_opt_str(
269            &mut writer,
270            "Description-Content-Type",
271            self.description_content_type.as_ref(),
272        );
273        write_all(&mut writer, "Dynamic", &self.dynamic);
274
275        if let Some(description) = &self.description {
276            writer.push('\n');
277            writer.push_str(description);
278        }
279        writer
280    }
281}
282
283impl FromStr for Metadata23 {
284    type Err = MetadataError;
285
286    fn from_str(s: &str) -> Result<Self, Self::Err> {
287        Self::parse(s.as_bytes())
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294    use crate::MetadataError;
295
296    #[test]
297    fn test_parse_from_str() {
298        let s = "Metadata-Version: 1.0";
299        let meta: Result<Metadata23, MetadataError> = s.parse();
300        assert!(matches!(meta, Err(MetadataError::FieldNotFound("Name"))));
301
302        let s = "Metadata-Version: 1.0\nName: asdf";
303        let meta = Metadata23::parse(s.as_bytes());
304        assert!(matches!(meta, Err(MetadataError::FieldNotFound("Version"))));
305
306        let s = "Metadata-Version: 1.0\nName: asdf\nVersion: 1.0";
307        let meta = Metadata23::parse(s.as_bytes()).unwrap();
308        assert_eq!(meta.metadata_version, "1.0");
309        assert_eq!(meta.name, "asdf");
310        assert_eq!(meta.version, "1.0");
311
312        let s = "Metadata-Version: 1.0\nName: asdf\nVersion: 1.0\nDescription: a Python package";
313        let meta: Metadata23 = s.parse().unwrap();
314        assert_eq!(meta.description.as_deref(), Some("a Python package"));
315
316        let s = "Metadata-Version: 1.0\nName: asdf\nVersion: 1.0\n\na Python package";
317        let meta: Metadata23 = s.parse().unwrap();
318        assert_eq!(meta.description.as_deref(), Some("a Python package"));
319
320        let s = "Metadata-Version: 1.0\nName: asdf\nVersion: 1.0\nAuthor: 中文\n\n一个 Python 包";
321        let meta: Metadata23 = s.parse().unwrap();
322        assert_eq!(meta.author.as_deref(), Some("中文"));
323        assert_eq!(meta.description.as_deref(), Some("一个 Python 包"));
324    }
325}