From 2a9c62e177f4c1bc00dcbf398d26d276979c110c Mon Sep 17 00:00:00 2001 From: messense Date: Sat, 29 May 2021 21:05:46 +0800 Subject: [PATCH 1/5] Use the `pyproject-toml` crate --- Cargo.lock | 11 +++++++++++ Cargo.toml | 1 + src/pyproject_toml.rs | 32 +++++++++++--------------------- 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 836fb3f95..e99781972 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -923,6 +923,7 @@ dependencies = [ "once_cell", "platform-info", "pretty_env_logger", + "pyproject-toml", "regex", "reqwest", "rpassword", @@ -1279,6 +1280,16 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "pyproject-toml" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e623b250f749d036a4a60f41fdc7bc0c214491a0241c889abdc18fd9ee9c155d" +dependencies = [ + "serde", + "toml", +] + [[package]] name = "quick-error" version = "1.2.3" diff --git a/Cargo.toml b/Cargo.toml index 9130157be..98be3ae80 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,6 +67,7 @@ toml_edit = "0.2.0" once_cell = "1.7.2" scroll = "0.10.2" target-lexicon = "0.12.0" +pyproject-toml = "0.1.0" [dev-dependencies] indoc = "1.0.3" diff --git a/src/pyproject_toml.rs b/src/pyproject_toml.rs index 70b58f4ca..70a7d2721 100644 --- a/src/pyproject_toml.rs +++ b/src/pyproject_toml.rs @@ -1,21 +1,9 @@ use anyhow::{format_err, Context, Result}; +use pyproject_toml::PyProjectToml as ProjectToml; use serde::{Deserialize, Serialize}; use std::fs; use std::path::Path; -/// The `[build-system]` section of a pyproject.toml as specified in PEP 517 -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "kebab-case")] -pub struct BuildSystem { - /// PEP 518: This key must have a value of a list of strings representing PEP 508 dependencies - /// required to execute the build system (currently that means what dependencies are required - /// to execute a setup.py file). - pub requires: Vec, - /// PEP 517: `build-backend` is a string naming a Python object that will be used to perform - /// the build - pub build_backend: String, -} - /// The `[tool]` section of a pyproject.toml #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "kebab-case")] @@ -34,14 +22,8 @@ pub struct ToolMaturin { #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "kebab-case")] pub struct PyProjectToml { - /// PEP 518: The [build-system] table is used to store build-related data. Initially only one - /// key of the table will be valid and is mandatory for the table: requires. This key must have - /// a value of a list of strings representing PEP 508 dependencies required to execute the - /// build system (currently that means what dependencies are required to execute a setup.py - /// file). - /// - /// We also mandate `build_backend` - pub build_system: BuildSystem, + #[serde(flatten)] + inner: ProjectToml, /// PEP 518: The `[tool]` table is where any tool related to your Python project, not just build /// tools, can have users specify configuration data as long as they use a sub-table within /// `[tool]`, e.g. the flit tool would store its configuration in `[tool.flit]`. @@ -50,6 +32,14 @@ pub struct PyProjectToml { pub tool: Option, } +impl std::ops::Deref for PyProjectToml { + type Target = ProjectToml; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + impl PyProjectToml { /// Returns the contents of a pyproject.toml with a `[build-system]` entry or an error /// From ae75e32cd0e1fea505dd2553a38b69733fc64424 Mon Sep 17 00:00:00 2001 From: messense Date: Sat, 29 May 2021 21:36:09 +0800 Subject: [PATCH 2/5] Merge python package metadata with pyproject.toml Co-authored-by: konstin --- Changelog.md | 2 + src/build_context.rs | 27 +--- src/build_options.rs | 2 - src/develop.rs | 7 +- src/main.rs | 2 +- src/metadata.rs | 219 ++++++++++++++++++++++++++- src/module_writer.rs | 25 ++- src/pyproject_toml.rs | 6 +- test-crates/pyo3-pure/Cargo.toml | 11 +- test-crates/pyo3-pure/pyproject.toml | 23 +++ 10 files changed, 269 insertions(+), 55 deletions(-) diff --git a/Changelog.md b/Changelog.md index e8364cbd3..4bdd92f38 100644 --- a/Changelog.md +++ b/Changelog.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unrelease +* Add support for reading metadata from [PEP 621](https://www.python.org/dev/peps/pep-0621/) project table in `pyproject.toml` in [#555](https://github.com/PyO3/maturin/pull/555) +* Users should migrate away from the old `[package.metadata.maturin]` table of `Cargo.toml` to this new `[project]` table of `pyproject.toml` * Add PEP 656 musllinux support in [#543](https://github.com/PyO3/maturin/pull/543) * `--manylinux` is now called `--compatibility` and supports musllinux * The pure rust install layout changed from just the shared library to a python module that reexports the shared library. This should have now observable consequences for users of the created wheel expect that `my_project.my_project` is now also importable (and equal to just `my_project`) diff --git a/src/build_context.rs b/src/build_context.rs index 5cc12e353..c9d238a69 100644 --- a/src/build_context.rs +++ b/src/build_context.rs @@ -14,7 +14,6 @@ use crate::Target; use anyhow::{anyhow, bail, Context, Result}; use cargo_metadata::Metadata; use fs_err as fs; -use std::collections::HashMap; use std::path::{Path, PathBuf}; /// The way the rust code is used in the wheel @@ -127,8 +126,6 @@ pub struct BuildContext { pub project_layout: ProjectLayout, /// Python Package Metadata 2.1 pub metadata21: Metadata21, - /// The `[console_scripts]` for the entry_points.txt - pub scripts: HashMap, /// The name of the crate pub crate_name: String, /// The name of the module can be distinct from the package name, mostly @@ -240,13 +237,7 @@ impl BuildContext { let platform = self.target.get_platform_tag(platform_tag, self.universal2); let tag = format!("cp{}{}-abi3-{}", major, min_minor, platform); - let mut writer = WheelWriter::new( - &tag, - &self.out, - &self.metadata21, - &self.scripts, - &[tag.clone()], - )?; + let mut writer = WheelWriter::new(&tag, &self.out, &self.metadata21, &[tag.clone()])?; write_bindings_module( &mut writer, @@ -301,13 +292,7 @@ impl BuildContext { ) -> Result { let tag = python_interpreter.get_tag(platform_tag, self.universal2); - let mut writer = WheelWriter::new( - &tag, - &self.out, - &self.metadata21, - &self.scripts, - &[tag.clone()], - )?; + let mut writer = WheelWriter::new(&tag, &self.out, &self.metadata21, &[tag.clone()])?; write_bindings_module( &mut writer, @@ -397,8 +382,7 @@ impl BuildContext { .target .get_universal_tags(platform_tag, self.universal2); - let mut builder = - WheelWriter::new(&tag, &self.out, &self.metadata21, &self.scripts, &tags)?; + let mut builder = WheelWriter::new(&tag, &self.out, &self.metadata21, &tags)?; write_cffi_module( &mut builder, @@ -436,12 +420,11 @@ impl BuildContext { .target .get_universal_tags(platform_tag, self.universal2); - if !self.scripts.is_empty() { + if !self.metadata21.scripts.is_empty() { bail!("Defining entrypoints and working with a binary doesn't mix well"); } - let mut builder = - WheelWriter::new(&tag, &self.out, &self.metadata21, &self.scripts, &tags)?; + let mut builder = WheelWriter::new(&tag, &self.out, &self.metadata21, &tags)?; match self.project_layout { ProjectLayout::Mixed { diff --git a/src/build_options.rs b/src/build_options.rs index 10695b110..2ad27da7e 100644 --- a/src/build_options.rs +++ b/src/build_options.rs @@ -133,7 +133,6 @@ impl BuildOptions { let metadata21 = Metadata21::from_cargo_toml(&cargo_toml, &manifest_dir) .context("Failed to parse Cargo.toml into python metadata")?; let extra_metadata = cargo_toml.remaining_core_metadata(); - let scripts = cargo_toml.scripts(); let crate_name = &cargo_toml.package.name; @@ -231,7 +230,6 @@ impl BuildOptions { bridge, project_layout, metadata21, - scripts, crate_name: crate_name.to_string(), module_name, manifest_path: self.manifest_path, diff --git a/src/develop.rs b/src/develop.rs index 25afea5a8..d8125ef77 100644 --- a/src/develop.rs +++ b/src/develop.rs @@ -178,12 +178,7 @@ pub fn develop( } }; - write_dist_info( - &mut writer, - &build_context.metadata21, - &build_context.scripts, - &tags, - )?; + write_dist_info(&mut writer, &build_context.metadata21, &tags)?; // https://packaging.python.org/specifications/recording-installed-packages/#the-installer-file writer.add_bytes( diff --git a/src/main.rs b/src/main.rs index 1fa492e5b..dffbc8d91 100644 --- a/src/main.rs +++ b/src/main.rs @@ -410,7 +410,7 @@ fn pep517(subcommand: Pep517Command) -> Result<()> { }; let mut writer = PathWriter::from_path(metadata_directory); - write_dist_info(&mut writer, &context.metadata21, &context.scripts, &tags)?; + write_dist_info(&mut writer, &context.metadata21, &tags)?; println!("{}", context.metadata21.get_dist_info_dir().display()); } Pep517Command::BuildWheel { diff --git a/src/metadata.rs b/src/metadata.rs index 7b643cfdc..480937ec6 100644 --- a/src/metadata.rs +++ b/src/metadata.rs @@ -1,5 +1,5 @@ -use crate::CargoToml; -use anyhow::{Context, Result}; +use crate::{CargoToml, PyProjectToml}; +use anyhow::{bail, Context, Result}; use fs_err as fs; use regex::Regex; use serde::{Deserialize, Serialize}; @@ -52,6 +52,9 @@ pub struct Metadata21 { pub requires_external: Vec, pub project_url: HashMap, pub provides_extra: Vec, + pub scripts: HashMap, + pub gui_scripts: HashMap, + pub entry_points: HashMap>, } const PLAINTEXT_CONTENT_TYPE: &str = "text/plain; charset=UTF-8"; @@ -76,6 +79,176 @@ fn path_to_content_type(path: &Path) -> String { } impl Metadata21 { + /// Merge metadata with pyproject.toml, where pyproject.toml takes precedence + /// + /// manifest_path must be the directory, not the file + fn merge_pyproject_toml(&mut self, manifest_path: impl AsRef) -> Result<()> { + let manifest_path = manifest_path.as_ref(); + if !manifest_path.join("pyproject.toml").is_file() { + return Ok(()); + } + let pyproject_toml = + PyProjectToml::new(manifest_path).context("pyproject.toml is invalid")?; + if let Some(project) = &pyproject_toml.project { + self.name = project.name.clone(); + + if let Some(version) = &project.version { + self.version = version.clone(); + } + + if let Some(description) = &project.description { + self.summary = Some(description.clone()); + } + + match &project.readme { + Some(pyproject_toml::ReadMe::RelativePath(readme_path)) => { + let readme_path = manifest_path.join(readme_path); + let description = Some(fs::read_to_string(&readme_path).context(format!( + "Failed to read readme specified in pyproject.toml, which should be at {}", + readme_path.display() + ))?); + self.description = description; + self.description_content_type = Some(path_to_content_type(&readme_path)); + } + Some(pyproject_toml::ReadMe::Table { + file, + text, + content_type, + }) => { + if file.is_some() && text.is_some() { + bail!("file and text fields of 'project.readme' are mutually-exclusive, only one of them should be specified"); + } + if let Some(readme_path) = file { + let readme_path = manifest_path.join(readme_path); + let description = Some(fs::read_to_string(&readme_path).context(format!( + "Failed to read readme specified in pyproject.toml, which should be at {}", + readme_path.display() + ))?); + self.description = description; + } + if let Some(description) = text { + self.description = Some(description.clone()); + } + self.description_content_type = content_type.clone(); + } + None => {} + } + + if let Some(requires_python) = &project.requires_python { + self.requires_python = Some(requires_python.clone()); + } + + if let Some(pyproject_toml::License { file, text }) = &project.license { + if file.is_some() && text.is_some() { + bail!("file and text fields of 'project.license' are mutually-exclusive, only one of them should be specified"); + } + if let Some(license_path) = file { + let license_path = manifest_path.join(license_path); + self.license = Some(fs::read_to_string(&license_path).context(format!( + "Failed to read license file specified in pyproject.toml, which should be at {}", + license_path.display() + ))?); + } + if let Some(license_text) = text { + self.license = Some(license_text.clone()); + } + } + + if let Some(authors) = &project.authors { + let mut names = Vec::with_capacity(authors.len()); + let mut emails = Vec::with_capacity(authors.len()); + for author in authors { + match (&author.name, &author.email) { + (Some(name), Some(email)) => { + emails.push(format!("{} <{}>", name, email)); + } + (Some(name), None) => { + names.push(name.as_str()); + } + (None, Some(email)) => { + emails.push(email.clone()); + } + (None, None) => {} + } + } + self.author = Some(names.join(", ")); + self.author_email = Some(emails.join(", ")); + } + + if let Some(maintainers) = &project.maintainers { + let mut names = Vec::with_capacity(maintainers.len()); + let mut emails = Vec::with_capacity(maintainers.len()); + for maintainer in maintainers { + match (&maintainer.name, &maintainer.email) { + (Some(name), Some(email)) => { + emails.push(format!("{} <{}>", name, email)); + } + (Some(name), None) => { + names.push(name.as_str()); + } + (None, Some(email)) => { + emails.push(email.clone()); + } + (None, None) => {} + } + } + self.maintainer = Some(names.join(", ")); + self.maintainer_email = Some(emails.join(", ")); + } + + if let Some(keywords) = &project.keywords { + self.keywords = Some(keywords.join(" ")); + } + + if let Some(classifiers) = &project.classifiers { + self.classifiers = classifiers.clone(); + } + + if let Some(urls) = &project.urls { + self.project_url = urls.clone(); + } + + if let Some(dependencies) = &project.dependencies { + self.requires_dist = dependencies.clone(); + } + + if let Some(dependencies) = &project.optional_dependencies { + for (extra, deps) in dependencies { + self.provides_extra.push(extra.clone()); + for dep in deps { + let dist = if let Some((dep, marker)) = dep.split_once(';') { + // optional dependency already has environment markers + let new_marker = + format!("({}) and extra == '{}'", marker.trim(), extra); + format!("{}; {}", dep, new_marker) + } else { + format!("{}; extra == '{}'", dep, extra) + }; + self.requires_dist.push(dist); + } + } + } + + if let Some(scripts) = &project.scripts { + self.scripts = scripts.clone(); + } + if let Some(gui_scripts) = &project.gui_scripts { + self.gui_scripts = gui_scripts.clone(); + } + if let Some(entry_points) = &project.entry_points { + // Raise error on ambiguous entry points: https://www.python.org/dev/peps/pep-0621/#entry-points + if entry_points.contains_key("console_scripts") { + bail!("console_scripts is not allowed in project.entry-points table"); + } + if entry_points.contains_key("gui_scripts") { + bail!("gui_scripts is not allowed in project.entry-points table"); + } + self.entry_points = entry_points.clone(); + } + } + Ok(()) + } + /// Uses a Cargo.toml to create the metadata for python packages /// /// manifest_path must be the directory, not the file @@ -123,7 +296,7 @@ impl Metadata21 { }) .unwrap_or_else(|| cargo_toml.package.name.clone()); - Ok(Metadata21 { + let mut metadata = Metadata21 { metadata_version: "2.1".to_owned(), // Mapped from cargo metadata @@ -161,7 +334,12 @@ impl Metadata21 { // Open question: Should those also be supported? And if so, how? platform: Vec::new(), supported_platform: Vec::new(), - }) + scripts: cargo_toml.scripts(), + gui_scripts: HashMap::new(), + entry_points: HashMap::new(), + }; + metadata.merge_pyproject_toml(manifest_path)?; + Ok(metadata) } /// Formats the metadata into a list where keys with multiple values @@ -519,4 +697,37 @@ mod test { ); } } + + #[test] + fn test_merge_metadata_from_pyproject_toml() { + let cargo_toml_str = fs_err::read_to_string("test-crates/pyo3-pure/Cargo.toml").unwrap(); + let cargo_toml: CargoToml = toml::from_str(&cargo_toml_str).unwrap(); + let metadata = Metadata21::from_cargo_toml(&cargo_toml, "test-crates/pyo3-pure").unwrap(); + assert_eq!( + metadata.summary, + Some("Implements a dummy function in Rust".to_string()) + ); + assert_eq!( + metadata.description, + Some(fs_err::read_to_string("test-crates/pyo3-pure/Readme.md").unwrap()) + ); + assert_eq!(metadata.classifiers, &["Programming Language :: Rust"]); + assert_eq!( + metadata.maintainer_email, + Some("messense ".to_string()) + ); + assert_eq!(metadata.scripts["get_42"], "pyo3_pure:DummyClass.get_42"); + assert_eq!( + metadata.gui_scripts["get_42_gui"], + "pyo3_pure:DummyClass.get_42" + ); + assert_eq!(metadata.provides_extra, &["test"]); + assert_eq!( + metadata.requires_dist, + &[ + "attrs; extra == 'test'", + "boltons; (sys_platform == 'win32') and extra == 'test'" + ] + ) + } } diff --git a/src/module_writer.rs b/src/module_writer.rs index d2c3a569f..881afa8f7 100644 --- a/src/module_writer.rs +++ b/src/module_writer.rs @@ -248,7 +248,6 @@ impl WheelWriter { tag: &str, wheel_dir: &Path, metadata21: &Metadata21, - scripts: &HashMap, tags: &[String], ) -> Result { let wheel_path = wheel_dir.join(format!( @@ -267,7 +266,7 @@ impl WheelWriter { wheel_path, }; - write_dist_info(&mut builder, &metadata21, &scripts, &tags)?; + write_dist_info(&mut builder, &metadata21, &tags)?; Ok(builder) } @@ -379,10 +378,13 @@ Root-Is-Purelib: false } /// https://packaging.python.org/specifications/entry-points/ -fn entry_points_txt(entrypoints: &HashMap) -> String { +fn entry_points_txt( + entry_type: &str, + entrypoints: &HashMap, +) -> String { entrypoints .iter() - .fold("[console_scripts]\n".to_owned(), |text, (k, v)| { + .fold(format!("[{}]\n", entry_type), |text, (k, v)| { text + k + "=" + v + "\n" }) } @@ -733,7 +735,6 @@ pub fn write_python_part( pub fn write_dist_info( writer: &mut impl ModuleWriter, metadata21: &Metadata21, - scripts: &HashMap, tags: &[String], ) -> Result<()> { let dist_info_dir = metadata21.get_dist_info_dir(); @@ -747,10 +748,20 @@ pub fn write_dist_info( writer.add_bytes(&dist_info_dir.join("WHEEL"), wheel_file(tags).as_bytes())?; - if !scripts.is_empty() { + let mut entry_points = String::new(); + if !metadata21.scripts.is_empty() { + entry_points.push_str(&entry_points_txt("console_scripts", &metadata21.scripts)); + } + if !metadata21.gui_scripts.is_empty() { + entry_points.push_str(&entry_points_txt("gui_scripts", &metadata21.gui_scripts)); + } + for (entry_type, scripts) in &metadata21.entry_points { + entry_points.push_str(&entry_points_txt(entry_type, scripts)); + } + if !entry_points.is_empty() { writer.add_bytes( &dist_info_dir.join("entry_points.txt"), - entry_points_txt(scripts).as_bytes(), + entry_points.as_bytes(), )?; } diff --git a/src/pyproject_toml.rs b/src/pyproject_toml.rs index 70a7d2721..983387b85 100644 --- a/src/pyproject_toml.rs +++ b/src/pyproject_toml.rs @@ -51,10 +51,10 @@ impl PyProjectToml { "Couldn't find pyproject.toml at {}", path.display() ))?; - let cargo_toml: PyProjectToml = toml::from_str(&contents) + let pyproject: PyProjectToml = toml::from_str(&contents) .map_err(|err| format_err!("pyproject.toml is not PEP 517 compliant: {}", err))?; - cargo_toml.warn_missing_maturin_version(); - Ok(cargo_toml) + pyproject.warn_missing_maturin_version(); + Ok(pyproject) } /// Returns the value of `[maturin.sdist-include]` in pyproject.toml diff --git a/test-crates/pyo3-pure/Cargo.toml b/test-crates/pyo3-pure/Cargo.toml index 3f19c3e79..f73940293 100644 --- a/test-crates/pyo3-pure/Cargo.toml +++ b/test-crates/pyo3-pure/Cargo.toml @@ -2,17 +2,8 @@ authors = ["konstin "] name = "pyo3-pure" version = "2.1.2" -description = "Implements a dummy function (get_fortytwo.DummyClass.get_42()) in rust" -readme = "Readme.md" edition = "2018" - -[package.metadata.maturin.scripts] -get_42 = "pyo3_pure:DummyClass.get_42" - -[package.metadata.maturin] -classifier = [ - "Programming Language :: Rust" -] +description = "Implements a dummy function (get_fortytwo.DummyClass.get_42()) in rust" [dependencies] pyo3 = { version = "0.13.2", features = ["abi3-py36", "extension-module"] } diff --git a/test-crates/pyo3-pure/pyproject.toml b/test-crates/pyo3-pure/pyproject.toml index 9a2f56d97..5b15900d6 100644 --- a/test-crates/pyo3-pure/pyproject.toml +++ b/test-crates/pyo3-pure/pyproject.toml @@ -1,3 +1,26 @@ [build-system] requires = ["maturin>=0.10,<0.11"] build-backend = "maturin" + +[project] +name = "pyo3-pure" +classifiers = [ + "Programming Language :: Rust" +] +description = "Implements a dummy function in Rust" +readme = "Readme.md" +maintainers = [ + {name = "messense", email = "messense@icloud.com"} +] + +[project.optional-dependencies] +test = [ + "attrs", + "boltons; sys_platform == 'win32'" +] + +[project.scripts] +get_42 = "pyo3_pure:DummyClass.get_42" + +[project.gui-scripts] +get_42_gui = "pyo3_pure:DummyClass.get_42" From 6d1dea6bdbc79463cf80dab1ce5e1215a058f659 Mon Sep 17 00:00:00 2001 From: messense Date: Sun, 30 May 2021 18:10:19 +0800 Subject: [PATCH 3/5] keywords metadata should be separated by commas instead of spaces https://packaging.python.org/specifications/core-metadata/#keywords The specification previously showed keywords separated by spaces, but distutils and setuptools implemented it with commas. These tools have been very widely used for many years, so it was easier to update the specification to match the de facto standard. --- src/metadata.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/metadata.rs b/src/metadata.rs index 480937ec6..9146f868f 100644 --- a/src/metadata.rs +++ b/src/metadata.rs @@ -197,7 +197,7 @@ impl Metadata21 { } if let Some(keywords) = &project.keywords { - self.keywords = Some(keywords.join(" ")); + self.keywords = Some(keywords.join(",")); } if let Some(classifiers) = &project.classifiers { @@ -309,7 +309,7 @@ impl Metadata21 { .package .keywords .clone() - .map(|keywords| keywords.join(" ")), + .map(|keywords| keywords.join(",")), home_page: cargo_toml.package.homepage.clone(), download_url: None, // Cargo.toml has no distinction between author and author email @@ -546,7 +546,7 @@ mod test { Requires-Dist: flask~=1.1.0 Requires-Dist: toml==0.10.0 Summary: A test project - Keywords: ffi test + Keywords: ffi,test Home-Page: https://example.org Author: konstin Author-Email: konstin @@ -605,7 +605,7 @@ mod test { Requires-Dist: flask~=1.1.0 Requires-Dist: toml==0.10.0 Summary: A test project - Keywords: ffi test + Keywords: ffi,test Home-Page: https://example.org Author: konstin Author-Email: konstin From 0cb3d79d5b3aa75a4cfc3a4ef8b353dfa7161279 Mon Sep 17 00:00:00 2001 From: messense Date: Sun, 30 May 2021 21:57:48 +0800 Subject: [PATCH 4/5] classifiers metadata name should be `Classifier` instead of `Classifiers` https://packaging.python.org/specifications/core-metadata/#classifier-multiple-use I've downloaded `Flask` and `requests` from PyPI, they all show `Classifier` in wheel metadata --- src/metadata.rs | 8 ++++---- src/upload.rs | 10 +++++++++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/metadata.rs b/src/metadata.rs index 9146f868f..1681bc28e 100644 --- a/src/metadata.rs +++ b/src/metadata.rs @@ -361,7 +361,7 @@ impl Metadata21 { add_vec("Supported-Platform", &self.supported_platform); add_vec("Platform", &self.platform); add_vec("Supported-Platform", &self.supported_platform); - add_vec("Classifiers", &self.classifiers); + add_vec("Classifier", &self.classifiers); add_vec("Requires-Dist", &self.requires_dist); add_vec("Provides-Dist", &self.provides_dist); add_vec("Obsoletes-Dist", &self.obsoletes_dist); @@ -542,7 +542,7 @@ mod test { Metadata-Version: 2.1 Name: info-project Version: 0.1.0 - Classifiers: Programming Language :: Python + Classifier: Programming Language :: Python Requires-Dist: flask~=1.1.0 Requires-Dist: toml==0.10.0 Summary: A test project @@ -601,7 +601,7 @@ mod test { Metadata-Version: 2.1 Name: info-project Version: 0.1.0 - Classifiers: Programming Language :: Python + Classifier: Programming Language :: Python Requires-Dist: flask~=1.1.0 Requires-Dist: toml==0.10.0 Summary: A test project @@ -649,7 +649,7 @@ mod test { Metadata-Version: 2.1 Name: info Version: 0.1.0 - Classifiers: Programming Language :: Python + Classifier: Programming Language :: Python Summary: A test project Home-Page: https://example.org Author: konstin diff --git a/src/upload.rs b/src/upload.rs index 388c9b8a7..1cd5b99f2 100644 --- a/src/upload.rs +++ b/src/upload.rs @@ -77,7 +77,15 @@ pub fn upload( // Type system shenanigans .chain(metadata21.to_vec().into_iter()) // All fields must be lower case and with underscores or they will be ignored by warehouse - .map(|(key, value)| (key.to_lowercase().replace("-", "_"), value)) + .map(|(key, value)| { + let mut key = key.to_lowercase().replace("-", "_"); + if key == "classifier" { + // PyPI upload api expects `classifiers` instead of `classifier` + // See https://github.com/pypa/warehouse/issues/3151#issuecomment-796965735 + key = "classifiers".to_string(); + } + (key, value) + }) .collect(); let mut form = Form::new(); From e682f65861b99ce0abb841847082550e1a65c586 Mon Sep 17 00:00:00 2001 From: messense Date: Wed, 2 Jun 2021 14:15:34 +0800 Subject: [PATCH 5/5] Fix some metadata field name cases according to https://packaging.python.org/specifications/core-metadata/ --- src/metadata.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/metadata.rs b/src/metadata.rs index 1681bc28e..329fc25aa 100644 --- a/src/metadata.rs +++ b/src/metadata.rs @@ -358,7 +358,6 @@ impl Metadata21 { } }; - add_vec("Supported-Platform", &self.supported_platform); add_vec("Platform", &self.platform); add_vec("Supported-Platform", &self.supported_platform); add_vec("Classifier", &self.classifiers); @@ -377,11 +376,11 @@ impl Metadata21 { add_option("Summary", &self.summary); add_option("Keywords", &self.keywords); add_option("Home-Page", &self.home_page); - add_option("Download-Url", &self.download_url); + add_option("Download-URL", &self.download_url); add_option("Author", &self.author); - add_option("Author-Email", &self.author_email); + add_option("Author-email", &self.author_email); add_option("Maintainer", &self.maintainer); - add_option("Maintainer-Email", &self.maintainer_email); + add_option("Maintainer-email", &self.maintainer_email); add_option("License", &self.license); add_option("Requires-Python", &self.requires_python); add_option("Description-Content-Type", &self.description_content_type); @@ -549,7 +548,7 @@ mod test { Keywords: ffi,test Home-Page: https://example.org Author: konstin - Author-Email: konstin + Author-email: konstin Description-Content-Type: text/plain; charset=UTF-8 Project-URL: Bug Tracker, http://bitbucket.org/tarek/distribute/issues/ @@ -608,7 +607,7 @@ mod test { Keywords: ffi,test Home-Page: https://example.org Author: konstin - Author-Email: konstin + Author-email: konstin Description-Content-Type: text/x-rst Some test package @@ -653,7 +652,7 @@ mod test { Summary: A test project Home-Page: https://example.org Author: konstin - Author-Email: konstin + Author-email: konstin "# );