diff --git a/crates/pixi_manifest/src/lib.rs b/crates/pixi_manifest/src/lib.rs index 326de2259..fbdabfe57 100644 --- a/crates/pixi_manifest/src/lib.rs +++ b/crates/pixi_manifest/src/lib.rs @@ -72,6 +72,15 @@ pub enum DependencyOverwriteBehavior { Error, } +pub enum PypiDependencyLocation { + // The [pypi-dependencies] or [tool.pixi.pypi-dependencies] table + Pixi, + // The [project.optional-dependencies] table in a 'pyproject.toml' manifest + OptionalDependencies, + // The [dependency-groups] table in a 'pyproject.toml' manifest + DependencyGroups, +} + /// Converts an array of Platforms to a non-empty Vec of Option fn to_options(platforms: &[Platform]) -> Vec> { match platforms.is_empty() { diff --git a/crates/pixi_manifest/src/manifests/manifest.rs b/crates/pixi_manifest/src/manifests/manifest.rs index ab28348df..091a294bc 100644 --- a/crates/pixi_manifest/src/manifests/manifest.rs +++ b/crates/pixi_manifest/src/manifests/manifest.rs @@ -22,8 +22,8 @@ use crate::{ pypi::PyPiPackageName, pyproject::PyProjectManifest, to_options, DependencyOverwriteBehavior, Environment, EnvironmentName, Feature, FeatureName, - GetFeatureError, ParsedManifest, PrioritizedChannel, SpecType, Target, TargetSelector, Task, - TaskName, + GetFeatureError, ParsedManifest, PrioritizedChannel, PypiDependencyLocation, SpecType, Target, + TargetSelector, Task, TaskName, }; #[derive(Debug, Clone)] @@ -384,6 +384,7 @@ impl Manifest { feature_name: &FeatureName, editable: Option, overwrite_behavior: DependencyOverwriteBehavior, + location: &Option, ) -> miette::Result { let mut any_added = false; for platform in crate::to_options(platforms) { @@ -398,6 +399,7 @@ impl Manifest { platform, feature_name, editable, + location, )?; any_added = true; } diff --git a/crates/pixi_manifest/src/manifests/project.rs b/crates/pixi_manifest/src/manifests/project.rs index c41059e81..f3e111305 100644 --- a/crates/pixi_manifest/src/manifests/project.rs +++ b/crates/pixi_manifest/src/manifests/project.rs @@ -5,6 +5,7 @@ use rattler_conda_types::{PackageName, Platform}; use toml_edit::{value, Array, Item, Table, Value}; use super::TomlManifest; +use crate::PypiDependencyLocation; use crate::{consts, error::TomlError, pypi::PyPiPackageName, PyPiRequirement}; use crate::{consts::PYPROJECT_PIXI_PREFIX, FeatureName, SpecType, Task}; @@ -188,30 +189,43 @@ impl ManifestSource { ) -> Result<(), TomlError> { // For 'pyproject.toml' manifest, try and remove the dependency from native // arrays - let array = match self { + let remove_requirement = + |source: &mut ManifestSource, table, array_name| -> Result<(), TomlError> { + let array = source.manifest().get_toml_array(table, array_name)?; + if let Some(array) = array { + array.retain(|x| { + let req: pep508_rs::Requirement = x + .as_str() + .unwrap_or("") + .parse() + .expect("should be a valid pep508 dependency"); + let name = PyPiPackageName::from_normalized(req.name); + name != *dep + }); + if array.is_empty() { + source + .manifest() + .get_or_insert_nested_table(table)? + .remove(array_name); + } + } + Ok(()) + }; + + match self { ManifestSource::PyProjectToml(_) if feature_name.is_default() => { - self.manifest().get_toml_array("project", "dependencies")? + remove_requirement(self, "project", "dependencies")?; + } + ManifestSource::PyProjectToml(_) => { + let name = feature_name.to_string(); + remove_requirement(self, "project.optional-dependencies", &name)?; + remove_requirement(self, "dependency-groups", &name)?; } - ManifestSource::PyProjectToml(_) => self - .manifest() - .get_toml_array("project.optional-dependencies", &feature_name.to_string())?, - _ => None, + _ => (), }; - if let Some(array) = array { - array.retain(|x| { - let req: pep508_rs::Requirement = x - .as_str() - .unwrap_or("") - .parse() - .expect("should be a valid pep508 dependency"); - let name = PyPiPackageName::from_normalized(req.name); - name != *dep - }); - } // For both 'pyproject.toml' and 'pixi.toml' manifest, // try and remove the dependency from pixi native tables - let table_name = TableName::new() .with_prefix(self.table_prefix()) .with_feature_name(Some(feature_name)) @@ -285,47 +299,72 @@ impl ManifestSource { platform: Option, feature_name: &FeatureName, editable: Option, + location: &Option, ) -> Result<(), TomlError> { - match self { - ManifestSource::PyProjectToml(_) => { - // Pypi dependencies can be stored in different places - // so we remove any potential dependency of the same name before adding it back - self.remove_pypi_dependency( - &PyPiPackageName::from_normalized(requirement.name.clone()), - platform, - feature_name, - )?; - if let FeatureName::Named(name) = feature_name { - self.manifest() - .get_or_insert_toml_array("project.optional-dependencies", name)? - .push(requirement.to_string()) - } else { - self.manifest() - .get_or_insert_toml_array("project", "dependencies")? - .push(requirement.to_string()) - } - } - ManifestSource::PixiToml(_) => { - let mut pypi_requirement = - PyPiRequirement::try_from(requirement.clone()).map_err(Box::new)?; - if let Some(editable) = editable { - pypi_requirement.set_editable(editable); - } + // Pypi dependencies can be stored in different places in pyproject.toml manifests + // so we remove any potential dependency of the same name before adding it back + if matches!(self, ManifestSource::PyProjectToml(_)) { + self.remove_pypi_dependency( + &PyPiPackageName::from_normalized(requirement.name.clone()), + platform, + feature_name, + )?; + } - let dependency_table = TableName::new() - .with_prefix(self.table_prefix()) - .with_platform(platform.as_ref()) - .with_feature_name(Some(feature_name)) - .with_table(Some(consts::PYPI_DEPENDENCIES)); - - self.manifest() - .get_or_insert_nested_table(dependency_table.to_string().as_str())? - .insert( - requirement.name.as_ref(), - Item::Value(pypi_requirement.into()), - ); + // The '[pypi-dependencies]' or '[tool.pixi.pypi-dependencies]' table is selected + // - For 'pixi.toml' manifests where it is the only choice + // - When explicitly requested + // - When a specific platform is requested, as markers are not supported (https://github.com/prefix-dev/pixi/issues/2149) + // - When an editable install is requested + if matches!(self, ManifestSource::PixiToml(_)) + || matches!(location, Some(PypiDependencyLocation::Pixi)) + || platform.is_some() + || editable.is_some_and(|e| e) + { + let mut pypi_requirement = + PyPiRequirement::try_from(requirement.clone()).map_err(Box::new)?; + if let Some(editable) = editable { + pypi_requirement.set_editable(editable); } - }; + + let dependency_table = TableName::new() + .with_prefix(self.table_prefix()) + .with_platform(platform.as_ref()) + .with_feature_name(Some(feature_name)) + .with_table(Some(consts::PYPI_DEPENDENCIES)); + + self.manifest() + .get_or_insert_nested_table(dependency_table.to_string().as_str())? + .insert( + requirement.name.as_ref(), + Item::Value(pypi_requirement.into()), + ); + return Ok(()); + } + + // Otherwise: + // - the [project.dependencies] array is selected for the default feature + // - the [dependency-groups.feature_name] array is selected unless + // - optional-dependencies is explicitly requested as location + let add_requirement = + |source: &mut ManifestSource, table, array| -> Result<(), TomlError> { + source + .manifest() + .get_or_insert_toml_array(table, array)? + .push(requirement.to_string()); + Ok(()) + }; + if feature_name.is_default() { + add_requirement(self, "project", "dependencies")? + } else if matches!(location, Some(PypiDependencyLocation::OptionalDependencies)) { + add_requirement( + self, + "project.optional-dependencies", + &feature_name.to_string(), + )? + } else { + add_requirement(self, "dependency-groups", &feature_name.to_string())? + } Ok(()) } diff --git a/crates/pixi_manifest/src/pyproject.rs b/crates/pixi_manifest/src/pyproject.rs index 4e0596734..c6c116920 100644 --- a/crates/pixi_manifest/src/pyproject.rs +++ b/crates/pixi_manifest/src/pyproject.rs @@ -5,7 +5,7 @@ use miette::{Diagnostic, IntoDiagnostic, Report, WrapErr}; use pep440_rs::VersionSpecifiers; use pep508_rs::Requirement; use pixi_spec::PixiSpec; -use pyproject_toml::{self, Project}; +use pyproject_toml::{self, pep735_resolve::Pep735Error, Contact, Project}; use rattler_conda_types::{PackageName, ParseStrictness::Lenient, VersionSpec}; use serde::Deserialize; use thiserror::Error; @@ -165,11 +165,10 @@ impl PyProjectManifest { return Some( pyproject_authors .iter() - .filter_map(|contact| match (contact.name(), contact.email()) { - (Some(name), Some(email)) => Some(format!("{} <{}>", name, email)), - (Some(name), None) => Some(name.to_string()), - (None, Some(email)) => Some(email.to_string()), - (None, None) => None, + .map(|contact| match contact { + Contact::NameEmail { name, email } => format!("{} <{}>", name, email), + Contact::Name { name } => name.clone(), + Contact::Email { email } => email.clone(), }) .collect(), ); @@ -192,6 +191,11 @@ impl PyProjectManifest { self.project().and_then(|p| p.optional_dependencies.clone()) } + /// Returns dependency groups from the `[dependency-groups]` table + fn dependency_groups(&self) -> Option>, Pep735Error>> { + self.dependency_groups.as_ref().map(|dg| dg.resolve()) + } + /// 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 @@ -226,6 +230,8 @@ impl PyProjectManifest { pub enum PyProjectToManifestError { #[error("Unsupported pep508 requirement: '{0}'")] DependencyError(Requirement, #[source] DependencyError), + #[error(transparent)] + DependencyGroupError(#[from] Pep735Error), } impl TryFrom for ParsedManifest { @@ -289,32 +295,37 @@ impl TryFrom for ParsedManifest { } } - // For each extra group, create a feature of the same name if it does not exist, - // and add pypi dependencies from project.optional-dependencies, - // filtering out self-references - if let Some(extras) = item.optional_dependencies() { - let project_name = item.package_name(); - for (extra, reqs) in extras { - let feature_name = FeatureName::Named(extra.to_string()); - let target = 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| { - PyProjectToManifestError::DependencyError(requirement.clone(), err) - })?; - } + // Define an iterator over both optional dependencies and dependency groups + let groups = item + .optional_dependencies() + .into_iter() + .chain(item.dependency_groups().transpose()?) + .flat_map(|map| map.into_iter()); + + // For each group of optional dependency or dependency group, + // create a feature of the same name if it does not exist, + // and add pypi dependencies, filtering out self-references in optional dependencies + let project_name = item.package_name(); + for (group, reqs) in groups { + let feature_name = FeatureName::Named(group.to_string()); + let target = 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| { + PyProjectToManifestError::DependencyError(requirement.clone(), err) + })?; } } } @@ -534,6 +545,7 @@ mod tests { &FeatureName::Default, None, DependencyOverwriteBehavior::Overwrite, + &None, ) .unwrap(); @@ -557,6 +569,7 @@ mod tests { &FeatureName::Named("test".to_string()), None, DependencyOverwriteBehavior::Overwrite, + &None, ) .unwrap(); assert!(manifest diff --git a/crates/pixi_manifest/src/snapshots/pixi_manifest__pyproject__tests__add_pypi_dependency.snap b/crates/pixi_manifest/src/snapshots/pixi_manifest__pyproject__tests__add_pypi_dependency.snap index 889d21ee5..ec1fbaac8 100644 --- a/crates/pixi_manifest/src/snapshots/pixi_manifest__pyproject__tests__add_pypi_dependency.snap +++ b/crates/pixi_manifest/src/snapshots/pixi_manifest__pyproject__tests__add_pypi_dependency.snap @@ -1,6 +1,5 @@ --- source: crates/pixi_manifest/src/pyproject.rs -assertion_line: 464 expression: manifest.document.to_string() --- [project] @@ -13,12 +12,12 @@ expression: manifest.document.to_string() requires-python = ">=3.11" dependencies = ["flask==2.*", "numpy>=3.12"] -[project.optional-dependencies] -test = ["pytest>=3.12"] - [tool.pixi.project] channels = ["conda-forge"] platforms = ["linux-64"] [tool.pixi.tasks] start = "python -m flask run --port=5050" + +[dependency-groups] +test = ["pytest>=3.12"] diff --git a/crates/pixi_manifest/src/snapshots/pixi_manifest__pyproject__tests__remove_pypi_dependency.snap b/crates/pixi_manifest/src/snapshots/pixi_manifest__pyproject__tests__remove_pypi_dependency.snap index b2ea7e17a..10c33d19e 100644 --- a/crates/pixi_manifest/src/snapshots/pixi_manifest__pyproject__tests__remove_pypi_dependency.snap +++ b/crates/pixi_manifest/src/snapshots/pixi_manifest__pyproject__tests__remove_pypi_dependency.snap @@ -1,6 +1,5 @@ --- source: crates/pixi_manifest/src/pyproject.rs -assertion_line: 489 expression: manifest.document.to_string() --- [project] @@ -11,7 +10,6 @@ expression: manifest.document.to_string() homepage = "https://github.com/prefix/pixi" readme = "README.md" requires-python = ">=3.11" - dependencies = [] [tool.pixi.project] channels = ["conda-forge"] diff --git a/docs/advanced/pyproject_toml.md b/docs/advanced/pyproject_toml.md index 73fa512d7..18420c2c7 100644 --- a/docs/advanced/pyproject_toml.md +++ b/docs/advanced/pyproject_toml.md @@ -155,6 +155,57 @@ In this example, three environments will be created by pixi: 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). +## Dependency groups + +If your python project includes dependency groups, pixi will automatically interpret them as [pixi features](../reference/project_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 dependency group. + +For instance, imagine you have a project folder with a `pyproject.toml` file similar to: + +```toml +[project] +name = "my_project" +dependencies = ["package1"] + +[dependency-groups] +test = ["pytest"] +docs = ["sphinx"] +dev = [{include-group = "test"}, {include-group = "docs"}] +``` + +Running `pixi init` in that project folder will transform the `pyproject.toml` file into: + +```toml +[project] +name = "my_project" +dependencies = ["package1"] + +[dependency-groups] +test = ["pytest"] +docs = ["sphinx"] +dev = [{include-group = "test"}, {include-group = "docs"}] + +[tool.pixi.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"} +docs = {features = ["docs"], solve-group = "default"} +dev = {features = ["dev"], solve-group = "default"} +``` + +In this example, four environments will be created by pixi: + +- **default** with 'package1' as pypi dependency +- **test** with 'package1' and 'pytest' as pypi dependencies +- **docs** with 'package1', 'sphinx' as pypi dependencies +- **dev** with 'package1', 'sphinx' 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 environment). + ## Example As the `pyproject.toml` file supports the full pixi spec with `[tool.pixi]` prepended an example would look like this: diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 3bd9242a9..82ad768e6 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -53,13 +53,23 @@ pixi init --format pixi --scm gitlab ## `add` Adds dependencies to the [manifest file](project_configuration.md). -It will only add if the package with its version constraint is able to work with rest of the dependencies in the project. +It will only add dependencies compatible with the rest of the dependencies in the project. [More info](../features/multi_platform_configuration.md) on multi-platform configuration. -If the project manifest is a `pyproject.toml`, adding a pypi dependency will add it to the native pyproject `project.dependencies` array, or to the native `project.optional-dependencies` table if a feature is specified: +If the project manifest is a `pyproject.toml`, by default, adding a pypi dependency will add it to the native `project.dependencies` array, or to the native `dependency-groups` table if a feature is specified: - `pixi add --pypi boto3` would add `boto3` to the `project.dependencies` array -- `pixi add --pypi boto3 --feature aws` would add `boto3` to the `project.dependencies.aws` array +- `pixi add --pypi boto3 --feature aws` would add `boto3` to the `dependency-groups.aws` array + +Note that if `--platform` or `--editable` are specified, the pypi dependency +will be added to the `tool.pixi.pypi-dependencies` table instead as native +arrays have no support for platform-specific or editable dependencies. + +Specifying `--location` to `pixi` will force adding the pypi dependency +to the `tool.pixi.pypi-dependencies` table. Specifying `--location` to +`optional-dependencies` will force adding the pypi dependency +to the `project.optional-dependencies` table (this will only have an effect +if a non-default feature is also specified). These dependencies will be read by pixi as if they had been added to the pixi `pypi-dependencies` tables of the default or a named feature. @@ -79,7 +89,8 @@ These dependencies will be read by pixi as if they had been added to the pixi `p - `--no-lockfile-update`: Don't update the lock-file, implies the `--no-install` flag. - `--platform (-p)`: The platform for which the dependency should be added. (Allowed to be used more than once) - `--feature (-f)`: The feature for which the dependency should be added. -- `--editable`: Specifies an editable dependency, only use in combination with `--pypi`. +- `--editable`: Specifies an editable dependency; only used in combination with `--pypi`. +- `--location`: Specifies where to add the pypi dependency to the manifest; only used in combination with `--pypi`. ```shell pixi add numpy # (1)! diff --git a/examples/flask-hello-world-pyproject/pixi.lock b/examples/flask-hello-world-pyproject/pixi.lock index e9cb1d446..e96fa001f 100644 --- a/examples/flask-hello-world-pyproject/pixi.lock +++ b/examples/flask-hello-world-pyproject/pixi.lock @@ -32,9 +32,13 @@ environments: - pypi: https://files.pythonhosted.org/packages/bb/2a/10164ed1f31196a2f7f3799368a821765c62851ead0e630ab52b8e14b4d0/blinker-1.8.2-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/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz + - 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/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4b/84/997bbf7c2bf2dc3f09565c6d0b4959fefe5355c18c4096cfd26d83e0785b/werkzeug-3.0.4-py3-none-any.whl - pypi: . osx-64: @@ -54,9 +58,13 @@ environments: - pypi: https://files.pythonhosted.org/packages/bb/2a/10164ed1f31196a2f7f3799368a821765c62851ead0e630ab52b8e14b4d0/blinker-1.8.2-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/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-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/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4b/84/997bbf7c2bf2dc3f09565c6d0b4959fefe5355c18c4096cfd26d83e0785b/werkzeug-3.0.4-py3-none-any.whl - pypi: . osx-arm64: @@ -76,9 +84,13 @@ environments: - pypi: https://files.pythonhosted.org/packages/bb/2a/10164ed1f31196a2f7f3799368a821765c62851ead0e630ab52b8e14b4d0/blinker-1.8.2-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/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-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/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4b/84/997bbf7c2bf2dc3f09565c6d0b4959fefe5355c18c4096cfd26d83e0785b/werkzeug-3.0.4-py3-none-any.whl - pypi: . win-64: @@ -101,9 +113,13 @@ environments: - 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/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-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/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4b/84/997bbf7c2bf2dc3f09565c6d0b4959fefe5355c18c4096cfd26d83e0785b/werkzeug-3.0.4-py3-none-any.whl - pypi: . test: @@ -141,10 +157,10 @@ environments: - pypi: https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz - - pypi: https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-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/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0f/f9/cf155cf32ca7d6fa3601bc4c5dd19086af4b320b706919d48a4c79081cf9/pytest-8.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4b/84/997bbf7c2bf2dc3f09565c6d0b4959fefe5355c18c4096cfd26d83e0785b/werkzeug-3.0.4-py3-none-any.whl - pypi: . osx-64: @@ -168,9 +184,9 @@ environments: - pypi: https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-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/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0f/f9/cf155cf32ca7d6fa3601bc4c5dd19086af4b320b706919d48a4c79081cf9/pytest-8.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4b/84/997bbf7c2bf2dc3f09565c6d0b4959fefe5355c18c4096cfd26d83e0785b/werkzeug-3.0.4-py3-none-any.whl - pypi: . osx-arm64: @@ -194,9 +210,9 @@ environments: - pypi: https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-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/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0f/f9/cf155cf32ca7d6fa3601bc4c5dd19086af4b320b706919d48a4c79081cf9/pytest-8.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4b/84/997bbf7c2bf2dc3f09565c6d0b4959fefe5355c18c4096cfd26d83e0785b/werkzeug-3.0.4-py3-none-any.whl - pypi: . win-64: @@ -223,9 +239,9 @@ environments: - pypi: https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-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/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0f/f9/cf155cf32ca7d6fa3601bc4c5dd19086af4b320b706919d48a4c79081cf9/pytest-8.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4b/84/997bbf7c2bf2dc3f09565c6d0b4959fefe5355c18c4096cfd26d83e0785b/werkzeug-3.0.4-py3-none-any.whl - pypi: . packages: @@ -395,7 +411,7 @@ packages: version: 0.4.6 url: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl sha256: 4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 - requires_python: '!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7' + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*' - kind: pypi name: flask version: 2.3.3 @@ -415,10 +431,9 @@ packages: name: flask-hello-world-pyproject version: 0.1.0 path: . - sha256: 64e1990dc0c7d619bb9188df371ec44ef2ed8f7603e4842ac4eed6a9c9a5e89f + sha256: 6033690dfa1adac9b808662d775d1d69ba25b676af8e5cb14387fe47ad97a83f requires_dist: - flask==2.* - - pytest ; extra == 'test' requires_python: '>=3.11' editable: true - kind: pypi @@ -813,6 +828,12 @@ packages: purls: [] size: 46921 timestamp: 1716874262512 +- kind: pypi + name: markupsafe + version: 2.1.5 + url: https://files.pythonhosted.org/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + sha256: f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5 + requires_python: '>=3.7' - kind: pypi name: markupsafe version: 2.1.5 @@ -831,12 +852,6 @@ 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/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz - sha256: d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b - requires_python: '>=3.7' - kind: conda name: ncurses version: '6.5' @@ -956,9 +971,9 @@ packages: timestamp: 1724403015051 - kind: pypi name: packaging - version: '24.1' - url: https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl - sha256: 5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124 + version: '24.2' + url: https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl + sha256: 09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759 requires_python: '>=3.8' - kind: pypi name: pluggy @@ -973,13 +988,13 @@ packages: requires_python: '>=3.8' - kind: pypi name: pytest - version: 8.3.2 - url: https://files.pythonhosted.org/packages/0f/f9/cf155cf32ca7d6fa3601bc4c5dd19086af4b320b706919d48a4c79081cf9/pytest-8.3.2-py3-none-any.whl - sha256: 4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5 + version: 8.3.3 + url: https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl + sha256: a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2 requires_dist: - iniconfig - packaging - - pluggy<2,>=1.5 + - pluggy>=1.5,<2 - exceptiongroup>=1.0.0rc8 ; python_full_version < '3.11' - tomli>=1 ; python_full_version < '3.11' - colorama ; sys_platform == 'win32' diff --git a/examples/flask-hello-world-pyproject/pyproject.toml b/examples/flask-hello-world-pyproject/pyproject.toml index c951fab58..ba6e58f6f 100644 --- a/examples/flask-hello-world-pyproject/pyproject.toml +++ b/examples/flask-hello-world-pyproject/pyproject.toml @@ -6,9 +6,6 @@ readme = "README.md" requires-python = ">=3.11" version = "0.1.0" -[project.optional-dependencies] -test = ["pytest"] - [build-system] build-backend = "hatchling.build" requires = ["hatchling"] @@ -29,3 +26,6 @@ start = "python -m flask --app flask_hello_world_pyproject.app:app run --port=50 [tool.pixi.feature.test.tasks] test = "pytest -v tests/*" + +[dependency-groups] +test = ["pytest>=8.3.3,<9"] diff --git a/src/cli/add.rs b/src/cli/add.rs index 0c79a6f8b..3c310f8c1 100644 --- a/src/cli/add.rs +++ b/src/cli/add.rs @@ -1,4 +1,4 @@ -use clap::Parser; +use clap::{Parser, ValueEnum}; use indexmap::IndexMap; use pixi_manifest::FeatureName; @@ -37,18 +37,28 @@ use crate::{ /// /// Mixing `--platform` and `--build`/`--host` flags is supported /// -/// The `--pypi` option will add the package as a pypi dependency. This can not +/// The `--pypi` option will add the package as a pypi dependency. This cannot /// be mixed with the conda dependencies /// - `pixi add --pypi boto3` /// - `pixi add --pypi "boto3==version" /// /// If the project manifest is a `pyproject.toml`, adding a pypi dependency will /// add it to the native pyproject `project.dependencies` array or to the native -/// `project.optional-dependencies` table if a feature is specified: +/// `dependency-groups` table if a feature is specified: /// - `pixi add --pypi boto3` will add `boto3` to the `project.dependencies` /// array /// - `pixi add --pypi boto3 --feature aws` will add `boto3` to the -/// `project.dependencies.aws` array +/// `dependency-groups.aws` array +/// +/// Note that if `--platform` or `--editable` are specified, the pypi dependency +/// will be added to the `tool.pixi.pypi-dependencies` table instead as native +/// arrays have no support for platform-specific or editable dependencies. +/// +/// Specifying `--location` to `pixi` will force adding the pypi dependency +/// to the `tool.pixi.pypi-dependencies` table. Specifying `--location` to +/// `optional-dependencies` will force adding the pypi dependency +/// to the `project.optional-dependencies` table (this will only have an effect +/// if a non-default feature is also specified). /// /// These dependencies will then be read by pixi as if they had been added to /// the pixi `pypi-dependencies` tables of the default or of a named feature. @@ -73,6 +83,29 @@ pub struct Args { /// Whether the pypi requirement should be editable #[arg(long, requires = "pypi")] pub editable: bool, + + /// Where to add the pypi requirement to the manifest + #[arg(long, requires = "pypi")] + pub location: Option, +} + +#[derive(Parser, Debug, Clone, PartialEq, ValueEnum)] +pub enum PypiDependencyLocation { + // The [pypi-dependencies] or [tool.pixi.pypi-dependencies] table + Pixi, + // The [project.optional-dependencies] table in a 'pyproject.toml' manifest + OptionalDependencies, +} + +impl From for pixi_manifest::PypiDependencyLocation { + fn from(val: PypiDependencyLocation) -> Self { + match val { + PypiDependencyLocation::Pixi => pixi_manifest::PypiDependencyLocation::Pixi, + PypiDependencyLocation::OptionalDependencies => { + pixi_manifest::PypiDependencyLocation::OptionalDependencies + } + } + } } pub async fn execute(args: Args) -> miette::Result<()> { @@ -112,6 +145,7 @@ pub async fn execute(args: Args) -> miette::Result<()> { // TODO: add dry_run logic to add let dry_run = false; + let location = args.location.map(|l| l.into()); let update_deps = project .update_dependencies( match_specs, @@ -120,6 +154,7 @@ pub async fn execute(args: Args) -> miette::Result<()> { &args.dependency_config.feature, &args.dependency_config.platforms, args.editable, + &location, dry_run, ) .await?; diff --git a/src/cli/init.rs b/src/cli/init.rs index 296da842b..3b5a1a9af 100644 --- a/src/cli/init.rs +++ b/src/cli/init.rs @@ -257,6 +257,7 @@ pub async fn execute(args: Args) -> miette::Result<()> { &FeatureName::default(), None, DependencyOverwriteBehavior::Overwrite, + &None, )?; } project.save()?; diff --git a/src/cli/upgrade.rs b/src/cli/upgrade.rs index 428e3a5b0..1c416afd9 100644 --- a/src/cli/upgrade.rs +++ b/src/cli/upgrade.rs @@ -179,6 +179,7 @@ pub async fn execute(args: Args) -> miette::Result<()> { &args.specs.feature, &[], false, + &None, args.dry_run, ) .await?; diff --git a/src/project/mod.rs b/src/project/mod.rs index 14254daee..30d8e24da 100644 --- a/src/project/mod.rs +++ b/src/project/mod.rs @@ -32,7 +32,8 @@ use pixi_config::{Config, PinningStrategy}; use pixi_consts::consts; use pixi_manifest::{ pypi::PyPiPackageName, DependencyOverwriteBehavior, EnvironmentName, Environments, FeatureName, - FeaturesExt, HasFeaturesIter, HasManifestRef, Manifest, ParsedManifest, SpecType, + FeaturesExt, HasFeaturesIter, HasManifestRef, Manifest, ParsedManifest, PypiDependencyLocation, + SpecType, }; use pixi_utils::reqwest::build_reqwest_clients; use pypi_mapping::{ChannelName, CustomMapping, MappingLocation, MappingSource}; @@ -639,6 +640,7 @@ impl Project { feature_name: &FeatureName, platforms: &[Platform], editable: bool, + location: &Option, dry_run: bool, ) -> Result, miette::Error> { let mut conda_specs_to_add_constraints_for = IndexMap::new(); @@ -670,6 +672,7 @@ impl Project { feature_name, Some(editable), DependencyOverwriteBehavior::Overwrite, + location, )?; if added { if spec.version_or_url.is_none() { @@ -761,6 +764,7 @@ impl Project { feature_name, platforms, editable, + location, )?; implicit_constraints.extend(pypi_constraints); } @@ -895,6 +899,7 @@ impl Project { /// Update the pypi specs of newly added packages based on the contents of the /// updated lock-file. + #[allow(clippy::too_many_arguments)] fn update_pypi_specs_from_lock_file( &mut self, updated_lock_file: &LockFile, @@ -903,6 +908,7 @@ impl Project { feature_name: &FeatureName, platforms: &[Platform], editable: bool, + location: &Option, ) -> miette::Result> { let mut implicit_constraints = HashMap::new(); @@ -949,6 +955,7 @@ impl Project { feature_name, Some(editable), DependencyOverwriteBehavior::Overwrite, + location, )?; } } diff --git a/tests/integration_rust/common/mod.rs b/tests/integration_rust/common/mod.rs index 302ab98fe..5c2ef7a0b 100644 --- a/tests/integration_rust/common/mod.rs +++ b/tests/integration_rust/common/mod.rs @@ -312,6 +312,7 @@ impl PixiControl { revalidate: false, }, editable: false, + location: None, }, } }