Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Configurable Description-Content-Type #360

Merged
merged 5 commits into from
Oct 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/cargo_toml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ pub struct RemainingCoreMetadata {
pub requires_external: Option<Vec<String>>,
pub project_url: Option<Vec<String>>,
pub provides_extra: Option<Vec<String>>,
pub description_content_type: Option<String>,
}

#[cfg(test)]
Expand Down
206 changes: 162 additions & 44 deletions src/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,27 @@ pub struct Metadata21 {
pub provides_extra: Vec<String>,
}

const PLAINTEXT_CONTENT_TYPE: &str = "text/plain; charset=UTF-8";
const GFM_CONTENT_TYPE: &str = "text/markdown; charset=UTF-8; variant=GFM";

/// Guess a Description-Content-Type based on the file extension,
/// defaulting to plaintext if extension is unknown or empty.
///
/// See https://packaging.python.org/specifications/core-metadata/#description-content-type
fn path_to_content_type(path: &PathBuf) -> String {
path.extension()
.map_or(String::from(PLAINTEXT_CONTENT_TYPE), |ext| {
let ext = ext.to_string_lossy().to_lowercase();
let type_str = match ext.as_str() {
"rst" => "text/x-rst; charset=UTF-8",
"md" => GFM_CONTENT_TYPE,
"markdown" => GFM_CONTENT_TYPE,
_ => PLAINTEXT_CONTENT_TYPE,
};
String::from(type_str)
})
}

