Skip to content

Commit 1366a26

Browse files
authored
Merge 2ea07b6 into 00f1915
2 parents 00f1915 + 2ea07b6 commit 1366a26

File tree

5 files changed

+549
-30
lines changed

5 files changed

+549
-30
lines changed

PIXI_PLAN.md

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# Pixi Integration - Simple Implementation Plan
2+
3+
## Core Philosophy
4+
**Let UniDep translate, let Pixi resolve**
5+
6+
UniDep should act as a simple translator from `requirements.yaml`/`pyproject.toml` to `pixi.toml` format.
7+
Pixi handles all dependency resolution, conflict management, and lock file generation.
8+
9+
## Current Problem with `pixi` Branch
10+
- **Over-engineering**: Pre-resolves conflicts that Pixi can handle
11+
- **Origin tracking**: Complex system to track where each dependency came from
12+
- **Unnecessary complexity**: ~500+ lines of code for what should be ~100 lines
13+
14+
## New Simple Architecture
15+
16+
### Phase 1: Basic Pixi.toml Generation ✅
17+
- [x] Create minimal `_pixi.py` module (~100 lines)
18+
- [ ] Parse requirements WITHOUT resolution
19+
- [ ] Create features with literal dependencies
20+
- [ ] Generate pixi.toml with proper structure
21+
- [ ] Add `--pixi` flag to merge command
22+
23+
### Phase 2: Pixi Lock Command
24+
- [ ] Add `pixi-lock` subcommand to CLI
25+
- [ ] Simple wrapper around `pixi lock` command
26+
- [ ] Support platform selection
27+
- [ ] Add basic tests
28+
29+
### Phase 3: Monorepo Support (Optional)
30+
- [ ] Generate sub-lock files if needed
31+
- [ ] But let Pixi handle the complexity
32+
33+
## Implementation Details
34+
35+
### 1. Simple Pixi.toml Structure
36+
```python
37+
def generate_pixi_toml(requirements_files, output_file):
38+
pixi_data = {
39+
"project": {
40+
"name": "myenv",
41+
"channels": channels,
42+
"platforms": platforms,
43+
},
44+
"dependencies": {},
45+
"pypi-dependencies": {},
46+
}
47+
48+
# For monorepo: create features
49+
if len(requirements_files) > 1:
50+
pixi_data["feature"] = {}
51+
pixi_data["environments"] = {}
52+
53+
for req_file in requirements_files:
54+
feature_name = req_file.parent.stem
55+
deps = parse_single_file(req_file) # NO RESOLUTION!
56+
57+
pixi_data["feature"][feature_name] = {
58+
"dependencies": deps.conda, # Literal copy
59+
"pypi-dependencies": deps.pip, # Literal copy
60+
}
61+
62+
# Create environments
63+
all_features = list(pixi_data["feature"].keys())
64+
pixi_data["environments"]["default"] = all_features
65+
for feat in all_features:
66+
pixi_data["environments"][feat.replace("_", "-")] = [feat]
67+
```
68+
69+
### 2. Key Simplifications
70+
- **NO conflict resolution** - Pixi handles this
71+
- **NO origin tracking** - Not needed
72+
- **NO version pinning combination** - Pixi does this
73+
- **NO platform resolution** - Use Pixi's native platform support
74+
75+
### 3. Testing Strategy
76+
- Test pixi.toml generation (structure validation)
77+
- Test CLI integration
78+
- Test with example monorepo
79+
- Let Pixi handle the actual resolution testing
80+
81+
## Files to Create/Modify
82+
83+
### New Files
84+
1. `unidep/_pixi.py` - Simple pixi.toml generation (~100 lines)
85+
2. `tests/test_pixi.py` - Basic tests (~50 lines)
86+
87+
### Modified Files
88+
1. `unidep/_cli.py` - Add `--pixi` flag and `pixi-lock` command
89+
2. `README.md` - Document new Pixi support
90+
91+
## Success Criteria
92+
- [ ] Generate valid pixi.toml files
93+
- [ ] Pass all tests
94+
- [ ] Work with monorepo example
95+
- [ ] Total implementation < 200 lines (vs 500+ in old branch)
96+
97+
## Timeline
98+
- **Hour 1-2**: Basic pixi.toml generation ✅
99+
- **Hour 3-4**: CLI integration and testing
100+
- **Hour 5-6**: Documentation and polish
101+
102+
## Testing Checkpoints
103+
After each major change:
104+
1. Run tests: `pytest tests/test_pixi.py -xvs`
105+
2. Test with monorepo: `unidep merge --pixi tests/simple_monorepo`
106+
3. Validate pixi.toml: `pixi list`
107+
108+
## Commit Strategy
109+
- Commit after each working phase
110+
- Clear commit messages
111+
- Test before each commit

