diff --git a/src/develop.rs b/src/develop.rs index dcecf9a24..f1cec46e5 100644 --- a/src/develop.rs +++ b/src/develop.rs @@ -67,7 +67,7 @@ impl InstallBackend { }; if let Some(captures) = re.expect("regex should be valid").captures(stdout) { Ok(semver::Version::parse(&captures[1]) - .with_context(|| format!("failed to parse semver from {:?}", stdout))?) + .with_context(|| format!("failed to parse semver from {stdout:?}"))?) } else { bail!("failed to parse version from {:?}", stdout); } @@ -321,7 +321,7 @@ fn install_wheel( ); } if let Err(err) = configure_as_editable(build_context, python, install_backend) { - eprintln!("⚠️ Warning: failed to set package as editable: {}", err); + eprintln!("⚠️ Warning: failed to set package as editable: {err}"); } Ok(()) } diff --git a/src/metadata.rs b/src/metadata.rs index 473b8521f..ecadf0f52 100644 --- a/src/metadata.rs +++ b/src/metadata.rs @@ -147,6 +147,41 @@ impl Metadata24 { bail!("`project.dynamic` must not specify `name` in pyproject.toml"); } + // According to PEP 621, build backends must not add metadata fields + // that are not declared in the dynamic list. Clear fields from Cargo.toml + // that are not in the dynamic list. + if !dynamic.contains("description") { + self.summary = None; + } + if !dynamic.contains("authors") { + self.author = None; + self.author_email = None; + } + if !dynamic.contains("maintainers") { + self.maintainer = None; + self.maintainer_email = None; + } + if !dynamic.contains("keywords") { + self.keywords = None; + } + if !dynamic.contains("urls") { + self.project_url.clear(); + } + if !dynamic.contains("license") { + self.license = None; + // Don't clear license_files as they may come from auto-discovery + } + if !dynamic.contains("classifiers") { + self.classifiers.clear(); + } + if !dynamic.contains("readme") { + self.description = None; + self.description_content_type = None; + } + if !dynamic.contains("requires-python") { + self.requires_python = None; + } + self.name.clone_from(&project.name); let version_ok = pyproject_toml.warn_invalid_version_info(); @@ -681,6 +716,7 @@ mod test { use expect_test::{expect, Expect}; use indoc::indoc; use pretty_assertions::assert_eq; + use tempfile::TempDir; fn assert_metadata_from_cargo_toml( readme: &str, @@ -948,4 +984,149 @@ A test project assert_eq!(result, expected); } } + + #[test] + fn test_issue_2544_respect_pyproject_dynamic_without_dynamic_fields() { + let temp_dir = TempDir::new().unwrap(); + let crate_path = temp_dir.path(); + let manifest_path = crate_path.join("Cargo.toml"); + let pyproject_path = crate_path.join("pyproject.toml"); + + // Create basic src structure + fs::create_dir(crate_path.join("src")).unwrap(); + fs::write(crate_path.join("src/lib.rs"), "").unwrap(); + + // Write Cargo.toml with metadata that should NOT be included + // because pyproject.toml doesn't declare them as dynamic + let cargo_toml = indoc!( + r#" + [package] + name = "test-package" + version = "0.1.0" + description = "Description from Cargo.toml - should not appear" + authors = ["author from cargo.toml "] + keywords = ["cargo", "toml", "keyword"] + repository = "https://github.com/example/repo" + + [lib] + crate-type = ["cdylib"] + "# + ); + fs::write(&manifest_path, cargo_toml).unwrap(); + + // Write pyproject.toml WITHOUT declaring the fields as dynamic + let pyproject_toml_content = indoc!( + r#" + [build-system] + requires = ["maturin>=1.0,<2.0"] + build-backend = "maturin" + + [project] + name = "test-package" + version = "0.1.0" + # Note: no description, authors, keywords, urls in dynamic list + # dynamic = [] # Not specified, so defaults to empty + "# + ); + fs::write(&pyproject_path, pyproject_toml_content).unwrap(); + + // Load metadata as maturin does + let cargo_metadata = MetadataCommand::new() + .manifest_path(&manifest_path) + .exec() + .unwrap(); + let mut metadata = Metadata24::from_cargo_toml(crate_path, &cargo_metadata).unwrap(); + let pyproject_toml = PyProjectToml::new(&pyproject_path).unwrap(); + metadata + .merge_pyproject_toml(crate_path, &pyproject_toml) + .unwrap(); + + assert_eq!( + metadata.summary, None, + "summary should be None when not in dynamic list" + ); + assert_eq!( + metadata.author, None, + "author should be None when not in dynamic list" + ); + assert_eq!( + metadata.keywords, None, + "keywords should be None when not in dynamic list" + ); + assert!( + metadata.project_url.is_empty(), + "project_url should be empty when not in dynamic list" + ); + } + + #[test] + fn test_issue_2544_respect_pyproject_dynamic_with_dynamic_fields() { + let temp_dir = TempDir::new().unwrap(); + let crate_path = temp_dir.path(); + let manifest_path = crate_path.join("Cargo.toml"); + let pyproject_path = crate_path.join("pyproject.toml"); + + // Create basic src structure + fs::create_dir(crate_path.join("src")).unwrap(); + fs::write(crate_path.join("src/lib.rs"), "").unwrap(); + + // Write Cargo.toml with metadata + let cargo_toml = indoc!( + r#" + [package] + name = "test-package" + version = "0.1.0" + description = "Description from Cargo.toml - should appear" + authors = ["author from cargo.toml "] + keywords = ["cargo", "toml", "keyword"] + repository = "https://github.com/example/repo" + + [lib] + crate-type = ["cdylib"] + "# + ); + fs::write(&manifest_path, cargo_toml).unwrap(); + + // Write pyproject.toml WITH fields declared as dynamic + let pyproject_toml_content = indoc!( + r#" + [build-system] + requires = ["maturin>=1.0,<2.0"] + build-backend = "maturin" + + [project] + name = "test-package" + version = "0.1.0" + # Fields declared as dynamic - should come from Cargo.toml + dynamic = ["description", "authors", "keywords", "urls"] + "# + ); + fs::write(&pyproject_path, pyproject_toml_content).unwrap(); + + // Load metadata as maturin does + let cargo_metadata = MetadataCommand::new() + .manifest_path(&manifest_path) + .exec() + .unwrap(); + let mut metadata = Metadata24::from_cargo_toml(crate_path, &cargo_metadata).unwrap(); + let pyproject_toml = PyProjectToml::new(&pyproject_path).unwrap(); + metadata + .merge_pyproject_toml(crate_path, &pyproject_toml) + .unwrap(); + + // These fields SHOULD be set because they are in dynamic list + assert_eq!( + metadata.summary, + Some("Description from Cargo.toml - should appear".to_string()) + ); + assert_eq!( + metadata.author, + Some("author from cargo.toml ".to_string()) + ); + assert_eq!(metadata.keywords, Some("cargo,toml,keyword".to_string())); + assert_eq!( + metadata.project_url.get("Source Code"), + Some(&"https://github.com/example/repo".to_string()) + ); + } } diff --git a/src/new_project.rs b/src/new_project.rs index 75870adfb..d6bc4f0fb 100644 --- a/src/new_project.rs +++ b/src/new_project.rs @@ -301,23 +301,20 @@ mod package_name_validations { } if ["core", "std", "alloc", "proc_macro", "proc-macro"].contains(&name) { eprintln!( - "⚠️ Warning: the name `{}` is part of Rust's standard library\n\ + "⚠️ Warning: the name `{name}` is part of Rust's standard library\n\ It is recommended to use a different name to avoid problems.", - name, ); } if is_windows_reserved(name) { eprintln!( - "⚠️ Warning: the name `{}` is a reserved Windows filename\n\ + "⚠️ Warning: the name `{name}` is a reserved Windows filename\n\ This package will not work on Windows platforms.", - name ); } if is_non_ascii_name(name) { eprintln!( - "⚠️ Warning: the name `{}` contains non-ASCII characters\n\ + "⚠️ Warning: the name `{name}` contains non-ASCII characters\n\ Non-ASCII crate names are not supported by Rust.", - name ); } let name_in_lowercase = name.to_lowercase(); diff --git a/src/pyproject_toml.rs b/src/pyproject_toml.rs index 5a772fe6f..965bf0729 100644 --- a/src/pyproject_toml.rs +++ b/src/pyproject_toml.rs @@ -379,10 +379,7 @@ impl PyProjectToml { if !version_specifier.contains(&self_version) { eprintln!( "⚠️ Warning: You specified {requires_maturin} in pyproject.toml under \ - `build-system.requires`, but the current {maturin} version is {version}", - requires_maturin = requires_maturin, - maturin = maturin, - version = self_version, + `build-system.requires`, but the current {maturin} version is {self_version}", ); return false; } diff --git a/src/source_distribution.rs b/src/source_distribution.rs index 38ac2e7f6..efe4fc414 100644 --- a/src/source_distribution.rs +++ b/src/source_distribution.rs @@ -370,8 +370,7 @@ fn find_path_deps(cargo_metadata: &Metadata) -> Result {} - Err(e) => eprintln!("⚠️ Warning: Failed to resolve PyPI token via OIDC: {}", e), + Err(e) => eprintln!("⚠️ Warning: Failed to resolve PyPI token via OIDC: {e}"), } if let Some((username, password)) = diff --git a/test-crates/hello-world/pyproject.toml b/test-crates/hello-world/pyproject.toml index 89947b83f..205603c2e 100644 --- a/test-crates/hello-world/pyproject.toml +++ b/test-crates/hello-world/pyproject.toml @@ -2,6 +2,7 @@ name = "hello-world" version = "0.1.0" license-files = ["LICENSE", "licenses/*"] +dynamic = ["authors", "readme"] # Allow authors and readme from Cargo.toml to be used [build-system] requires = ["maturin>=1.0,<2.0"] diff --git a/test-crates/license-test/pyproject.toml b/test-crates/license-test/pyproject.toml index d22505500..82b823146 100644 --- a/test-crates/license-test/pyproject.toml +++ b/test-crates/license-test/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "maturin" [project] name = "license-test" license = { file = "LICENCE.txt" } -dynamic = ["version"] +dynamic = ["version", "license"] # Allow license from Cargo.toml to be used [tool.maturin] bindings = "bin" diff --git a/test-crates/pyo3-mixed-py-subdir/pyproject.toml b/test-crates/pyo3-mixed-py-subdir/pyproject.toml index a62f3fb17..27ee21971 100644 --- a/test-crates/pyo3-mixed-py-subdir/pyproject.toml +++ b/test-crates/pyo3-mixed-py-subdir/pyproject.toml @@ -9,7 +9,7 @@ classifiers = [ "Programming Language :: Rust" ] requires-python = ">=3.6" -dynamic = ["version"] +dynamic = ["version", "description"] # Allow description from Cargo.toml to be used [project.scripts] get_42 = "pyo3_mixed_py_subdir:get_42" diff --git a/test-crates/pyo3-pure/pyproject.toml b/test-crates/pyo3-pure/pyproject.toml index 532a0e695..1d2ae3f26 100644 --- a/test-crates/pyo3-pure/pyproject.toml +++ b/test-crates/pyo3-pure/pyproject.toml @@ -25,6 +25,7 @@ maintainers = [ {name = "messense", email = "messense@icloud.com"} ] license = { file = "LICENSE" } +dynamic = ["license"] # Allow license from Cargo.toml to be used [project.optional-dependencies] test = [ diff --git a/tests/run.rs b/tests/run.rs index 09ed055c3..d9ee9e2bc 100644 --- a/tests/run.rs +++ b/tests/run.rs @@ -219,7 +219,7 @@ fn develop_hello_world(#[case] backend: TestInstallBackend, #[case] name: &str) handle_result(develop::test_develop( "test-crates/hello-world", None, - format!("develop-hello-world-{}", name).as_str(), + format!("develop-hello-world-{name}").as_str(), false, backend, )); @@ -246,7 +246,7 @@ fn develop_pyo3_ffi_pure(#[case] backend: TestInstallBackend, #[case] name: &str handle_result(develop::test_develop( "test-crates/pyo3-ffi-pure", None, - format!("develop-pyo3-ffi-pure-{}", name).as_str(), + format!("develop-pyo3-ffi-pure-{name}").as_str(), false, backend, ));