impl Metadata21 {
/// Uses a Cargo.toml to create the metadata for python packages
///
Expand All @@ -64,33 +85,32 @@ impl Metadata21 {
) -> Result<Metadata21> {
let authors = cargo_toml.package.authors.join(", ");

// See https://packaging.python.org/specifications/core-metadata/#description
let description = if let Some(ref readme) = cargo_toml.package.readme {
Some(
read_to_string(manifest_path.as_ref().join(readme)).context(format!(
"Failed to read readme specified in Cargo.toml, which should be at {}",
manifest_path.as_ref().join(readme).display()
))?,
)
} else {
None
};
let classifier = cargo_toml.classifier();

let description_content_type = if description.is_some() {
// I'm not hundred percent sure if that's the best preset
Some("text/markdown; charset=UTF-8; variant=GFM".to_owned())
let author_email = if authors.contains('@') {
Some(authors.clone())
} else {
None
};

let classifier = cargo_toml.classifier();

let extra_metadata = cargo_toml.remaining_core_metadata();

let author_email = if authors.contains('@') {
Some(authors.clone())
let description: Option<String>;
let description_content_type: Option<String>;
// See https://packaging.python.org/specifications/core-metadata/#description
if let Some(ref readme) = cargo_toml.package.readme {
let readme_path = manifest_path.as_ref().join(readme);
description = Some(read_to_string(&readme_path).context(format!(
"Failed to read readme specified in Cargo.toml, which should be at {}",
readme_path.display()
))?);

description_content_type = extra_metadata
.description_content_type
.or_else(|| Some(path_to_content_type(&readme_path)));
} else {
None
description = None;
description_content_type = None;
};

Ok(Metadata21 {
Expand Down Expand Up @@ -243,16 +263,7 @@ mod test {
use indoc::indoc;
use std::io::Write;

#[test]
fn test_metadata_from_cargo_toml() {
let readme = indoc!(
r#"
# Some test package

This is the readme for a test package
"#
);

fn assert_metadata_from_cargo_toml(readme: &str, cargo_toml: &str, expected: &str) {
let mut readme_md = tempfile::NamedTempFile::new().unwrap();

let readme_path = if cfg!(windows) {
Expand All @@ -263,6 +274,47 @@ mod test {

readme_md.write_all(readme.as_bytes()).unwrap();

let toml_with_path = cargo_toml.replace("REPLACE_README_PATH", &readme_path);

let cargo_toml_struct: CargoToml = toml::from_str(&toml_with_path).unwrap();

let metadata =
Metadata21::from_cargo_toml(&cargo_toml_struct, &readme_md.path().parent().unwrap())
.unwrap();

let actual = metadata.to_file_contents();

assert_eq!(
actual.trim(),
expected.trim(),
"Actual metadata differed from expected\nEXPECTED:\n{}\n\nGOT:\n{}",
expected,
actual
);

// get_dist_info_dir test checks against hard-coded values - check that they are as expected in the source first
assert!(
cargo_toml.contains("name = \"info-project\"")
&& cargo_toml.contains("version = \"0.1.0\""),
"cargo_toml name and version string do not match hardcoded values, test will fail",
);
assert_eq!(
metadata.get_dist_info_dir(),
PathBuf::from("info_project-0.1.0.dist-info"),
"Dist info dir differed from expected"
);
}

#[test]
fn test_metadata_from_cargo_toml() {
let readme = indoc!(
r#"
# Some test package

This is the readme for a test package
"#
);

let cargo_toml = indoc!(
r#"
[package]
Expand All @@ -271,7 +323,7 @@ mod test {
version = "0.1.0"
description = "A test project"
homepage = "https://example.org"
readme = "readme.md"
readme = "REPLACE_README_PATH"
keywords = ["ffi", "test"]

[lib]
Expand All @@ -285,13 +337,7 @@ mod test {
classifier = ["Programming Language :: Python"]
requires-dist = ["flask~=1.1.0", "toml==0.10.0"]
"#
)
.replace("readme.md", &readme_path);

let cargo_toml: CargoToml = toml::from_str(&cargo_toml).unwrap();

let metadata =
Metadata21::from_cargo_toml(&cargo_toml, &readme_md.path().parent().unwrap()).unwrap();
);

let expected = indoc!(
r#"
Expand All @@ -306,21 +352,93 @@ mod test {
Home-Page: https://example.org
Author: konstin <[email protected]>
Author-Email: konstin <[email protected]>
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Description-Content-Type: text/plain; charset=UTF-8

# Some test package

This is the readme for a test package
"#
);

let actual = metadata.to_file_contents();
assert_metadata_from_cargo_toml(readme, cargo_toml, expected);
}

assert_eq!(actual.trim(), expected.trim());
#[test]
fn test_metadata_from_cargo_toml_rst() {
let readme = indoc!(
r#"
Some test package
=================
"#
);

assert_eq!(
metadata.get_dist_info_dir(),
PathBuf::from("info_project-0.1.0.dist-info")
)
let cargo_toml = indoc!(
r#"
[package]
authors = ["konstin <[email protected]>"]
name = "info-project"
version = "0.1.0"
description = "A test project"
homepage = "https://example.org"
readme = "REPLACE_README_PATH"
keywords = ["ffi", "test"]

[lib]
crate-type = ["cdylib"]
name = "pyo3_pure"

[package.metadata.maturin.scripts]
ph = "maturin:print_hello"

[package.metadata.maturin]
classifier = ["Programming Language :: Python"]
requires-dist = ["flask~=1.1.0", "toml==0.10.0"]
description-content-type = "text/x-rst"
"#
);

let expected = indoc!(
r#"
Metadata-Version: 2.1
Name: info-project
Version: 0.1.0
Classifier: Programming Language :: Python
Requires-Dist: flask~=1.1.0
Requires-Dist: toml==0.10.0
Summary: A test project
Keywords: ffi test
Home-Page: https://example.org
Author: konstin <[email protected]>
Author-Email: konstin <[email protected]>
Description-Content-Type: text/x-rst

Some test package
=================
"#
);

assert_metadata_from_cargo_toml(readme, cargo_toml, expected);
}

#[test]
fn test_path_to_content_type() {
for (filename, expected) in vec![
("r.md", GFM_CONTENT_TYPE),
("r.markdown", GFM_CONTENT_TYPE),
("r.mArKdOwN", GFM_CONTENT_TYPE),
("r.rst", "text/x-rst; charset=UTF-8"),
("r.somethingelse", PLAINTEXT_CONTENT_TYPE),
("r", PLAINTEXT_CONTENT_TYPE),
] {
let result = path_to_content_type(&PathBuf::from(filename));
assert_eq!(
result.as_str(),
expected,
"Wrong content type for file '{}'. Expected '{}', got '{}'",
filename,
expected,
result
);
}
}
}