diff --git a/Cargo.lock b/Cargo.lock index 362e4706f1..1e74004570 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3088,7 +3088,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.59.0", + "windows-core 0.61.2", ] [[package]] @@ -3701,7 +3701,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.53.3", ] [[package]] @@ -5749,7 +5749,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" dependencies = [ "anyhow", - "itertools 0.12.1", + "itertools 0.14.0", "proc-macro2", "quote", "syn", @@ -5855,16 +5855,16 @@ dependencies = [ [[package]] name = "pyproject-toml" -version = "0.13.5" +version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b0f6160dc48298b9260d9b958ad1d7f96f6cd0b9df200b22329204e09334663" +checksum = "ec768e063102b426e8962989758115e8659485124de9207bc365fab524125d65" dependencies = [ "indexmap 2.11.1", "pep440_rs", "pep508_rs", "serde", "thiserror 2.0.16", - "toml 0.8.23", + "toml", ] [[package]] @@ -7795,7 +7795,7 @@ dependencies = [ "ntapi", "objc2-core-foundation", "objc2-io-kit", - "windows 0.59.0", + "windows 0.61.3", ] [[package]] @@ -8117,18 +8117,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "toml_edit 0.22.27", -] - [[package]] name = "toml" version = "0.9.5" @@ -8582,7 +8570,7 @@ dependencies = [ "spdx", "tar", "thiserror 2.0.16", - "toml 0.9.5", + "toml", "tracing", "uv-distribution-filename", "uv-fs", @@ -8670,7 +8658,7 @@ dependencies = [ "globwalk", "serde", "thiserror 2.0.16", - "toml 0.9.5", + "toml", "tracing", "walkdir", ] @@ -8843,7 +8831,7 @@ dependencies = [ "thiserror 2.0.16", "tokio", "tokio-util", - "toml 0.9.5", + "toml", "tracing", "url", "uv-cache", @@ -9342,7 +9330,7 @@ dependencies = [ "rustc-hash", "serde", "thiserror 2.0.16", - "toml 0.9.5", + "toml", "tracing", "url", "uv-cache-key", @@ -9415,7 +9403,7 @@ dependencies = [ "thiserror 2.0.16", "tokio", "tokio-stream", - "toml 0.9.5", + "toml", "toml_edit 0.23.4", "tracing", "url", @@ -9595,7 +9583,7 @@ dependencies = [ "serde", "thiserror 2.0.16", "tokio", - "toml 0.9.5", + "toml", "toml_edit 0.23.4", "tracing", "uv-build-backend", @@ -9891,7 +9879,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index bc420b9c01..ce2f4f050f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,7 +72,7 @@ pep440_rs = "0.7.3" pep508_rs = "0.9.2" percent-encoding = "2.3.1" pin-project-lite = "0.2.16" -pyproject-toml = "0.13.4" +pyproject-toml = "0.13.6" rand = { version = "0.9.1", default-features = false } rayon = "1.10.0" regex = "1.11.1" diff --git a/crates/pixi/tests/integration_rust/pypi_tests.rs b/crates/pixi/tests/integration_rust/pypi_tests.rs index f91217371f..741e5d60ed 100644 --- a/crates/pixi/tests/integration_rust/pypi_tests.rs +++ b/crates/pixi/tests/integration_rust/pypi_tests.rs @@ -1,13 +1,104 @@ -use std::io::Write; +use std::{fs::File, io::Write, str::FromStr}; +use pep508_rs::Requirement; use rattler_conda_types::Platform; use tempfile::tempdir; use typed_path::Utf8TypedPath; use crate::common::pypi_index::{Database as PyPIDatabase, PyPIPackage}; -use crate::common::{LockFileExt, PixiControl}; +use crate::common::{ + LockFileExt, PixiControl, + package_database::{Package, PackageDatabase}, +}; use crate::setup_tracing; -use std::fs::File; + +/// This tests if we can resolve pyproject optional dependencies recursively +/// before when running `pixi list -e all`, this would have not included numpy +/// we are now explicitly testing that this works +#[tokio::test] +async fn pyproject_optional_dependencies_resolve_recursively() { + setup_tracing(); + + let simple = PyPIDatabase::new() + .with(PyPIPackage::new("numpy", "1.0.0")) + .with(PyPIPackage::new("sphinx", "1.0.0")) + .with(PyPIPackage::new("pytest", "1.0.0")) + .into_simple_index() + .unwrap(); + + let platform = Platform::current(); + let platform_str = platform.to_string(); + + let mut package_db = PackageDatabase::default(); + package_db.add_package( + Package::build("python", "3.11.0") + .with_subdir(platform) + .finish(), + ); + let channel = package_db.into_channel().await.unwrap(); + let channel_url = channel.url(); + let index_url = simple.index_url(); + + let pyproject = format!( + r#" +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +name = "recursive-optional-groups" + +[project.optional-dependencies] +np = ["numpy"] +all = ["recursive-optional-groups[np]"] + +[dependency-groups] +docs = ["sphinx"] +test = ["recursive-optional-groups[np]", "pytest", {{include-group = "docs"}}] + +[tool.pixi.project] +channels = ["{channel_url}"] +platforms = ["{platform}"] + +[tool.pixi.dependencies] +python = "==3.11.0" + +[tool.pixi.pypi-options] +index-url = "{index_url}" + +[tool.pixi.environments] +np = {{features = ["np"]}} +all = {{features = ["all"]}} +test = {{features = ["test"]}} +"#, + platform = platform_str, + channel_url = channel_url, + index_url = index_url, + ); + + let pixi = PixiControl::from_pyproject_manifest(&pyproject).unwrap(); + + let lock = pixi.update_lock_file().await.unwrap(); + + let numpy_req = Requirement::from_str("numpy").unwrap(); + let sphinx_req = Requirement::from_str("sphinx").unwrap(); + assert!( + lock.contains_pep508_requirement("np", platform, numpy_req.clone()), + "np environment should include numpy from optional dependencies" + ); + assert!( + lock.contains_pep508_requirement("all", platform, numpy_req.clone()), + "all environment should include numpy inherited from recursive optional dependency" + ); + assert!( + lock.contains_pep508_requirement("test", platform, numpy_req), + "test environment should include numpy inherited from recursive optional dependency" + ); + assert!( + lock.contains_pep508_requirement("test", platform, sphinx_req), + "test environment should include sphinx inherited from recursive dependency group" + ); +} #[tokio::test] #[cfg_attr(not(feature = "slow_integration_tests"), ignore)] diff --git a/crates/pixi_api/src/workspace/init/mod.rs b/crates/pixi_api/src/workspace/init/mod.rs index ebf6d53b9e..da97550109 100644 --- a/crates/pixi_api/src/workspace/init/mod.rs +++ b/crates/pixi_api/src/workspace/init/mod.rs @@ -155,10 +155,10 @@ pub(crate) async fn init( } let (name, pixi_name) = match pyproject.name() { - Some(name) => (name, false), - None => (default_name.as_str(), true), + Some(name) => (name.to_string(), false), + None => (default_name.clone(), true), }; - let environments = pyproject.environments_from_extras().into_diagnostic()?; + let environments = pyproject.environments_from_groups().into_diagnostic()?; let rv = env .render_named_str( consts::PYPROJECT_MANIFEST, diff --git a/crates/pixi_manifest/src/error.rs b/crates/pixi_manifest/src/error.rs index 9d4e6d0327..34e04c8cf9 100644 --- a/crates/pixi_manifest/src/error.rs +++ b/crates/pixi_manifest/src/error.rs @@ -9,6 +9,7 @@ use itertools::Itertools; use miette::{Diagnostic, LabeledSpan, SourceOffset, SourceSpan}; use pixi_pypi_spec::Pep508ToPyPiRequirementError; use pixi_toml::TomlDiagnostic; +use pyproject_toml::ResolveError; use rattler_conda_types::{InvalidPackageNameError, version_spec::ParseVersionSpecError}; use thiserror::Error; use toml_span::{DeserError, Error}; @@ -113,6 +114,8 @@ pub enum TomlError { #[error(transparent)] Conversion(#[from] Box), #[error(transparent)] + ResolveError(#[from] ResolveError), + #[error(transparent)] InvalidNonPackageDependencies(#[from] InvalidNonPackageDependencies), } @@ -163,6 +166,7 @@ impl Display for TomlError { write!(f, "Could not convert pep508 to pixi pypi requirement") } TomlError::InvalidNonPackageDependencies(err) => write!(f, "{err}"), + TomlError::ResolveError(err) => write!(f, "{err}"), } } } diff --git a/crates/pixi_manifest/src/pyproject.rs b/crates/pixi_manifest/src/pyproject.rs index df35a6401c..0c7749b24f 100644 --- a/crates/pixi_manifest/src/pyproject.rs +++ b/crates/pixi_manifest/src/pyproject.rs @@ -4,15 +4,11 @@ use std::{ str::FromStr, }; -use indexmap::IndexMap; -use miette::{Diagnostic, IntoDiagnostic, Report, WrapErr}; -use pep440_rs::{Version, VersionSpecifiers}; -use pep508_rs::Requirement; +use miette::{IntoDiagnostic, Report, WrapErr}; +use pep440_rs::VersionSpecifiers; use pixi_spec::PixiSpec; -use pyproject_toml::{self, Contact, pep735_resolve::Pep735Error}; +use pyproject_toml::{self, Contact, ResolveError}; use rattler_conda_types::{PackageName, ParseStrictness::Lenient, VersionSpec}; -use thiserror::Error; -use toml_span::Spanned; use super::{ DependencyOverwriteBehavior, Feature, SpecType, WorkspaceManifest, @@ -20,11 +16,10 @@ use super::{ }; use crate::{ FeatureName, ManifestKind, Warning, - error::{DependencyError, GenericError}, + error::GenericError, manifests::PackageManifest, toml::{ ExternalWorkspaceProperties, FromTomlStr, PackageDefaults, PyProjectToml, TomlManifest, - pyproject::{TomlContact, TomlDependencyGroups, TomlProject}, }, }; @@ -81,24 +76,10 @@ impl PyProjectManifest { /// - the `[project]` table /// - the `[tool.poetry]` table pub fn name(&self) -> Option<&str> { - if let Some(pixi_name) = self - .pixi_manifest() + self.pixi_manifest() .and_then(|p| p.workspace.as_ref()?.value.name.as_deref()) - { - return Some(pixi_name); - } - if let Some(pyproject) = &self.project.project { - return Some(pyproject.name.value.as_str()); - } - if let Some(poetry_name) = self.poetry().and_then(|p| p.name.as_ref()) { - return Some(poetry_name.as_str()); - } - None - } - - /// Returns the project name as PEP508 name - fn package_name(&self) -> Option { - pep508_rs::PackageName::new(self.name()?.to_string()).ok() + .or_else(|| self.project.project.as_ref().map(|p| p.name.value.as_str())) + .or_else(|| self.poetry().and_then(|p| p.name.as_deref())) } fn tool(&self) -> Option<&Tool> { @@ -106,7 +87,7 @@ impl PyProjectManifest { } /// Returns a reference to the poetry section if it exists. - pub fn poetry(&self) -> Option<&ToolPoetry> { + fn poetry(&self) -> Option<&ToolPoetry> { self.tool().and_then(|t| t.poetry.as_ref()) } @@ -121,102 +102,23 @@ impl PyProjectManifest { self.pixi_manifest().is_some() } - /// Returns optional dependencies from the `[project.optional-dependencies]` - /// table - fn optional_dependencies(&self) -> Option>> { - let project = self.project.project.as_ref()?; - let optional_dependencies = project.optional_dependencies.as_ref()?; - Some( - optional_dependencies - .iter() - .map(|(k, v)| (k.clone(), v.iter().cloned().map(Spanned::take).collect())) - .collect(), - ) - } - - /// Returns dependency groups from the `[dependency-groups]` table - fn dependency_groups(&self) -> Option>, Pep735Error>> { - let dg = self.project.dependency_groups.as_ref()?; - Some(dg.value.0.resolve()) - } - /// Builds a list of pixi environments from pyproject groups of optional /// dependencies and/or dependency groups: /// - one environment is created per group with the same name /// - each environment includes the feature of the same name - /// - it will also include other features inferred from any self references - /// to other groups of optional dependencies (but won't for dependency - /// groups, as recursion between groups is resolved upstream) - pub fn environments_from_extras(&self) -> Result>, Pep735Error> { - let mut environments = HashMap::new(); - if let Some(extras) = self.optional_dependencies() { - let pname = self.package_name(); - for (extra, reqs) in extras { - let mut features = vec![extra.to_string()]; - // Add any references to other groups of extra dependencies - for req in reqs.iter() { - if pname.as_ref() == Some(&req.name) { - for extra in &req.extras { - features.push(extra.to_string()) - } - } - } - // Environments can only contain number, strings and dashes - environments.insert(extra.replace('_', "-").clone(), features); - } - } + pub fn environments_from_groups(self) -> Result>, ResolveError> { + let resolved = self.project.into_inner().resolve()?; + let mut groups = resolved.optional_dependencies; + groups.extend(resolved.dependency_groups); - if let Some(groups) = self.dependency_groups().transpose()? { - for group in groups.into_keys() { - let normalised = group.replace('_', "-"); - // Nothing to do if a group of optional dependencies has the same name as the - // dependency group - if !environments.contains_key(&normalised) { - environments.insert(normalised.clone(), vec![normalised]); - } - } + let mut environments = HashMap::new(); + for group in groups.into_keys() { + environments.insert(group.replace('_', "-"), vec![group.clone()]); } Ok(environments) } -} - -#[derive(Debug, Error, Diagnostic)] -pub enum PyProjectToManifestError { - #[error("Unsupported pep508 requirement: '{0}'")] - DependencyError(Requirement, #[source] DependencyError), - #[error(transparent)] - DependencyGroupError(#[from] Pep735Error), - #[error(transparent)] - TomlError(#[from] TomlError), -} -#[derive(Default)] -pub struct PyProjectFields { - pub name: Option>, - pub description: Option>, - pub version: Option>, - pub authors: Option>>, - pub requires_python: Option>, - pub dependencies: Option>>, - pub optional_dependencies: Option>>>, -} - -impl From for PyProjectFields { - fn from(project: TomlProject) -> Self { - Self { - name: Some(project.name), - description: project.description, - version: project.version, - authors: project.authors, - requires_python: project.requires_python, - dependencies: project.dependencies, - optional_dependencies: project.optional_dependencies, - } - } -} - -impl PyProjectManifest { /// Returns true if the pyproject.toml file also contains a pixi workspace. pub fn has_pixi_workspace(&self) -> bool { self.tool() @@ -224,13 +126,6 @@ impl PyProjectManifest { .is_some_and(TomlManifest::has_workspace) } - /// Returns true if the pyproject.toml file also contains a pixi workspace. - pub fn has_pixi_package(&self) -> bool { - self.tool() - .and_then(|t| t.pixi.as_ref()) - .is_some_and(TomlManifest::has_package) - } - /// Assume that the manifest is a workspace manifest and convert it as such. /// /// If the manifest also contains a package section that will be converted @@ -240,44 +135,7 @@ impl PyProjectManifest { workspace: &WorkspaceManifest, root_directory: Option<&Path>, ) -> Result<(PackageManifest, Vec), TomlError> { - // Load the data nested under '[tool.pixi]' as pixi manifest - let Some(Tool { - pixi: Some(pixi), - poetry, - }) = self.tool - else { - return Err(TomlError::MissingField("tool.pixi".into(), None)); - }; - - // Extract some of the values we are interested in from the poetry table. - let poetry = poetry.unwrap_or_default(); - - // Extract the values we are interested in from the pyproject.toml - let project = self - .project - .project - .map(PyProjectFields::from) - .unwrap_or_default(); - - // Extract package defaults from [project] section - let package_defaults = PackageDefaults { - name: project.name.map(Spanned::take), - version: project - .version - .and_then(|v| v.take().to_string().parse().ok()) - .or(poetry.version.and_then(|v| v.parse().ok())), - description: project - .description - .map(Spanned::take) - .or(poetry.description), - authors: project.authors.map(contacts_to_authors).or(poetry.authors), - license: None, - license_file: None, - readme: None, - homepage: None, - repository: None, - documentation: None, - }; + let (pixi, _, package_defaults) = self.load_pixi_and_defaults()?; pixi.into_package_manifest( workspace.workspace_package_properties(), @@ -287,17 +145,10 @@ impl PyProjectManifest { ) } - #[allow(clippy::result_large_err)] - pub fn into_workspace_manifest( + /// Helper function to load the `[tool.pixi]` manifest and package defaults. + fn load_pixi_and_defaults( self, - root_directory: Option<&Path>, - ) -> Result<(WorkspaceManifest, Option, Vec), TomlError> { - let PyProjectToml { - project, - dependency_groups, - .. - } = self.project; - + ) -> Result<(TomlManifest, pyproject_toml::PyProjectToml, PackageDefaults), TomlError> { // Load the data nested under '[tool.pixi]' as pixi manifest let Some(Tool { pixi: Some(pixi), @@ -307,85 +158,53 @@ impl PyProjectManifest { return Err(TomlError::MissingField("tool.pixi".into(), None)); }; - // Extract the values we are interested in from the pyproject.toml - let project = project.map(PyProjectFields::from).unwrap_or_default(); - - // Extract some of the values we are interested in from the poetry table. let poetry = poetry.unwrap_or_default(); + let pyproject = self.project.into_inner(); + let package_defaults = get_package_defaults(&pyproject, &poetry); - // Define an iterator over both optional dependencies and dependency groups - let pypi_dependency_groups = - Self::extract_dependency_groups(dependency_groups, project.optional_dependencies)?; + Ok((pixi, pyproject, package_defaults)) + } + + #[allow(clippy::result_large_err)] + pub fn into_workspace_manifest( + self, + root_directory: Option<&Path>, + ) -> Result<(WorkspaceManifest, Option, Vec), TomlError> { + let (pixi, pyproject, package_defaults) = self.load_pixi_and_defaults()?; + let resolved = pyproject.resolve()?; + let mut groups = resolved.optional_dependencies; + groups.extend(resolved.dependency_groups); // Convert the TOML document into a pixi manifest. // TODO: would be nice to add license, license-file, readme, homepage, // repository, documentation, regarding the above, the types are a bit // different than we expect, so the conversion is not straightforward we // could change these types or we can convert. Let's decide when we make it. - // etc. - let implicit_pypi_features = pypi_dependency_groups - .iter() - .map(|(name, _)| { - ( - FeatureName::from(name.clone()), - Feature::new(FeatureName::from(name.clone())), - ) - }) + + let implicit_pypi_features = groups + .keys() + .map(|name| FeatureName::from(name.clone())) + .map(|name| (name.clone(), Feature::new(name))) .collect(); - // Extract and convert project authors to Vec format for reuse - let project_authors = project - .authors - .map(contacts_to_authors) - .or(poetry.authors.clone()); - - // Extract package defaults from [project] section - let package_defaults = PackageDefaults { - name: project.name.as_ref().map(|name| name.value.clone()), - version: project - .version - .as_ref() - .and_then(|v| v.value.to_string().parse().ok()) - .or(poetry.version.as_ref().and_then(|v| v.parse().ok())), - description: project - .description - .as_ref() - .map(|desc| desc.value.clone()) - .or(poetry.description.clone()), - authors: project_authors.clone(), - license: None, - license_file: None, - readme: None, - homepage: None, - repository: None, - documentation: None, - }; let (mut workspace_manifest, package_manifest, warnings) = pixi.into_workspace_manifest( ExternalWorkspaceProperties { - name: project.name.map(Spanned::take), - version: project - .version - .and_then(|v| v.take().to_string().parse().ok()) - .or(poetry.version.and_then(|v| v.parse().ok())), - description: project - .description - .map(Spanned::take) - .or(poetry.description), - authors: project_authors, - license: None, - license_file: None, - readme: None, - homepage: None, - repository: None, - documentation: None, + name: package_defaults.name.clone(), + version: package_defaults.version.clone(), + description: package_defaults.description.clone(), + authors: package_defaults.authors.clone(), features: implicit_pypi_features, + ..Default::default() }, package_defaults, root_directory, )?; // Add python as dependency based on the `project.requires_python` property - let python_spec = project.requires_python; + let python_spec = pyproject + .project + .as_ref() + .and_then(|p| p.requires_python.clone()); let target = workspace_manifest .default_feature_mut() @@ -397,7 +216,7 @@ impl PyProjectManifest { if !target.has_dependency(&python, SpecType::Run, None) { target.add_dependency( &python, - &version_or_url_to_spec(&python_spec.map(Spanned::take)).unwrap(), + &version_or_url_to_spec(&python_spec).unwrap(), SpecType::Run, ); } else if let Some(_spec) = python_spec { @@ -410,79 +229,65 @@ impl PyProjectManifest { } // Add pyproject dependencies as pypi dependencies - if let Some(deps) = project.dependencies { - for requirement in deps.iter() { + if let Some(deps) = pyproject.project.and_then(|p| p.dependencies) { + groups + .entry("default".to_string()) + .or_default() + .extend(deps); + } + + // For each group of (optional) dependencies or dependency group, add pypi + // dependencies + for (group, reqs) in groups.iter() { + let feature_name = FeatureName::from(group.as_str()); + let target = workspace_manifest.target_mut(None, &feature_name).unwrap(); + + for requirement in reqs.iter() { target .try_add_pep508_dependency( - &requirement.value, + requirement, None, DependencyOverwriteBehavior::Error, ) - .map_err(|err| { - GenericError::new(format!("{}", err)).with_span(requirement.span.into()) - })?; - } - } - - // For each group of optional dependency or dependency group, add pypi - // dependencies, filtering out self-references in optional dependencies - let project_name = workspace_manifest - .workspace - .name - .clone() - .and_then(|name| pep508_rs::PackageName::new(name).ok()); - for (group, reqs) in pypi_dependency_groups { - let feature_name = FeatureName::from(group.to_string()); - let target = workspace_manifest - .features - .entry(feature_name.clone()) - .or_insert_with(move || Feature::new(feature_name)) - .targets - .default_mut(); - for requirement in reqs.iter() { - // filter out any self references in groups of extra dependencies - if project_name.as_ref() != Some(&requirement.name) { - target - .try_add_pep508_dependency( - requirement, - None, - DependencyOverwriteBehavior::Error, - ) - .map_err(|err| GenericError::new(format!("{}", err)))?; - } + .map_err(|err| GenericError::new(format!("{}", err)))?; } } Ok((workspace_manifest, package_manifest, warnings)) } +} - fn extract_dependency_groups( - dependency_groups: Option>, - optional_dependencies: Option>>>, - ) -> Result)>, TomlError> { - Ok(optional_dependencies - .map(|deps| { - deps.into_iter() - .map(|(group, reqs)| { - ( - group, - reqs.into_iter().map(Spanned::take).collect::>(), - ) +/// Returns default package data from the pyproject.toml project section or the +/// poetry section. +fn get_package_defaults( + pyproject: &pyproject_toml::PyProjectToml, + poetry: &ToolPoetry, +) -> PackageDefaults { + let project = pyproject.project.as_ref(); + + PackageDefaults { + name: project.map(|p| p.name.clone()), + version: project + .and_then(|p| p.version.clone()) + .and_then(|v| v.to_string().parse().ok()) + .or(poetry.version.as_ref().and_then(|v| v.parse().ok())), + description: project + .and_then(|p| p.description.clone()) + .or(poetry.description.clone()), + authors: project + .and_then(|p| p.authors.clone()) + .map(|authors| { + authors + .into_iter() + .map(|contact| match contact { + Contact::NameEmail { name, email } => format!("{} <{}>", name, email), + Contact::Name { name } => name, + Contact::Email { email } => email, }) .collect() }) - .into_iter() - .chain( - dependency_groups - .map(|Spanned { span, value }| { - value.0.resolve().map_err(|err| { - GenericError::new(format!("{}", err)).with_span(span.into()) - }) - }) - .transpose()?, - ) - .flat_map(|map| map.into_iter()) - .collect::>()) + .or(poetry.authors.clone()), + ..Default::default() } } @@ -504,19 +309,6 @@ fn version_or_url_to_spec( } } -/// Converts [`Contact`] from pyproject.toml to a representation that is used in -/// pixi. -fn contacts_to_authors(contacts: Vec>) -> Vec { - contacts - .into_iter() - .map(|contact| match contact.take().into_inner() { - Contact::NameEmail { name, email } => format!("{} <{}>", name, email), - Contact::Name { name } => name.clone(), - Contact::Email { email } => email.clone(), - }) - .collect() -} - #[cfg(test)] mod tests { use std::str::FromStr; diff --git a/crates/pixi_manifest/src/toml/pyproject.rs b/crates/pixi_manifest/src/toml/pyproject.rs index 9411329806..0348faa020 100644 --- a/crates/pixi_manifest/src/toml/pyproject.rs +++ b/crates/pixi_manifest/src/toml/pyproject.rs @@ -8,7 +8,8 @@ use pep440_rs::{Version, VersionSpecifiers}; use pep508_rs::Requirement; use pixi_toml::{DeserializeAs, Same, TomlFromStr, TomlIndexMap, TomlWith}; use pyproject_toml::{ - BuildSystem, Contact, DependencyGroupSpecifier, DependencyGroups, License, Project, ReadMe, + self, BuildSystem, Contact, DependencyGroupSpecifier, DependencyGroups, License, Project, + ReadMe, }; use toml_span::{ DeserError, Deserialize, Error, ErrorKind, Spanned, Value, @@ -25,6 +26,19 @@ pub struct PyProjectToml { pub dependency_groups: Option>, } +impl PyProjectToml { + pub fn into_inner(self) -> pyproject_toml::PyProjectToml { + pyproject_toml::PyProjectToml { + project: self.project.map(TomlProject::into_inner), + build_system: self.build_system.map(TomlBuildSystem::into_inner), + dependency_groups: self + .dependency_groups + .map(Spanned::take) + .map(TomlDependencyGroups::into_inner), + } + } +} + impl<'de> toml_span::Deserialize<'de> for PyProjectToml { fn deserialize(value: &mut Value<'de>) -> Result { let mut th = TableHelper::new(value)?; diff --git a/docs/python/pyproject_toml.md b/docs/python/pyproject_toml.md index ca2f756ef8..cdd89c03dc 100644 --- a/docs/python/pyproject_toml.md +++ b/docs/python/pyproject_toml.md @@ -142,7 +142,7 @@ platforms = ["linux-64"] # if executed on linux [tool.pixi.environments] default = {features = [], solve-group = "default"} test = {features = ["test"], solve-group = "default"} -all = {features = ["all", "test"], solve-group = "default"} +all = {features = ["all"], solve-group = "default"} ``` In this example, three environments will be created by pixi: