From 9191a032da077019d8d72ca12be781631027152d Mon Sep 17 00:00:00 2001 From: Olivier Lacroix Date: Thu, 28 Mar 2024 23:30:44 +1100 Subject: [PATCH 01/17] Create features and environments from extras --- src/cli/init.rs | 28 +++++++++++++++++++++++++++- src/project/manifest/pyproject.rs | 23 ++++++++++++++++++++--- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/src/cli/init.rs b/src/cli/init.rs index edf27e0781..b18ae5890c 100644 --- a/src/cli/init.rs +++ b/src/cli/init.rs @@ -10,9 +10,11 @@ use indexmap::IndexMap; use itertools::Itertools; use miette::IntoDiagnostic; use minijinja::{context, Environment}; +use pyproject_toml::PyProjectToml; use rattler_conda_types::ParseStrictness::{Lenient, Strict}; use rattler_conda_types::{Channel, MatchSpec, Platform}; use regex::Regex; +use std::collections::HashMap; use std::io::{Error, ErrorKind, Write}; use std::path::Path; use std::str::FromStr; @@ -62,6 +64,13 @@ const PYROJECT_TEMPLATE: &str = r#" name = "{{ name }}" channels = [{%- if channels %}"{{ channels|join("\", \"") }}"{%- endif %}] platforms = ["{{ platforms|join("\", \"") }}"] +{%- for env, features in environments|items %} +{%- if loop.first %} + +[tool.pixi.environments] +{%- endif %} +{{env}} = {features = {{features}}, solve-group = "default"} +{%- endfor %} "#; @@ -159,6 +168,8 @@ pub async fn execute(args: Args) -> miette::Result<()> { let path = dir.join(consts::PYPROJECT_MANIFEST); let file = fs::read_to_string(path.clone()).unwrap(); if !file.contains("[tool.pixi.project]") { + let pyproject = PyProjectToml::new(&file).unwrap(); + let environments = environments_from_extras(&pyproject); let rv = env .render_named_str( consts::PYPROJECT_MANIFEST, @@ -166,7 +177,8 @@ pub async fn execute(args: Args) -> miette::Result<()> { context! { name, channels, - platforms + platforms, + environments, }, ) .unwrap(); @@ -388,6 +400,20 @@ fn parse_channels(channels: Vec) -> Vec { new_channels } +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) { + for (extra, _reqs) in extras { + let features = vec![extra.to_string()]; + // TODO add self references + // for req in reqs.iter() { + // if req.name == PackageName::from_str(self.project.name) {} + // } + environments.insert(extra.clone(), features); + } + } + environments +} #[cfg(test)] mod tests { use super::*; diff --git a/src/project/manifest/pyproject.rs b/src/project/manifest/pyproject.rs index a6bedc9d68..1eaeef83e4 100644 --- a/src/project/manifest/pyproject.rs +++ b/src/project/manifest/pyproject.rs @@ -5,6 +5,8 @@ use std::str::FromStr; use toml_edit; use toml_edit::TomlError; +use crate::FeatureName; + use super::{ error::RequirementConversionError, python::PyPiPackageName, ProjectManifest, PyPiRequirement, SpecType, @@ -74,9 +76,24 @@ impl From for ProjectManifest { } // For each extra group, create a feature of the same name if it does not exist, - // add dependencies and create corresponding environments if they do not exist - // TODO: Add solve groups as well? - // TODO: Deal with self referencing extras? + // and add pypi dependencies from project.optional-dependencies + if let Some(Some(extras)) = &item.project.as_ref().map(|p| &p.optional_dependencies) { + for (extra, reqs) in extras { + let target = manifest + .features + .entry(FeatureName::Named(extra.to_string())) + .or_default() + .targets + .default_mut(); + for req in reqs.iter() { + // TODO: filter out self references + target.add_pypi_dependency( + PyPiPackageName::from_normalized(req.name.clone()), + PyPiRequirement::from(req.clone()), + ) + } + } + } manifest } From 47280435cc0b0f96c7f088b9628425a639894ef7 Mon Sep 17 00:00:00 2001 From: Olivier Lacroix Date: Fri, 29 Mar 2024 15:50:18 +1100 Subject: [PATCH 02/17] Handle self-refencing extras --- src/cli/init.rs | 21 +++++++++++++++------ src/project/manifest/pyproject.rs | 9 ++++++++- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/cli/init.rs b/src/cli/init.rs index b18ae5890c..3cb3508d0f 100644 --- a/src/cli/init.rs +++ b/src/cli/init.rs @@ -68,6 +68,7 @@ platforms = ["{{ platforms|join("\", \"") }}"] {%- if loop.first %} [tool.pixi.environments] +default = {features = [], solve-group = "default"} {%- endif %} {{env}} = {features = {{features}}, solve-group = "default"} {%- endfor %} @@ -403,12 +404,20 @@ fn parse_channels(channels: Vec) -> Vec { 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) { - for (extra, _reqs) in extras { - let features = vec![extra.to_string()]; - // TODO add self references - // for req in reqs.iter() { - // if req.name == PackageName::from_str(self.project.name) {} - // } + 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()) + } + } + } environments.insert(extra.clone(), features); } } diff --git a/src/project/manifest/pyproject.rs b/src/project/manifest/pyproject.rs index 1eaeef83e4..2ed8d80934 100644 --- a/src/project/manifest/pyproject.rs +++ b/src/project/manifest/pyproject.rs @@ -85,8 +85,15 @@ impl From for ProjectManifest { .or_default() .targets .default_mut(); + let pname = &item + .project + .as_ref() + .map(|p| pep508_rs::PackageName::new(p.name.clone()).unwrap()); for req in reqs.iter() { - // TODO: filter out self references + // filter out any self references in groups of extra dependencies + if pname.as_ref().is_some_and(|n| n == &req.name) { + continue; + } target.add_pypi_dependency( PyPiPackageName::from_normalized(req.name.clone()), PyPiRequirement::from(req.clone()), From b6c3f3505990f92a5556e3e2eabb2813965b118f Mon Sep 17 00:00:00 2001 From: Olivier Lacroix Date: Fri, 29 Mar 2024 16:35:24 +1100 Subject: [PATCH 03/17] Add documentation --- docs/advanced/pyproject_toml.md | 44 +++++++++++++++++++++++++++++++++ src/cli/init.rs | 4 +++ 2 files changed, 48 insertions(+) diff --git a/docs/advanced/pyproject_toml.md b/docs/advanced/pyproject_toml.md index c55f362e02..88d745eb79 100644 --- a/docs/advanced/pyproject_toml.md +++ b/docs/advanced/pyproject_toml.md @@ -104,6 +104,50 @@ matplotlib = "*" This would result in the conda dependencies being installed and the pypi dependencies being ignored. As pixi takes the conda dependencies over the pypi dependencies. +## Optional dependencies +If your python project includes groups of optional dependencies, pixi will automatically interpret them as pixi features of the same name with the associated pypi dependencies. + +You can add them to pixi environments manually, or use `pixi init` to setup the project, which will create one environment per feature. Self-references to other groups of optional dependencies are also handled. + +For instance, imagine you have a project folder with a `pyproject.toml` file similar to: +```toml title="pyproject.toml" +[project] +name = "my_project" +dependencies = ["package1"] + +[project.optional-dependencies] +test = ["pytest"] +all = ["package2","my_project[test]"] +``` + +Running `pixi init` in that project folder will transform the `pyproject.toml` file into: +```toml title="pyproject.toml" +[project] +name = "my_project" +dependencies = ["package1"] + +[project.optional-dependencies] +test = ["pytest"] +all = ["package2","my_project[test]"] + +[tool.pixi.project] +name = "my_project" +channels = ["conda-forge"] +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"} +``` +In this example, three environments will be created by pixi: + + - **default** with 'package1' as pypi dependency + - **test** with 'package1' and 'pytest' as pypi dependencies + - **all** with 'package1', 'package2' and 'pytest' as pypi dependencies + +All environments will be solved together, as indicated by the common `solve-group`, and added to the lock file. You can edit the `[tool.pixi.environments]` section manually to adapt it to your use case (e.g. if you do not need a particular environement). + ## Example As the `pyproject.toml` file supports the full pixi spec with `[tool.pixi]` prepended an example would look like this: ```toml title="pyproject.toml" diff --git a/src/cli/init.rs b/src/cli/init.rs index 3cb3508d0f..87cc599dba 100644 --- a/src/cli/init.rs +++ b/src/cli/init.rs @@ -401,6 +401,10 @@ fn parse_channels(channels: Vec) -> Vec { new_channels } +/// 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) { From 46fee3db7bdcdaf891ff576f135d4dce73c4ff43 Mon Sep 17 00:00:00 2001 From: Olivier Lacroix Date: Fri, 29 Mar 2024 16:49:22 +1100 Subject: [PATCH 04/17] lint --- docs/advanced/pyproject_toml.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/advanced/pyproject_toml.md b/docs/advanced/pyproject_toml.md index 88d745eb79..b50262d522 100644 --- a/docs/advanced/pyproject_toml.md +++ b/docs/advanced/pyproject_toml.md @@ -110,7 +110,7 @@ If your python project includes groups of optional dependencies, pixi will autom You can add them to pixi environments manually, or use `pixi init` to setup the project, which will create one environment per feature. Self-references to other groups of optional dependencies are also handled. For instance, imagine you have a project folder with a `pyproject.toml` file similar to: -```toml title="pyproject.toml" +```toml [project] name = "my_project" dependencies = ["package1"] @@ -120,8 +120,8 @@ test = ["pytest"] all = ["package2","my_project[test]"] ``` -Running `pixi init` in that project folder will transform the `pyproject.toml` file into: -```toml title="pyproject.toml" +Running `pixi init` in that project folder will transform the `pyproject.toml` file into: +```toml [project] name = "my_project" dependencies = ["package1"] @@ -146,7 +146,7 @@ In this example, three environments will be created by pixi: - **test** with 'package1' and 'pytest' as pypi dependencies - **all** with 'package1', 'package2' and 'pytest' as pypi dependencies -All environments will be solved together, as indicated by the common `solve-group`, and added to the lock file. You can edit the `[tool.pixi.environments]` section manually to adapt it to your use case (e.g. if you do not need a particular environement). +All environments will be solved together, as indicated by the common `solve-group`, and added to the lock file. You can edit the `[tool.pixi.environments]` section manually to adapt it to your use case (e.g. if you do not need a particular environment). ## Example As the `pyproject.toml` file supports the full pixi spec with `[tool.pixi]` prepended an example would look like this: From bbe7d9e0309fcd2051ecdf10402904a32754587e Mon Sep 17 00:00:00 2001 From: Olivier Lacroix Date: Sun, 31 Mar 2024 13:37:27 +1100 Subject: [PATCH 05/17] Simplify project templates --- src/cli/init.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/cli/init.rs b/src/cli/init.rs index 87cc599dba..d2fd30c823 100644 --- a/src/cli/init.rs +++ b/src/cli/init.rs @@ -51,8 +51,8 @@ description = "Add a short description here" {%- if author %} authors = ["{{ author[0] }} <{{ author[1] }}>"] {%- endif %} -channels = [{%- if channels %}"{{ channels|join("\", \"") }}"{%- endif %}] -platforms = ["{{ platforms|join("\", \"") }}"] +channels = {{ channels }} +platforms = {{ platforms }} [tasks] @@ -62,15 +62,15 @@ platforms = ["{{ platforms|join("\", \"") }}"] const PYROJECT_TEMPLATE: &str = r#" [tool.pixi.project] name = "{{ name }}" -channels = [{%- if channels %}"{{ channels|join("\", \"") }}"{%- endif %}] -platforms = ["{{ platforms|join("\", \"") }}"] +channels = {{ channels }} +platforms = {{ platforms }} {%- for env, features in environments|items %} {%- if loop.first %} [tool.pixi.environments] -default = {features = [], solve-group = "default"} +default = { features = [], solve-group = "default" } {%- endif %} -{{env}} = {features = {{features}}, solve-group = "default"} +{{env}} = { features = {{ features }}, solve-group = "default" } {%- endfor %} "#; From 551f62764bb92adc849828d5e9af556ce47f85f7 Mon Sep 17 00:00:00 2001 From: Olivier Lacroix Date: Mon, 1 Apr 2024 19:06:28 +1100 Subject: [PATCH 06/17] Add a "default" SolveGroup to the default Environment (if not redefined) --- docs/configuration.md | 7 +++++ src/cli/init.rs | 1 - src/project/manifest/mod.rs | 59 +++++++++++++++++++------------------ src/project/solve_group.rs | 11 ++++--- 4 files changed, 45 insertions(+), 33 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 51f2d54541..e764e4d4e1 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -564,6 +564,13 @@ prod = {features = ["prod"], solve-group = "test"} lint = ["lint"] ``` +Unless the `default` environment is redefined, it is included in the `default` solve group. That is, the following is implied unless overridden in the manifest's `[environments]` table: +```toml title="default environment" +[environments] +default = { features = [], solve-group = "default" } # the default environment includes the 'default' feature only +``` + + ## Global configuration The global configuration options are documented in the [global configuration](advanced/global_configuration.md) section. diff --git a/src/cli/init.rs b/src/cli/init.rs index d2fd30c823..05b2b32e98 100644 --- a/src/cli/init.rs +++ b/src/cli/init.rs @@ -68,7 +68,6 @@ platforms = {{ platforms }} {%- if loop.first %} [tool.pixi.environments] -default = { features = [], solve-group = "default" } {%- endif %} {{env}} = { features = {{ features }}, solve-group = "default" } {%- endfor %} diff --git a/src/project/manifest/mod.rs b/src/project/manifest/mod.rs index 8cb16fe8fb..ffec46bc5a 100644 --- a/src/project/manifest/mod.rs +++ b/src/project/manifest/mod.rs @@ -750,6 +750,31 @@ impl SolveGroups { pub fn iter(&self) -> impl Iterator + '_ { self.solve_groups.iter() } + + /// Adds an environment (by index) to a solve-group. + /// If the solve-group does not exist, it is created + /// + /// Returns the index of the solve-group + pub fn add(&mut self, name: &str, environment_idx: usize) -> usize { + match self.by_name.get(name) { + Some(idx) => { + // The solve-group exists, add the environment index to it + self.solve_groups[*idx].environments.push(environment_idx); + *idx + } + None => { + // The solve-group does not exist, create it + // and initialise it with the environment index + let idx = self.solve_groups.len(); + self.solve_groups.push(SolveGroup { + name: name.to_string(), + environments: vec![environment_idx], + }); + self.by_name.insert(name.to_string(), idx); + idx + } + } + } } /// Describes the contents of a project manifest. @@ -1022,16 +1047,15 @@ impl<'de> Deserialize<'de> for ProjectManifest { name: EnvironmentName::Default, features: Vec::new(), features_source_loc: None, - solve_group: None, + solve_group: Some(solve_groups.add("default", 0)), }); environments.by_name.insert(EnvironmentName::Default, 0); } // Add all named environments + // Note: if the default Environment is redefined, but does not specify a solve-group, + // it is not added to the 'default' solve-group. for (name, env) in toml_manifest.environments { - let environment_idx = environments.environments.len(); - environments.by_name.insert(name.clone(), environment_idx); - // Decompose the TOML let (features, features_source_loc, solve_group) = match env { TomlEnvironmentMapOrSeq::Map(env) => { @@ -1040,34 +1064,13 @@ impl<'de> Deserialize<'de> for ProjectManifest { TomlEnvironmentMapOrSeq::Seq(features) => (features, None, None), }; - // Add to the solve group if defined - let solve_group = if let Some(solve_group) = solve_group { - Some(match solve_groups.by_name.get(&solve_group) { - Some(idx) => { - solve_groups.solve_groups[*idx] - .environments - .push(environment_idx); - *idx - } - None => { - let idx = solve_groups.solve_groups.len(); - solve_groups.solve_groups.push(SolveGroup { - name: solve_group.clone(), - environments: vec![environment_idx], - }); - solve_groups.by_name.insert(solve_group, idx); - idx - } - }) - } else { - None - }; - + let environment_idx = environments.environments.len(); + environments.by_name.insert(name.clone(), environment_idx); environments.environments.push(Environment { name, features, features_source_loc, - solve_group, + solve_group: solve_group.map(|sg| solve_groups.add(&sg, environment_idx)), }); } diff --git a/src/project/solve_group.rs b/src/project/solve_group.rs index 32671a3af6..aabac2539a 100644 --- a/src/project/solve_group.rs +++ b/src/project/solve_group.rs @@ -223,10 +223,10 @@ mod tests { let foo_environment = project.environment("foo").unwrap(); let bar_environment = project.environment("bar").unwrap(); - let solve_groups = project.solve_groups(); - assert_eq!(solve_groups.len(), 1); + let solve_groups = project.solve_groups(); // contains 'default', 'group1' + assert_eq!(solve_groups.len(), 2); - let solve_group = solve_groups[0].clone(); + let solve_group = solve_groups[1].clone(); // 'group1' solve group let solve_group_envs = solve_group.environments().collect_vec(); assert_eq!(solve_group_envs.len(), 2); assert_eq!(solve_group_envs[0].name(), "foo"); @@ -235,7 +235,10 @@ mod tests { // Make sure that the environments properly reference the group assert_eq!(foo_environment.solve_group(), Some(solve_group.clone())); assert_eq!(bar_environment.solve_group(), Some(solve_group.clone())); - assert_eq!(default_environment.solve_group(), None); + assert_eq!( + default_environment.solve_group(), + Some(solve_groups[0].clone()) + ); // Make sure that all the environments share the same system requirements, because they are // in the same solve-group. From 5fe6b890c5d4fc286187cc3f5562051e13d6f061 Mon Sep 17 00:00:00 2001 From: Olivier Lacroix Date: Tue, 2 Apr 2024 21:04:17 +1100 Subject: [PATCH 07/17] Move environment extraction from extras to pyproject.rs --- src/cli/init.rs | 28 +------------------------ src/project/manifest/mod.rs | 2 +- src/project/manifest/pyproject.rs | 34 ++++++++++++++++++++++++++++--- 3 files changed, 33 insertions(+), 31 deletions(-) diff --git a/src/cli/init.rs b/src/cli/init.rs index d79eb48907..9aa02a9d48 100644 --- a/src/cli/init.rs +++ b/src/cli/init.rs @@ -1,5 +1,6 @@ use crate::config::Config; use crate::environment::{get_up_to_date_prefix, LockFileUsage}; +use crate::project::manifest::pyproject::environments_from_extras; use crate::utils::conda_environment_file::CondaEnvFile; use crate::{config::get_default_author, consts}; use crate::{FeatureName, Project}; @@ -9,7 +10,6 @@ use miette::IntoDiagnostic; use minijinja::{context, Environment}; use pyproject_toml::PyProjectToml; use rattler_conda_types::Platform; -use std::collections::HashMap; use std::io::{Error, ErrorKind, Write}; use std::path::Path; use std::{fs, path::PathBuf}; @@ -291,32 +291,6 @@ fn get_dir(path: PathBuf) -> Result { } } -/// 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()) - } - } - } - environments.insert(extra.clone(), features); - } - } - environments -} #[cfg(test)] mod tests { use super::*; diff --git a/src/project/manifest/mod.rs b/src/project/manifest/mod.rs index ca9c72d3ee..99d220e9c6 100644 --- a/src/project/manifest/mod.rs +++ b/src/project/manifest/mod.rs @@ -5,7 +5,7 @@ mod environment; mod error; mod feature; mod metadata; -mod pyproject; +pub mod pyproject; pub mod python; mod system_requirements; mod target; diff --git a/src/project/manifest/pyproject.rs b/src/project/manifest/pyproject.rs index 2ed8d80934..640e424020 100644 --- a/src/project/manifest/pyproject.rs +++ b/src/project/manifest/pyproject.rs @@ -1,7 +1,8 @@ use pep508_rs::VersionOrUrl; +use pyproject_toml::PyProjectToml; use rattler_conda_types::{NamelessMatchSpec, PackageName, ParseStrictness::Lenient, VersionSpec}; use serde::Deserialize; -use std::str::FromStr; +use std::{collections::HashMap, str::FromStr}; use toml_edit; use toml_edit::TomlError; @@ -15,7 +16,7 @@ use super::{ #[derive(Deserialize, Debug, Clone)] pub struct PyProjectManifest { #[serde(flatten)] - inner: pyproject_toml::PyProjectToml, + inner: PyProjectToml, tool: Tool, } @@ -25,7 +26,7 @@ struct Tool { } impl std::ops::Deref for PyProjectManifest { - type Target = pyproject_toml::PyProjectToml; + type Target = PyProjectToml; fn deref(&self) -> &Self::Target { &self.inner @@ -126,6 +127,33 @@ 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()) + } + } + } + environments.insert(extra.clone(), features); + } + } + environments +} + #[cfg(test)] mod tests { use std::path::Path; From bd7a8fc6e6ece28adc9efeb88c4ccf814b6e7465 Mon Sep 17 00:00:00 2001 From: Olivier Lacroix Date: Thu, 4 Apr 2024 20:04:07 +1100 Subject: [PATCH 08/17] Update docs/advanced/pyproject_toml.md Co-authored-by: Ruben Arts --- docs/advanced/pyproject_toml.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced/pyproject_toml.md b/docs/advanced/pyproject_toml.md index b50262d522..0eec871924 100644 --- a/docs/advanced/pyproject_toml.md +++ b/docs/advanced/pyproject_toml.md @@ -105,7 +105,7 @@ This would result in the conda dependencies being installed and the pypi depende As pixi takes the conda dependencies over the pypi dependencies. ## Optional dependencies -If your python project includes groups of optional dependencies, pixi will automatically interpret them as pixi features of the same name with the associated pypi dependencies. +If your python project includes groups of optional dependencies, pixi will automatically interpret them as [pixi features](../configuration.md#the-feature-table) of the same name with the associated `pypi-dependencies`. You can add them to pixi environments manually, or use `pixi init` to setup the project, which will create one environment per feature. Self-references to other groups of optional dependencies are also handled. From 9077bdda7a953bc8fc4951ef324cf68c2c8f1787 Mon Sep 17 00:00:00 2001 From: Olivier Lacroix Date: Fri, 5 Apr 2024 09:44:27 +1100 Subject: [PATCH 09/17] Revert "Add a "default" SolveGroup to the default Environment (if not redefined)" This reverts commit 551f62764bb92adc849828d5e9af556ce47f85f7. --- src/cli/init.rs | 1 + src/project/manifest/mod.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/cli/init.rs b/src/cli/init.rs index 9aa02a9d48..8726b10db8 100644 --- a/src/cli/init.rs +++ b/src/cli/init.rs @@ -63,6 +63,7 @@ platforms = {{ platforms }} {%- if loop.first %} [tool.pixi.environments] +default = { features = [], solve-group = "default" } {%- endif %} {{env}} = { features = {{ features }}, solve-group = "default" } {%- endfor %} diff --git a/src/project/manifest/mod.rs b/src/project/manifest/mod.rs index 99d220e9c6..e95c903df9 100644 --- a/src/project/manifest/mod.rs +++ b/src/project/manifest/mod.rs @@ -1053,7 +1053,7 @@ impl<'de> Deserialize<'de> for ProjectManifest { name: EnvironmentName::Default, features: Vec::new(), features_source_loc: None, - solve_group: Some(solve_groups.add("default", 0)), + solve_group: None, }); environments.by_name.insert(EnvironmentName::Default, 0); } From 3f116d97c8ce0516e2b918da572c27d31ce53524 Mon Sep 17 00:00:00 2001 From: Olivier Lacroix Date: Fri, 5 Apr 2024 11:05:12 +1100 Subject: [PATCH 10/17] refactor parsing and display info message --- src/cli/init.rs | 25 ++++++++++++------------- src/project/manifest/mod.rs | 2 +- src/project/manifest/pyproject.rs | 17 +++++++++++++++++ 3 files changed, 30 insertions(+), 14 deletions(-) diff --git a/src/cli/init.rs b/src/cli/init.rs index 408171a86e..b8010366f0 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::environments_from_extras; +use crate::project::manifest::pyproject; use crate::utils::conda_environment_file::CondaEnvFile; use crate::{config::get_default_author, consts}; use crate::{FeatureName, Project}; @@ -8,7 +8,6 @@ use clap::Parser; use indexmap::IndexMap; use miette::IntoDiagnostic; use minijinja::{context, Environment}; -use pyproject_toml::PyProjectToml; use rattler_conda_types::Platform; use std::io::{Error, ErrorKind, Write}; use std::path::Path; @@ -160,16 +159,9 @@ pub async fn execute(args: Args) -> miette::Result<()> { if pyproject_manifest_path.is_file() { let file = fs::read_to_string(pyproject_manifest_path.clone()).unwrap(); if !file.contains("[tool.pixi.project]") { - let pyproject = PyProjectToml::new(&file).unwrap(); - let environments = environments_from_extras(&pyproject); - // Get name from the pyproject [project] table - let name = match PyProjectToml::new(file.as_str()) { - Ok(pyproject) => pyproject - .project - .map(|p| p.name) - .expect("'name' should be defined in the [project] table"), - Err(e) => miette::bail!("Failed to parse 'pyproject.toml'. Error is {}", e), - }; + let pyproject = pyproject::pyproject(&file)?; + let name = pyproject.project.as_ref().unwrap().name.clone(); + let environments = pyproject::environments_from_extras(&pyproject); let rv = env .render_named_str( consts::PYPROJECT_MANIFEST, @@ -200,9 +192,16 @@ pub async fn execute(args: Args) -> miette::Result<()> { console::style(console::Emoji("✔ ", "")).green(), name ); + // Inform about the addition of environments from extras (if any) + if !environments.is_empty() { + eprintln!( + "{}Added environments '{:?}' from optional extras.", + console::style(console::Emoji("✔ ", "")).green(), + environments.keys().collect::>() + ); + } } } - // Create a 'pixi.toml' manifest } else { // Check if the 'pixi.toml' file doesn't already exist. We don't want to overwrite it. diff --git a/src/project/manifest/mod.rs b/src/project/manifest/mod.rs index 47215834df..2883f20c5a 100644 --- a/src/project/manifest/mod.rs +++ b/src/project/manifest/mod.rs @@ -2,7 +2,7 @@ mod activation; pub(crate) mod channel; mod document; mod environment; -mod error; +pub mod error; mod feature; mod metadata; pub mod pyproject; diff --git a/src/project/manifest/pyproject.rs b/src/project/manifest/pyproject.rs index 3e8ae2e362..c79fa21f47 100644 --- a/src/project/manifest/pyproject.rs +++ b/src/project/manifest/pyproject.rs @@ -1,3 +1,4 @@ +use miette::Report; use pep508_rs::VersionOrUrl; use pyproject_toml::PyProjectToml; use rattler_conda_types::{NamelessMatchSpec, PackageName, ParseStrictness::Lenient, VersionSpec}; @@ -164,6 +165,22 @@ pub fn environments_from_extras(pyproject: &PyProjectToml) -> HashMap 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() { + return TomlError::NoProjectTable.to_fancy("pyproject.toml", source); + } else { + Ok(pyproject) + } + } + } +} + #[cfg(test)] mod tests { use std::path::Path; From b4c6e9d5da5c3b0d97a5e050ec02046c991a3587 Mon Sep 17 00:00:00 2001 From: Olivier Lacroix Date: Fri, 5 Apr 2024 12:18:55 +1100 Subject: [PATCH 11/17] revert test changes --- src/project/manifest/mod.rs | 2 +- src/project/solve_group.rs | 11 ++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/project/manifest/mod.rs b/src/project/manifest/mod.rs index 2883f20c5a..47215834df 100644 --- a/src/project/manifest/mod.rs +++ b/src/project/manifest/mod.rs @@ -2,7 +2,7 @@ mod activation; pub(crate) mod channel; mod document; mod environment; -pub mod error; +mod error; mod feature; mod metadata; pub mod pyproject; diff --git a/src/project/solve_group.rs b/src/project/solve_group.rs index aabac2539a..32671a3af6 100644 --- a/src/project/solve_group.rs +++ b/src/project/solve_group.rs @@ -223,10 +223,10 @@ mod tests { let foo_environment = project.environment("foo").unwrap(); let bar_environment = project.environment("bar").unwrap(); - let solve_groups = project.solve_groups(); // contains 'default', 'group1' - assert_eq!(solve_groups.len(), 2); + let solve_groups = project.solve_groups(); + assert_eq!(solve_groups.len(), 1); - let solve_group = solve_groups[1].clone(); // 'group1' solve group + let solve_group = solve_groups[0].clone(); let solve_group_envs = solve_group.environments().collect_vec(); assert_eq!(solve_group_envs.len(), 2); assert_eq!(solve_group_envs[0].name(), "foo"); @@ -235,10 +235,7 @@ mod tests { // Make sure that the environments properly reference the group assert_eq!(foo_environment.solve_group(), Some(solve_group.clone())); assert_eq!(bar_environment.solve_group(), Some(solve_group.clone())); - assert_eq!( - default_environment.solve_group(), - Some(solve_groups[0].clone()) - ); + assert_eq!(default_environment.solve_group(), None); // Make sure that all the environments share the same system requirements, because they are // in the same solve-group. From 1c2f5f124acf082875539a7f3c68ebed1ff4d976 Mon Sep 17 00:00:00 2001 From: Olivier Lacroix Date: Fri, 5 Apr 2024 12:29:17 +1100 Subject: [PATCH 12/17] lint --- src/project/manifest/pyproject.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/project/manifest/pyproject.rs b/src/project/manifest/pyproject.rs index c79fa21f47..a7b0437a65 100644 --- a/src/project/manifest/pyproject.rs +++ b/src/project/manifest/pyproject.rs @@ -173,7 +173,7 @@ pub fn pyproject(source: &str) -> Result { // Make sure [project] exists in pyproject.toml, // This will ensure project.name is defined if pyproject.project.is_none() { - return TomlError::NoProjectTable.to_fancy("pyproject.toml", source); + TomlError::NoProjectTable.to_fancy("pyproject.toml", source) } else { Ok(pyproject) } From 54d6772ec50e1279bc349f5902e676244403ff40 Mon Sep 17 00:00:00 2001 From: Olivier Lacroix Date: Fri, 5 Apr 2024 13:20:05 +1100 Subject: [PATCH 13/17] Refine message, and update flask example --- .../.gitattributes | 2 + .../flask-hello-world-pyproject/.gitignore | 3 + .../flask-hello-world-pyproject/pixi.lock | 119 ++++++++++++++++-- .../pyproject.toml | 11 +- src/cli/init.rs | 18 ++- 5 files changed, 140 insertions(+), 13 deletions(-) create mode 100644 examples/flask-hello-world-pyproject/.gitattributes diff --git a/examples/flask-hello-world-pyproject/.gitattributes b/examples/flask-hello-world-pyproject/.gitattributes new file mode 100644 index 0000000000..d5799bd69d --- /dev/null +++ b/examples/flask-hello-world-pyproject/.gitattributes @@ -0,0 +1,2 @@ +# GitHub syntax highlighting +pixi.lock linguist-language=YAML diff --git a/examples/flask-hello-world-pyproject/.gitignore b/examples/flask-hello-world-pyproject/.gitignore index 11041c7834..606bbc65f8 100644 --- a/examples/flask-hello-world-pyproject/.gitignore +++ b/examples/flask-hello-world-pyproject/.gitignore @@ -1 +1,4 @@ *.egg-info +# pixi environments +.pixi +*.egg-info diff --git a/examples/flask-hello-world-pyproject/pixi.lock b/examples/flask-hello-world-pyproject/pixi.lock index 709004e6fe..ca0892a8e7 100644 --- a/examples/flask-hello-world-pyproject/pixi.lock +++ b/examples/flask-hello-world-pyproject/pixi.lock @@ -1,6 +1,109 @@ version: 4 environments: default: + channels: + - url: https://conda.anaconda.org/conda-forge/ + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hd590300_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2024.2.2-hbcca054_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.40-h41732ed_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.6.2-h59595ed_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.2-h7f98852_5.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-13.2.0-h807b86a_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-13.2.0-h807b86a_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hd590300_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.45.2-h2797004_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.2.13-hd590300_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.4.20240210-h59595ed_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.2.1-hd590300_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.2-hab00c5b_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8228510_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xz-5.2.6-h166bdaf_0.tar.bz2 + - pypi: https://files.pythonhosted.org/packages/fa/2a/7f3714cbc6356a0efec525ce7a0613d581072ed6eb53eb7b9754f33db807/blinker-1.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fd/56/26f0be8adc2b4257df20c1c4260ddd0aa396cf8e75d90ab2f7ff99bc34f9/flask-2.3.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/68/5f/447e04e828f47465eeab35b5d408b7ebaaaee207f48b7136c5a7267a30ae/itsdangerous-2.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/30/6d/6de6be2d02603ab56e72997708809e8a5b0fbfee080735109b40a3564843/Jinja2-3.1.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e3/23/c9843d7550092ae7ad380611c238f44afef66f58f76c1dab7dcf313e4339/werkzeug-3.0.2-py3-none-any.whl + - pypi: . + osx-64: + - conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h10d778d_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ca-certificates-2024.2.2-h8857fd0_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.6.2-h73e2aa4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.4.2-h0d85af4_5.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.45.2-h92b6c6a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.2.13-h8a1eda9_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.4.20240210-h73e2aa4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.2.1-hd75f5a5_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.12.2-h9f0c242_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.2-h9e318b2_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h1abcd95_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/xz-5.2.6-h775f41a_0.tar.bz2 + - pypi: https://files.pythonhosted.org/packages/fa/2a/7f3714cbc6356a0efec525ce7a0613d581072ed6eb53eb7b9754f33db807/blinker-1.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fd/56/26f0be8adc2b4257df20c1c4260ddd0aa396cf8e75d90ab2f7ff99bc34f9/flask-2.3.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/68/5f/447e04e828f47465eeab35b5d408b7ebaaaee207f48b7136c5a7267a30ae/itsdangerous-2.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/30/6d/6de6be2d02603ab56e72997708809e8a5b0fbfee080735109b40a3564843/Jinja2-3.1.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e3/23/c9843d7550092ae7ad380611c238f44afef66f58f76c1dab7dcf313e4339/werkzeug-3.0.2-py3-none-any.whl + - pypi: . + osx-arm64: + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-h93a5062_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ca-certificates-2024.2.2-hf0a4a13_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.6.2-hebf3989_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.4.2-h3422bc3_5.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.45.2-h091b4b1_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.2.13-h53f4e23_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.4.20240210-h078ce10_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.2.1-h0d3ecfb_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.12.2-hdf0ec26_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h92ec313_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h5083fa2_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xz-5.2.6-h57fd34a_0.tar.bz2 + - pypi: https://files.pythonhosted.org/packages/fa/2a/7f3714cbc6356a0efec525ce7a0613d581072ed6eb53eb7b9754f33db807/blinker-1.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fd/56/26f0be8adc2b4257df20c1c4260ddd0aa396cf8e75d90ab2f7ff99bc34f9/flask-2.3.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/68/5f/447e04e828f47465eeab35b5d408b7ebaaaee207f48b7136c5a7267a30ae/itsdangerous-2.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/30/6d/6de6be2d02603ab56e72997708809e8a5b0fbfee080735109b40a3564843/Jinja2-3.1.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/e3/23/c9843d7550092ae7ad380611c238f44afef66f58f76c1dab7dcf313e4339/werkzeug-3.0.2-py3-none-any.whl + - pypi: . + win-64: + - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-hcfcfb64_5.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ca-certificates-2024.2.2-h56e8100_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.6.2-h63175ca_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.4.2-h8ffe710_5.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.45.2-hcfcfb64_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.2.13-hcfcfb64_5.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.2.1-hcfcfb64_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.12.2-h2628c8c_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h5226925_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.22621.0-h57928b3_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-hcf57466_18.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.38.33130-h82b7239_18.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.38.33130-hcb4865c_18.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/xz-5.2.6-h8d14728_0.tar.bz2 + - pypi: https://files.pythonhosted.org/packages/fa/2a/7f3714cbc6356a0efec525ce7a0613d581072ed6eb53eb7b9754f33db807/blinker-1.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fd/56/26f0be8adc2b4257df20c1c4260ddd0aa396cf8e75d90ab2f7ff99bc34f9/flask-2.3.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/68/5f/447e04e828f47465eeab35b5d408b7ebaaaee207f48b7136c5a7267a30ae/itsdangerous-2.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/30/6d/6de6be2d02603ab56e72997708809e8a5b0fbfee080735109b40a3564843/Jinja2-3.1.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/14/c3554d512d5f9100a95e737502f4a2323a1959f6d0d01e0d0997b35f7b10/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/e3/23/c9843d7550092ae7ad380611c238f44afef66f58f76c1dab7dcf313e4339/werkzeug-3.0.2-py3-none-any.whl + - pypi: . + test: channels: - url: https://conda.anaconda.org/conda-forge/ packages: @@ -291,10 +394,10 @@ packages: name: flask-hello-world-pyproject version: 0.1.0 path: . - sha256: 472d48341f3660c59666006a765c70932272cb4b0a1017c66b1d5ae8ab7778d0 + sha256: abf0f05892c27341f38a76b6fb0b65003e78b083fdda7721519f047503327870 requires_dist: - flask ==2.* - - pytest + - pytest ; extra == 'test' requires_python: '>=3.11' editable: true - kind: pypi @@ -649,12 +752,6 @@ packages: url: https://files.pythonhosted.org/packages/3f/14/c3554d512d5f9100a95e737502f4a2323a1959f6d0d01e0d0997b35f7b10/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl sha256: 823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb requires_python: '>=3.7' -- kind: pypi - name: markupsafe - version: 2.1.5 - url: https://files.pythonhosted.org/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl - sha256: 3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4 - requires_python: '>=3.7' - kind: pypi name: markupsafe version: 2.1.5 @@ -667,6 +764,12 @@ packages: url: https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl sha256: 8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1 requires_python: '>=3.7' +- kind: pypi + name: markupsafe + version: 2.1.5 + url: https://files.pythonhosted.org/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl + sha256: 3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4 + requires_python: '>=3.7' - kind: conda name: ncurses version: 6.4.20240210 diff --git a/examples/flask-hello-world-pyproject/pyproject.toml b/examples/flask-hello-world-pyproject/pyproject.toml index 7fecef7134..4bc8a9be4a 100644 --- a/examples/flask-hello-world-pyproject/pyproject.toml +++ b/examples/flask-hello-world-pyproject/pyproject.toml @@ -4,7 +4,10 @@ version = "0.1.0" description = "Example how to get started with flask in a pixi environment." readme = "README.md" requires-python = ">=3.11" -dependencies = ["flask==2.*", "pytest"] +dependencies = ["flask==2.*"] + +[project.optional-dependencies] +test = ["pytest"] [build-system] requires = ["setuptools", "wheel"] @@ -17,6 +20,12 @@ platforms = ["linux-64", "osx-arm64", "osx-64", "win-64"] [tool.pixi.pypi-dependencies] flask-hello-world-pyproject = { path = ".", editable = true } +[tool.pixi.environments] +default = { features = [], solve-group = "default" } +test = { features = ["test"], solve-group = "default" } + [tool.pixi.tasks] start = "python -m flask --app flask_hello_world_pyproject.app:app run --port=5050" + +[tool.pixi.feature.test.tasks] test = "pytest -v tests/*" diff --git a/src/cli/init.rs b/src/cli/init.rs index b8010366f0..3ae24707c8 100644 --- a/src/cli/init.rs +++ b/src/cli/init.rs @@ -193,12 +193,22 @@ pub async fn execute(args: Args) -> miette::Result<()> { name ); // Inform about the addition of environments from extras (if any) - if !environments.is_empty() { + let envs = environments + .keys() + .map(AsRef::as_ref) + .collect::>(); + if envs.len() > 1 { eprintln!( - "{}Added environments '{:?}' from optional extras.", + "{}Added environments {} from optional extras.", console::style(console::Emoji("✔ ", "")).green(), - environments.keys().collect::>() - ); + format!("'{}'", envs.join("', '")) + ) + } else if envs.len() == 1 { + eprintln!( + "{}Added environment {} from optional extras.", + console::style(console::Emoji("✔ ", "")).green(), + format!("'{}'", envs[0]) + ) } } } From d09885abb57a024a3342f1f4203fe18ef00b105a Mon Sep 17 00:00:00 2001 From: Olivier Lacroix Date: Fri, 5 Apr 2024 13:26:58 +1100 Subject: [PATCH 14/17] Simplify print --- src/cli/init.rs | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/cli/init.rs b/src/cli/init.rs index 3ae24707c8..bbc165c644 100644 --- a/src/cli/init.rs +++ b/src/cli/init.rs @@ -193,22 +193,14 @@ pub async fn execute(args: Args) -> miette::Result<()> { name ); // Inform about the addition of environments from extras (if any) - let envs = environments - .keys() - .map(AsRef::as_ref) - .collect::>(); - if envs.len() > 1 { + if !environments.is_empty() { + let envs: Vec<&str> = environments.keys().map(AsRef::as_ref).collect(); eprintln!( - "{}Added environments {} from optional extras.", + "{}Added environment{} {} from optional extras.", console::style(console::Emoji("✔ ", "")).green(), + if envs.len() > 1 { "s" } else { "" }, format!("'{}'", envs.join("', '")) ) - } else if envs.len() == 1 { - eprintln!( - "{}Added environment {} from optional extras.", - console::style(console::Emoji("✔ ", "")).green(), - format!("'{}'", envs[0]) - ) } } } From 05650ee3ec0e4eb1dd7b135b94a9e705362deaee Mon Sep 17 00:00:00 2001 From: Olivier Lacroix Date: Fri, 5 Apr 2024 13:48:47 +1100 Subject: [PATCH 15/17] Add a message when not runnning init --- src/cli/init.rs | 91 ++++++++++++++++++++++++++----------------------- 1 file changed, 49 insertions(+), 42 deletions(-) diff --git a/src/cli/init.rs b/src/cli/init.rs index bbc165c644..a00c5eaa90 100644 --- a/src/cli/init.rs +++ b/src/cli/init.rs @@ -158,52 +158,59 @@ 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(); - if !file.contains("[tool.pixi.project]") { - let pyproject = pyproject::pyproject(&file)?; - let name = pyproject.project.as_ref().unwrap().name.clone(); - let environments = pyproject::environments_from_extras(&pyproject); - let rv = env - .render_named_str( - consts::PYPROJECT_MANIFEST, - PYROJECT_TEMPLATE, - context! { - name, - channels, - platforms, - environments, - }, - ) - .unwrap(); - if let Err(e) = { - fs::OpenOptions::new() - .append(true) - .open(pyproject_manifest_path.clone()) - .and_then(|mut p| p.write_all(rv.as_bytes())) - } { - tracing::warn!( - "Warning, couldn't update '{}' because of: {}", - pyproject_manifest_path.to_string_lossy(), - e - ); - } else { - // Inform about the addition of the package itself as an editable dependency of the project + if file.contains("[tool.pixi.project]") { + eprintln!( + "{}Nothing to do here: 'pyproject.toml' already contains a '[tool.pixi.project]' section.", + console::style(console::Emoji("🤔 ", "")).blue(), + ); + return Ok(()); + } + + let pyproject = pyproject::pyproject(&file)?; + let name = pyproject.project.as_ref().unwrap().name.clone(); + let environments = pyproject::environments_from_extras(&pyproject); + let rv = env + .render_named_str( + consts::PYPROJECT_MANIFEST, + PYROJECT_TEMPLATE, + context! { + name, + channels, + platforms, + environments, + }, + ) + .unwrap(); + if let Err(e) = { + fs::OpenOptions::new() + .append(true) + .open(pyproject_manifest_path.clone()) + .and_then(|mut p| p.write_all(rv.as_bytes())) + } { + tracing::warn!( + "Warning, couldn't update '{}' because of: {}", + pyproject_manifest_path.to_string_lossy(), + e + ); + } else { + // Inform about the addition of the package itself as an editable dependency of the project + eprintln!( + "{}Added package '{}' as an editable dependency.", + console::style(console::Emoji("✔ ", "")).green(), + name + ); + // Inform about the addition of environments from extras (if any) + if !environments.is_empty() { + let envs: Vec<&str> = environments.keys().map(AsRef::as_ref).collect(); eprintln!( - "{}Added package '{}' as an editable dependency.", + "{}Added environment{} '{}' from optional extras.", console::style(console::Emoji("✔ ", "")).green(), - name - ); - // Inform about the addition of environments from extras (if any) - if !environments.is_empty() { - let envs: Vec<&str> = environments.keys().map(AsRef::as_ref).collect(); - eprintln!( - "{}Added environment{} {} from optional extras.", - console::style(console::Emoji("✔ ", "")).green(), - if envs.len() > 1 { "s" } else { "" }, - format!("'{}'", envs.join("', '")) - ) - } + if envs.len() > 1 { "s" } else { "" }, + envs.join("', '") + ) } } + // Create a 'pixi.toml' manifest } else { // Check if the 'pixi.toml' file doesn't already exist. We don't want to overwrite it. From d6e983343e6012ba2bd9073e66da2572f870ba46 Mon Sep 17 00:00:00 2001 From: Olivier Lacroix Date: Fri, 5 Apr 2024 14:02:40 +1100 Subject: [PATCH 16/17] cleanup --- docs/advanced/pyproject_toml.md | 1 - docs/configuration.md | 7 ------- examples/flask-hello-world-pyproject/.gitignore | 1 - src/cli/init.rs | 2 ++ src/project/manifest/mod.rs | 2 -- src/project/manifest/pyproject.rs | 11 ++++------- 6 files changed, 6 insertions(+), 18 deletions(-) diff --git a/docs/advanced/pyproject_toml.md b/docs/advanced/pyproject_toml.md index f32a602467..8197f7579c 100644 --- a/docs/advanced/pyproject_toml.md +++ b/docs/advanced/pyproject_toml.md @@ -130,7 +130,6 @@ test = ["pytest"] all = ["package2","my_project[test]"] [tool.pixi.project] -name = "my_project" channels = ["conda-forge"] platforms = ["linux-64"] # if executed on linux diff --git a/docs/configuration.md b/docs/configuration.md index e764e4d4e1..51f2d54541 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -564,13 +564,6 @@ prod = {features = ["prod"], solve-group = "test"} lint = ["lint"] ``` -Unless the `default` environment is redefined, it is included in the `default` solve group. That is, the following is implied unless overridden in the manifest's `[environments]` table: -```toml title="default environment" -[environments] -default = { features = [], solve-group = "default" } # the default environment includes the 'default' feature only -``` - - ## Global configuration The global configuration options are documented in the [global configuration](advanced/global_configuration.md) section. diff --git a/examples/flask-hello-world-pyproject/.gitignore b/examples/flask-hello-world-pyproject/.gitignore index 606bbc65f8..096b5eb543 100644 --- a/examples/flask-hello-world-pyproject/.gitignore +++ b/examples/flask-hello-world-pyproject/.gitignore @@ -1,4 +1,3 @@ -*.egg-info # pixi environments .pixi *.egg-info diff --git a/src/cli/init.rs b/src/cli/init.rs index a00c5eaa90..e6f075d060 100644 --- a/src/cli/init.rs +++ b/src/cli/init.rs @@ -158,6 +158,8 @@ 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(); + + // Early exit if 'pyproject.toml' already contains a '[tool.pixi.project]' section if file.contains("[tool.pixi.project]") { eprintln!( "{}Nothing to do here: 'pyproject.toml' already contains a '[tool.pixi.project]' section.", diff --git a/src/project/manifest/mod.rs b/src/project/manifest/mod.rs index 47215834df..84ddaa2adf 100644 --- a/src/project/manifest/mod.rs +++ b/src/project/manifest/mod.rs @@ -1062,8 +1062,6 @@ impl<'de> Deserialize<'de> for ProjectManifest { } // Add all named environments - // Note: if the default Environment is redefined, but does not specify a solve-group, - // it is not added to the 'default' solve-group. for (name, env) in toml_manifest.environments { // Decompose the TOML let (features, features_source_loc, solve_group) = match env { diff --git a/src/project/manifest/pyproject.rs b/src/project/manifest/pyproject.rs index a7b0437a65..e524f39191 100644 --- a/src/project/manifest/pyproject.rs +++ b/src/project/manifest/pyproject.rs @@ -63,7 +63,7 @@ impl From for ProjectManifest { // Get tool.pixi.project.name from project.name // TODO: could copy across / convert some other optional fields if relevant - manifest.project.name = item.project.as_ref().map(|p| p.name.clone()); + manifest.project.name = Some(pyproject.name.clone()); // Add python as dependency based on the project.requires_python property (if any) let pythonspec = pyproject @@ -89,7 +89,8 @@ impl From for ProjectManifest { // For each extra group, create a feature of the same name if it does not exist, // and add pypi dependencies from project.optional-dependencies - if let Some(Some(extras)) = &item.project.as_ref().map(|p| &p.optional_dependencies) { + if let Some(extras) = pyproject.optional_dependencies.as_ref() { + let project_name = pep508_rs::PackageName::new(pyproject.name.clone()).unwrap(); for (extra, reqs) in extras { let target = manifest .features @@ -97,13 +98,9 @@ impl From for ProjectManifest { .or_default() .targets .default_mut(); - let pname = &item - .project - .as_ref() - .map(|p| pep508_rs::PackageName::new(p.name.clone()).unwrap()); for req in reqs.iter() { // filter out any self references in groups of extra dependencies - if pname.as_ref().is_some_and(|n| n == &req.name) { + if project_name == req.name { continue; } target.add_pypi_dependency( From 0ee04116957ecadb993ab90a0b82776aca72b85d Mon Sep 17 00:00:00 2001 From: Olivier Lacroix Date: Fri, 5 Apr 2024 15:47:51 +1100 Subject: [PATCH 17/17] Filter out unused features --- src/project/manifest/mod.rs | 2 +- src/project/manifest/pyproject.rs | 44 ++++++++++++++++++++----------- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/src/project/manifest/mod.rs b/src/project/manifest/mod.rs index 84ddaa2adf..9e7df07d89 100644 --- a/src/project/manifest/mod.rs +++ b/src/project/manifest/mod.rs @@ -756,7 +756,7 @@ impl SolveGroups { /// If the solve-group does not exist, it is created /// /// Returns the index of the solve-group - pub fn add(&mut self, name: &str, environment_idx: usize) -> usize { + fn add(&mut self, name: &str, environment_idx: usize) -> usize { match self.by_name.get(name) { Some(idx) => { // The solve-group exists, add the environment index to it diff --git a/src/project/manifest/pyproject.rs b/src/project/manifest/pyproject.rs index e524f39191..025bc0205a 100644 --- a/src/project/manifest/pyproject.rs +++ b/src/project/manifest/pyproject.rs @@ -3,7 +3,10 @@ use pep508_rs::VersionOrUrl; use pyproject_toml::PyProjectToml; use rattler_conda_types::{NamelessMatchSpec, PackageName, ParseStrictness::Lenient, VersionSpec}; use serde::Deserialize; -use std::{collections::HashMap, str::FromStr}; +use std::{ + collections::{HashMap, HashSet}, + str::FromStr, +}; use toml_edit; use crate::FeatureName; @@ -88,25 +91,34 @@ impl From for ProjectManifest { } // For each extra group, create a feature of the same name if it does not exist, - // and add pypi dependencies from project.optional-dependencies + // and add pypi dependencies from project.optional-dependencies, + // filtering out unused features and self-references if let Some(extras) = pyproject.optional_dependencies.as_ref() { let project_name = pep508_rs::PackageName::new(pyproject.name.clone()).unwrap(); + let mut features_used = HashSet::new(); + for env in manifest.environments.iter() { + for feature in env.features.iter() { + features_used.insert(feature); + } + } for (extra, reqs) in extras { - let target = manifest - .features - .entry(FeatureName::Named(extra.to_string())) - .or_default() - .targets - .default_mut(); - for req in reqs.iter() { - // filter out any self references in groups of extra dependencies - if project_name == req.name { - continue; + // Filter out unused features + if features_used.contains(extra) { + let target = manifest + .features + .entry(FeatureName::Named(extra.to_string())) + .or_default() + .targets + .default_mut(); + for req in reqs.iter() { + // filter out any self references in groups of extra dependencies + if project_name != req.name { + target.add_pypi_dependency( + PyPiPackageName::from_normalized(req.name.clone()), + PyPiRequirement::from(req.clone()), + ) + } } - target.add_pypi_dependency( - PyPiPackageName::from_normalized(req.name.clone()), - PyPiRequirement::from(req.clone()), - ) } } }