@@ -60,48 +60,101 @@ def _initialize_pixi_data(
6060 return pixi_data
6161
6262
63- def _process_dependencies (
63+ def group_by_origin (
64+ resolved_deps : dict [str , dict [Platform | None , dict [CondaPip , Spec ]]],
65+ ) -> dict [Path , dict [str , dict [Platform | None , dict [CondaPip , Spec ]]]]:
66+ groups : dict [Path , dict [str , dict [Platform | None , dict [CondaPip , Spec ]]]] = {}
67+ for pkg_name , platform_map in resolved_deps .items ():
68+ for plat , manager_map in platform_map .items ():
69+ for manager , spec in manager_map .items ():
70+ for origin in spec .origin :
71+ # Normalize origin to a Path object
72+ origin_path = Path (origin )
73+ groups .setdefault (origin_path , {})
74+ groups [origin_path ].setdefault (pkg_name , {})
75+ groups [origin_path ][pkg_name ].setdefault (plat , {})
76+ groups [origin_path ][pkg_name ][plat ][manager ] = spec
77+ return groups
78+
79+
80+ def _process_dependencies ( # noqa: PLR0912
6481 pixi_data : dict [str , dict [str , Any ]],
6582 resolved_dependencies : dict [str , dict [Platform | None , dict [CondaPip , Spec ]]],
6683) -> None :
67- # Extract conda and pip dependencies
68- conda_deps , pip_deps = _extract_conda_pip_dependencies (resolved_dependencies )
69-
70- # Process conda dependencies
71- for pkg_name , platform_to_spec in conda_deps .items ():
72- for _platform , spec in platform_to_spec .items ():
73- pin = spec .pin or "*"
74- if _platform is None :
75- # Applies to all platforms
76- pixi_data ["dependencies" ][pkg_name ] = pin
77- else :
78- # Platform-specific dependency
79- # Ensure target section exists
80- target = pixi_data ["target" ].setdefault (_platform , {})
81- deps = target .setdefault ("dependencies" , {})
82- deps [pkg_name ] = pin
83-
84- # Process pip dependencies
85- for pkg_name , platform_to_spec in pip_deps .items ():
86- for _platform , spec in platform_to_spec .items ():
87- pin = spec .pin or "*"
88- if _platform is None :
89- # Applies to all platforms
90- pixi_data ["pypi-dependencies" ][pkg_name ] = pin
91- else :
92- # Platform-specific dependency
93- # Ensure target section exists
94- target = pixi_data ["target" ].setdefault (_platform , {})
95- deps = target .setdefault ("pypi-dependencies" , {})
96- deps [pkg_name ] = pin
97-
98- # Remove empty sections if necessary
99- if not pixi_data ["dependencies" ]:
100- del pixi_data ["dependencies" ]
101- if not pixi_data ["pypi-dependencies" ]:
102- del pixi_data ["pypi-dependencies" ]
103- if not pixi_data ["target" ]:
104- del pixi_data ["target" ]
84+ """Process the resolved dependencies and update the pixi manifest data.
85+
86+ This function first groups the resolved dependencies by origin (using
87+ group_by_origin) and then creates a separate feature (under the "feature"
88+ key in pixi_data) for each origin. The feature name is derived using the
89+ parent directory's stem of the origin file.
90+
91+ After creating the per-origin features, if the manifest does not yet have an
92+ "environments" table, we automatically add one with:
93+ - a "default" environment that includes all features, and
94+ - one environment per feature (with the feature name as the sole member).
95+ """
96+ # --- Step 1: Group by origin and create per-origin features ---
97+ origin_groups = group_by_origin (resolved_dependencies )
98+ features = pixi_data .setdefault ("feature" , {})
99+
100+ for origin_path , group_deps in origin_groups .items ():
101+ # Derive a feature name from the parent folder of the origin file.
102+ feature_name = origin_path .resolve ().parent .stem
103+
104+ # Initialize the feature entry.
105+ feature_entry : dict [str , Any ] = {
106+ "dependencies" : {},
107+ "pypi-dependencies" : {},
108+ "target" : {},
109+ }
110+
111+ # Extract conda and pip dependencies from the grouped data.
112+ group_conda , group_pip = _extract_conda_pip_dependencies (group_deps )
113+
114+ # Process conda dependencies for this feature.
115+ for pkg_name , platform_to_spec in group_conda .items ():
116+ for _platform , spec in platform_to_spec .items ():
117+ pin = spec .pin or "*"
118+ if _platform is None :
119+ feature_entry ["dependencies" ][pkg_name ] = pin
120+ else :
121+ target = feature_entry ["target" ].setdefault (_platform , {})
122+ deps = target .setdefault ("dependencies" , {})
123+ deps [pkg_name ] = pin
124+
125+ # Process pip dependencies for this feature.
126+ for pkg_name , platform_to_spec in group_pip .items ():
127+ for _platform , spec in platform_to_spec .items ():
128+ pin = spec .pin or "*"
129+ if _platform is None :
130+ feature_entry ["pypi-dependencies" ][pkg_name ] = pin
131+ else :
132+ target = feature_entry ["target" ].setdefault (_platform , {})
133+ deps = target .setdefault ("pypi-dependencies" , {})
134+ deps [pkg_name ] = pin
135+
136+ # Remove empty sections.
137+ if not feature_entry ["dependencies" ]:
138+ del feature_entry ["dependencies" ]
139+ if not feature_entry ["pypi-dependencies" ]:
140+ del feature_entry ["pypi-dependencies" ]
141+ if not feature_entry ["target" ]:
142+ del feature_entry ["target" ]
143+
144+ # Save this feature entry.
145+ features [feature_name ] = feature_entry
146+
147+ # --- Step 2: Automatically add the environments table if not already defined ---
148+ if "environments" not in pixi_data :
149+ all_features = list (features .keys ())
150+ pixi_data ["environments" ] = {}
151+ # The "default" environment will include all features.
152+ pixi_data ["environments" ]["default" ] = all_features
153+ # Also create one environment per feature.
154+ for feat in all_features :
155+ # Environment names cannot use _, only lowercase letters, digits, and -
156+ name = feat .replace ("_" , "-" )
157+ pixi_data ["environments" ][name ] = [feat ]
105158
106159
107160def _write_pixi_toml (
0 commit comments