tests/test_pixi.py

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
"""Tests for simple Pixi.toml generation."""
2+
3+
from __future__ import annotations
4+
5+
import textwrap
6+
from typing import TYPE_CHECKING
7+
8+
from unidep._pixi import generate_pixi_toml
9+
10+
if TYPE_CHECKING:
11+
from pathlib import Path
12+
13+
14+
def test_simple_pixi_generation(tmp_path: Path) -> None:
15+
"""Test basic pixi.toml generation from a single requirements.yaml."""
16+
req_file = tmp_path / "requirements.yaml"
17+
req_file.write_text(
18+
textwrap.dedent(
19+
"""\
20+
channels:
21+
- conda-forge
22+
dependencies:
23+
- numpy >=1.20
24+
- pandas
25+
- pip: requests
26+
platforms:
27+
- linux-64
28+
- osx-arm64
29+
""",
30+
),
31+
)
32+
33+
output_file = tmp_path / "pixi.toml"
34+
generate_pixi_toml(
35+
req_file,
36+
project_name="test-project",
37+
output_file=output_file,
38+
verbose=False,
39+
)
40+
41+
assert output_file.exists()
42+
content = output_file.read_text()
43+
44+
# Check basic structure
45+
assert "[project]" in content
46+
assert 'name = "test-project"' in content
47+
assert "conda-forge" in content
48+
assert "linux-64" in content
49+
assert "osx-arm64" in content
50+
51+
# Check dependencies
52+
assert "[dependencies]" in content
53+
assert 'numpy = ">=1.20"' in content
54+
assert 'pandas = "*"' in content
55+
56+
assert "[pypi-dependencies]" in content
57+
assert 'requests = "*"' in content
58+
59+
60+
def test_monorepo_pixi_generation(tmp_path: Path) -> None:
61+
"""Test pixi.toml generation with features for multiple requirements files."""
62+
# Create project1
63+
project1_dir = tmp_path / "project1"
64+
project1_dir.mkdir()
65+
req1 = project1_dir / "requirements.yaml"
66+
req1.write_text(
67+
textwrap.dedent(
68+
"""\
69+
channels:
70+
- conda-forge
71+
dependencies:
72+
- numpy
73+
- conda: scipy
74+
""",
75+
),
76+
)
77+
78+
# Create project2
79+
project2_dir = tmp_path / "project2"
80+
project2_dir.mkdir()
81+
req2 = project2_dir / "requirements.yaml"
82+
req2.write_text(
83+
textwrap.dedent(
84+
"""\
85+
channels:
86+
- conda-forge
87+
dependencies:
88+
- pandas
89+
- pip: requests
90+
""",
91+
),
92+
)
93+
94+
output_file = tmp_path / "pixi.toml"
95+
generate_pixi_toml(
96+
req1,
97+
req2,
98+
project_name="monorepo",
99+
output_file=output_file,
100+
verbose=False,
101+
)
102+
103+
assert output_file.exists()
104+
content = output_file.read_text()
105+
106+
# Check project section
107+
assert "[project]" in content
108+
assert 'name = "monorepo"' in content
109+
110+
# Check feature dependencies (TOML writes them directly without parent section)
111+
assert "[feature.project1.dependencies]" in content
112+
assert 'numpy = "*"' in content
113+
assert 'scipy = "*"' in content
114+
115+
assert "[feature.project2.dependencies]" in content
116+
assert 'pandas = "*"' in content
117+
118+
assert "[feature.project2.pypi-dependencies]" in content
119+
assert 'requests = "*"' in content
120+
121+
# Check environments (be flexible with TOML formatting)
122+
assert "[environments]" in content
123+
assert "default =" in content
124+
assert "project1" in content
125+
assert "project2" in content
126+
# Verify that default includes both projects
127+
assert content.count('"project1"') >= 2 # In default and individual env
128+
assert content.count('"project2"') >= 2 # In default and individual env
129+
130+
131+
def test_pixi_with_version_pins(tmp_path: Path) -> None:
132+
"""Test that version pins are passed through without resolution."""
133+
req_file = tmp_path / "requirements.yaml"
134+
req_file.write_text(
135+
textwrap.dedent(
136+
"""\
137+
channels:
138+
- conda-forge
139+
dependencies:
140+
- numpy >=1.20,<2.0
141+
- conda: scipy =1.9.0
142+
- pip: requests >2.20
143+
""",
144+
),
145+
)
146+
147+
output_file = tmp_path / "pixi.toml"
148+
generate_pixi_toml(
149+
req_file,
150+
output_file=output_file,
151+
verbose=False,
152+
)
153+
154+
content = output_file.read_text()
155+
156+
# Check that pins are preserved exactly
157+
assert 'numpy = ">=1.20,<2.0"' in content
158+
assert 'scipy = "=1.9.0"' in content
159+
assert 'requests = ">2.20"' in content
160+
161+
162+
def test_pixi_empty_dependencies(tmp_path: Path) -> None:
163+
"""Test handling of requirements file with no dependencies."""
164+
req_file = tmp_path / "requirements.yaml"
165+
req_file.write_text(
166+
textwrap.dedent(
167+
"""\
168+
channels:
169+
- conda-forge
170+
platforms:
171+
- linux-64
172+
""",
173+
),
174+
)
175+
176+
output_file = tmp_path / "pixi.toml"
177+
generate_pixi_toml(
178+
req_file,
179+
output_file=output_file,
180+
verbose=False,
181+
)
182+
183+
assert output_file.exists()
184+
content = output_file.read_text()
185+
186+
# Should have project section but no dependencies sections
187+
assert "[project]" in content
188+
assert "[dependencies]" not in content
189+
assert "[pypi-dependencies]" not in content

0 commit comments

Comments
 (0)