diff --git a/src/cli/init.rs b/src/cli/init.rs index e6f075d06..4a91352b6 100644 --- a/src/cli/init.rs +++ b/src/cli/init.rs @@ -1,6 +1,6 @@ use crate::config::Config; use crate::environment::{get_up_to_date_prefix, LockFileUsage}; -use crate::project::manifest::pyproject; +use crate::project::manifest::pyproject::PyProjectToml; use crate::utils::conda_environment_file::CondaEnvFile; use crate::{config::get_default_author, consts}; use crate::{FeatureName, Project}; @@ -64,7 +64,7 @@ platforms = {{ platforms }} {%- if loop.first %} [tool.pixi.environments] -default = { features = [], solve-group = "default" } +default = { solve-group = "default" } {%- endif %} {{env}} = { features = {{ features }}, solve-group = "default" } {%- endfor %} @@ -157,10 +157,10 @@ pub async fn execute(args: Args) -> miette::Result<()> { // Inject a tool.pixi.project section into an existing pyproject.toml file if there is one without '[tool.pixi.project]' if pyproject_manifest_path.is_file() { - let file = fs::read_to_string(pyproject_manifest_path.clone()).unwrap(); + let file = fs::read_to_string(&pyproject_manifest_path).unwrap(); - // Early exit if 'pyproject.toml' already contains a '[tool.pixi.project]' section - if file.contains("[tool.pixi.project]") { + // Early exit if 'pyproject.toml' already contains a '[tool.pixi.project]' table + if PyProjectToml::is_pixi_str(&file)? { eprintln!( "{}Nothing to do here: 'pyproject.toml' already contains a '[tool.pixi.project]' section.", console::style(console::Emoji("🤔 ", "")).blue(), @@ -168,9 +168,9 @@ pub async fn execute(args: Args) -> miette::Result<()> { return Ok(()); } - let pyproject = pyproject::pyproject(&file)?; - let name = pyproject.project.as_ref().unwrap().name.clone(); - let environments = pyproject::environments_from_extras(&pyproject); + let pyproject = PyProjectToml::from(&file)?; + let name = pyproject.name(); + let environments = pyproject.environments_from_extras(); let rv = env .render_named_str( consts::PYPROJECT_MANIFEST, diff --git a/src/project/manifest/pyproject.rs b/src/project/manifest/pyproject.rs index 025bc0205..e5cccd99b 100644 --- a/src/project/manifest/pyproject.rs +++ b/src/project/manifest/pyproject.rs @@ -1,13 +1,15 @@ use miette::Report; use pep508_rs::VersionOrUrl; -use pyproject_toml::PyProjectToml; +use pyproject_toml::{self, Project}; use rattler_conda_types::{NamelessMatchSpec, PackageName, ParseStrictness::Lenient, VersionSpec}; use serde::Deserialize; +use std::fs; +use std::path::PathBuf; use std::{ collections::{HashMap, HashSet}, str::FromStr, }; -use toml_edit; +use toml_edit::DocumentMut; use crate::FeatureName; @@ -20,7 +22,7 @@ use super::{ #[derive(Deserialize, Debug, Clone)] pub struct PyProjectManifest { #[serde(flatten)] - inner: PyProjectToml, + inner: pyproject_toml::PyProjectToml, tool: Tool, } @@ -30,7 +32,7 @@ struct Tool { } impl std::ops::Deref for PyProjectManifest { - type Target = PyProjectToml; + type Target = pyproject_toml::PyProjectToml; fn deref(&self) -> &Self::Target { &self.inner @@ -147,46 +149,83 @@ fn version_or_url_to_nameless_matchspec( } } -/// Builds a list of pixi environments from pyproject groups of extra dependencies: -/// - one environment is created per group of extra, with the same name as the group of extra -/// - each environment includes the feature of the same name as the group of extra -/// - it will also include other features inferred from any self references to other groups of extras -pub fn environments_from_extras(pyproject: &PyProjectToml) -> HashMap> { - let mut environments = HashMap::new(); - if let Some(Some(extras)) = &pyproject.project.as_ref().map(|p| &p.optional_dependencies) { - let pname = &pyproject - .project - .as_ref() - .map(|p| pep508_rs::PackageName::new(p.name.clone()).unwrap()); - 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().is_some_and(|n| n == &req.name) { - for extra in &req.extras { - features.push(extra.to_string()) - } +/// A struct wrapping pyproject_toml::PyProjectToml +/// ensuring it has a project table +/// +/// This is used during 'pixi init' to parse a potentially non-pixi 'pyproject.toml' +pub struct PyProjectToml { + inner: pyproject_toml::PyProjectToml, +} + +impl PyProjectToml { + /// Parses a non-pixi pyproject.toml string into a PyProjectToml struct + /// making sure it contains a 'project' table + pub fn from(source: &str) -> Result { + match toml_edit::de::from_str::(source) + .map_err(TomlError::from) + { + Err(e) => e.to_fancy("pyproject.toml", source), + Ok(pyproject) => { + // Make sure [project] exists in pyproject.toml, + // This will ensure project.name is defined + if pyproject.project.is_none() { + TomlError::NoProjectTable.to_fancy("pyproject.toml", source) + } else { + Ok(PyProjectToml { inner: pyproject }) } } - environments.insert(extra.clone(), features); } } - environments -} -/// Parses a non-pixi pyproject.toml string. -pub fn pyproject(source: &str) -> Result { - match toml_edit::de::from_str::(source).map_err(TomlError::from) { - Err(e) => e.to_fancy("pyproject.toml", source), - Ok(pyproject) => { - // Make sure [project] exists in pyproject.toml, - // This will ensure project.name is defined - if pyproject.project.is_none() { - TomlError::NoProjectTable.to_fancy("pyproject.toml", source) - } else { - Ok(pyproject) + pub fn name(&self) -> String { + self.project().name.clone() + } + + pub fn project(&self) -> &Project { + self.inner.project.as_ref().unwrap() + } + + /// Builds a list of pixi environments from pyproject groups of extra dependencies: + /// - one environment is created per group of extra, with the same name as the group of extra + /// - each environment includes the feature of the same name as the group of extra + /// - it will also include other features inferred from any self references to other groups of extras + pub fn environments_from_extras(&self) -> HashMap> { + let mut environments = HashMap::new(); + if let Some(extras) = &self.project().optional_dependencies { + let pname = pep508_rs::PackageName::new(self.name()).unwrap(); + 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 == req.name { + for extra in &req.extras { + features.push(extra.to_string()) + } + } + } + environments.insert(extra.clone(), features); } } + environments + } + + /// Checks whether a path is a valid `pyproject.toml` for use with pixi by checking if it + /// contains a `[tool.pixi.project]` item. + pub fn is_pixi(path: &PathBuf) -> bool { + let source = fs::read_to_string(path).unwrap(); + Self::is_pixi_str(&source).unwrap_or(false) + } + /// Checks whether a string is a valid `pyproject.toml` for use with pixi by checking if it + /// contains a `[tool.pixi.project]` item. + pub fn is_pixi_str(source: &str) -> Result { + match source.parse::().map_err(TomlError::from) { + Err(e) => e.to_fancy("pyproject.toml", source), + Ok(doc) => Ok(doc + .get("tool") + .and_then(|t| t.get("pixi")) + .and_then(|p| p.get("project")) + .is_some()), + } } } diff --git a/src/project/mod.rs b/src/project/mod.rs index 44b198aa2..3212bff4d 100644 --- a/src/project/mod.rs +++ b/src/project/mod.rs @@ -13,7 +13,6 @@ use miette::{IntoDiagnostic, NamedSource}; use rattler_conda_types::{Channel, Platform, Version}; use reqwest_middleware::ClientWithMiddleware; -use std::fs; use std::hash::Hash; use rattler_virtual_packages::VirtualPackage; @@ -41,7 +40,7 @@ pub use dependencies::Dependencies; pub use environment::Environment; pub use solve_group::SolveGroup; -use self::manifest::Environments; +use self::manifest::{pyproject::PyProjectToml, Environments}; /// The dependency types we support #[derive(Debug, Copy, Clone)] @@ -164,7 +163,7 @@ impl Project { if let Some(project_toml) = project_toml { if env_manifest_path != project_toml.to_string_lossy() { tracing::warn!( - "Using mainfest {} from `PIXI_PROJECT_MANIFEST` rather than local {}", + "Using manifest {} from `PIXI_PROJECT_MANIFEST` rather than local {}", env_manifest_path, project_toml.to_string_lossy() ); @@ -515,7 +514,7 @@ pub fn find_project_manifest() -> Option { if path.is_file() { match *manifest { PROJECT_MANIFEST => Some(path.to_path_buf()), - PYPROJECT_MANIFEST if is_valid_pixi_pyproject_toml(&path) => { + PYPROJECT_MANIFEST if PyProjectToml::is_pixi(&path) => { Some(path.to_path_buf()) } _ => None, @@ -527,14 +526,6 @@ pub fn find_project_manifest() -> Option { }) } -/// Checks whether a path is a valid `pyproject.toml` for use with pixi file by checking if it -/// contains the `[tool.pixi.project]` section. -fn is_valid_pixi_pyproject_toml(path: &Path) -> bool { - fs::read_to_string(path) - .map(|content| content.contains("[tool.pixi.project]")) - .unwrap_or(false) -} - #[cfg(test)] mod tests { use super::*;