Skip to content

Commit 6e48656

Browse files
authored
feat(py-pixi): Add option to create ProjectModelV1 from json and dict (#373)
Add option to create ProjectModelV1 from json and dict
1 parent 4d8fd4f commit 6e48656

File tree

9 files changed

+319
-136
lines changed

9 files changed

+319
-136
lines changed

backends/pixi-build-ros/pixi.lock

Lines changed: 130 additions & 125 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backends/pixi-build-ros/pixi.toml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ test = "pytest tests"
2121
[feature.test.dependencies]
2222
pixi-pycharm = ">=0.0.8,<0.1"
2323
pytest = "*"
24-
py-pixi-build-backend = "*"
25-
# Skipping as it's a long build, uncomment this when developing from source
26-
# py-pixi-build-backend = { path = "../../py-pixi-build-backend" }
24+
# You can skip this as it's a long build, uncomment this when developing only in this package
25+
#py-pixi-build-backend = "*"
26+
py-pixi-build-backend = { path = "../../py-pixi-build-backend" }
2727

2828
[package.build.backend]
2929
name = "pixi-build-python"
@@ -44,4 +44,6 @@ pyyaml = "*"
4444
pixi-build-api-version = ">=2,<3"
4545
# should be added to `py-pixi-build-backend`
4646
typing-extensions = "*"
47-
py-pixi-build-backend = "*"
47+
# this depends has to match the test dependency, so switch comments if needed
48+
# py-pixi-build-backend = "*"
49+
py-pixi-build-backend = { path = "../../py-pixi-build-backend" }

backends/pixi-build-ros/tests/test_package_xml.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
from pathlib import Path
1+
import json
2+
import shutil
23
import tempfile
4+
import tomllib
5+
from pathlib import Path
36
from typing import Dict
47

58
from pixi_build_ros.distro import Distro
@@ -144,6 +147,56 @@ def test_generate_recipe(package_xmls: Path):
144147
)
145148

146149

150+
def test_recipe_includes_project_run_dependency(package_xmls: Path):
151+
"""Ensure dependencies declared in project manifest merge into run requirements."""
152+
153+
with tempfile.TemporaryDirectory() as temp_dir:
154+
temp_path = Path(temp_dir)
155+
156+
package_xml_source = package_xmls / "custom_ros.xml"
157+
package_xml_dest = temp_path / "package.xml"
158+
package_xml_dest.write_text(package_xml_source.read_text(encoding="utf-8"))
159+
160+
model_payload = {
161+
"name": "custom_ros",
162+
"version": "0.0.1",
163+
"description": "Demo",
164+
"authors": ["Tester the Tester"],
165+
"targets": {
166+
"defaultTarget": {
167+
"hostDependencies": {},
168+
"buildDependencies": {},
169+
"runDependencies": {
170+
"rich": {
171+
"binary": {
172+
"version": ">=10.0"
173+
}
174+
}
175+
},
176+
},
177+
"targets": {},
178+
},
179+
}
180+
model = ProjectModelV1.from_json(json.dumps(model_payload))
181+
182+
config = {"distro": "noetic", "noarch": False}
183+
host_platform = Platform.current()
184+
generator = ROSGenerator()
185+
186+
generated_recipe = generator.generate_recipe(
187+
model=model,
188+
config=config,
189+
manifest_path=str(temp_path),
190+
host_platform=host_platform,
191+
)
192+
193+
run_requirements = [str(dep) for dep in generated_recipe.recipe.requirements.run]
194+
195+
assert any("rich" in dep for dep in run_requirements), (
196+
f"Expected pixi run dependency 'rich' missing from recipe run requirements"
197+
)
198+
199+
147200
def test_robostack_target_platform_linux(package_map: Dict[str, PackageMapEntry]):
148201
"""Test that target platform correctly selects Linux packages from robostack.yaml."""
149202
distro = Distro("jazzy")

py-pixi-build-backend/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ serde = { version = "1.0", features = ["derive"] }
3636
serde_json = "1.0"
3737
serde_yaml = "0.9"
3838
tokio = { version = "1.43.0", features = ["macros", "rt-multi-thread", "rt"] }
39-
4039
rattler_conda_types = { version = "0.39.0", default-features = false }
4140

4241
rattler-build = "*"

py-pixi-build-backend/pixi_build_backend/types/project_model.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from typing import Optional, List
1+
from typing import Optional, List, Mapping, Dict, Any
2+
from pathlib import Path
23
from pixi_build_backend.pixi_build_backend import (
34
PyProjectModelV1,
45
)
@@ -35,6 +36,27 @@ def _from_py(cls, model: PyProjectModelV1) -> "ProjectModelV1":
3536
instance._inner = model
3637
return instance
3738

39+
@classmethod
40+
def from_json(cls, json: str) -> "ProjectModelV1":
41+
"""Create a ProjectModelV1 from a JSON document."""
42+
instance = cls.__new__(cls)
43+
instance._inner = PyProjectModelV1.from_json(json)
44+
return instance
45+
46+
@classmethod
47+
def from_dict(cls, data: Mapping[str, Any] | Dict[str, Any]) -> "ProjectModelV1":
48+
"""Create a ProjectModelV1 from a Python mapping."""
49+
instance = cls.__new__(cls)
50+
instance._inner = PyProjectModelV1.from_dict(dict(data))
51+
return instance
52+
53+
@classmethod
54+
def from_json_file(cls, path: Path | str) -> "ProjectModelV1":
55+
"""Create a ProjectModelV1 from a JSON file."""
56+
instance = cls.__new__(cls)
57+
instance._inner = PyProjectModelV1.from_json_file(str(path))
58+
return instance
59+
3860
@property
3961
def version(self) -> Optional[str]:
4062
"""

py-pixi-build-backend/src/types/project_model.rs

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1-
use std::str::FromStr;
1+
use std::{
2+
fs,
3+
str::FromStr,
4+
};
25

3-
use pixi_build_types::ProjectModelV1;
4-
use pyo3::prelude::*;
5-
use rattler_conda_types::Version;
6+
use pixi_build_types::{ProjectModelV1};
7+
use pyo3::{exceptions::PyValueError, prelude::*};
8+
use rattler_conda_types::{Version};
9+
use serde_json::from_str;
10+
use pythonize::depythonize;
611

712
#[pyclass]
813
#[derive(Clone)]
@@ -35,6 +40,34 @@ impl PyProjectModelV1 {
3540
}
3641
}
3742

43+
#[staticmethod]
44+
pub fn from_json(json: &str) -> PyResult<Self> {
45+
let project: ProjectModelV1 = from_str(json).map_err(|err| {
46+
PyErr::new::<PyValueError, _>(format!(
47+
"Failed to parse ProjectModelV1 from JSON: {err}"
48+
))
49+
})?;
50+
51+
Ok(PyProjectModelV1 { inner: project })
52+
}
53+
54+
#[staticmethod]
55+
pub fn from_dict(value: &Bound<PyAny>) -> PyResult<Self> {
56+
let project: ProjectModelV1 = depythonize(value)?;
57+
Ok(PyProjectModelV1 { inner: project })
58+
}
59+
60+
#[staticmethod]
61+
pub fn from_json_file(path: &str) -> PyResult<Self> {
62+
let content = fs::read_to_string(path).map_err(|err| {
63+
PyErr::new::<PyValueError, _>(format!(
64+
"Failed to read ProjectModelV1 JSON file '{path}': {err}"
65+
))
66+
})?;
67+
68+
Self::from_json(&content)
69+
}
70+
3871
#[getter]
3972
pub fn name(&self) -> Option<&String> {
4073
self.inner.name.as_ref()
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"name": "minimal-package",
3+
"version": "1.0.0",
4+
"description": null,
5+
"authors": null,
6+
"license": null,
7+
"license_file": null,
8+
"readme": null,
9+
"homepage": null,
10+
"repository": null,
11+
"documentation": null,
12+
"targets": {
13+
"defaultTarget": {
14+
"hostDependencies": {},
15+
"buildDependencies": {},
16+
"runDependencies": {
17+
"python": {
18+
"binary": {
19+
"version": ">=3.8"
20+
}
21+
}
22+
}
23+
},
24+
"targets": {
25+
"Linux": {
26+
"hostDependencies": {
27+
"rich": {
28+
"binary": {
29+
"version": ">=10.0"
30+
}
31+
}
32+
},
33+
"buildDependencies": {},
34+
"runDependencies": {}
35+
}
36+
}
37+
}
38+
}

py-pixi-build-backend/tests/unit/__snapshots__/test_project_model.ambr

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,12 @@
22
# name: test_project_model_initialization
33
'ProjectModelV1 { name: Some("test_project"), version: Some(Version { version: [[0], [1], [0], [0]], local: [] }), description: None, authors: None, license: None, license_file: None, readme: None, homepage: None, repository: None, documentation: None, targets: None }'
44
# ---
5+
# name: test_project_model_initialization_from_json
6+
'ProjectModelV1 { name: Some("test_project"), version: Some(Version { version: [[0], [1], [0], [0]], local: [] }), description: None, authors: None, license: None, license_file: None, readme: None, homepage: None, repository: None, documentation: None, targets: None }'
7+
# ---
8+
# name: test_project_model_initialization_from_dict
9+
'ProjectModelV1 { name: Some("test_project"), version: Some(Version { version: [[0], [1], [0], [0]], local: [] }), description: None, authors: None, license: None, license_file: None, readme: None, homepage: None, repository: None, documentation: None, targets: None }'
10+
# ---
11+
# name: test_project_model_initialization_from_json_file
12+
'ProjectModelV1 { name: Some("minimal-package"), version: Some(Version { version: [[0], [1], [0], [0]], local: [] }), description: None, authors: None, license: None, license_file: None, readme: None, homepage: None, repository: None, documentation: None, targets: Some(TargetsV1 { default_target: Some(TargetV1 { host_dependencies: Some({}), build_dependencies: Some({}), run_dependencies: Some({"python": Binary(NamelessMatchSpecV1 { version: Range(GreaterEquals, Version { version: [[0], [3], [8]], local: [] }) })}) }), targets: Some({Platform("Linux"): TargetV1 { host_dependencies: Some({"rich": Binary(NamelessMatchSpecV1 { version: Range(GreaterEquals, Version { version: [[0], [10], [0]], local: [] }) })}), build_dependencies: Some({}), run_dependencies: Some({}) }}) }) }'
13+
# ---

py-pixi-build-backend/tests/unit/test_project_model.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""Unit tests for project_model.py module."""
22

33
from typing import Any
4+
from pathlib import Path
5+
import json
46
from pixi_build_backend.types.project_model import ProjectModelV1
57

68

@@ -9,3 +11,23 @@ def test_project_model_initialization(snapshot: Any) -> None:
911
model = ProjectModelV1(name="test_project", version="1.0.0")
1012

1113
assert model._debug_str() == snapshot
14+
15+
16+
def test_project_model_initialization_from_json(snapshot: Any) -> None:
17+
"""Test initialization of ProjectModelV1."""
18+
model = ProjectModelV1.from_json(json.dumps({"name": "test_project", "version": "1.0.0"}))
19+
20+
assert model._debug_str() == snapshot
21+
22+
23+
def test_project_model_initialization_from_dict(snapshot: Any) -> None:
24+
"""Test initialization of ProjectModelV1 from a Python mapping."""
25+
model = ProjectModelV1.from_dict({"name": "test_project", "version": "1.0.0"})
26+
27+
assert model._debug_str() == snapshot
28+
29+
30+
def test_project_model_initialization_from_json_file(snapshot: Any) -> None:
31+
json_file = Path(__file__).parent.parent / "data" / "project_model_example.json"
32+
model = ProjectModelV1.from_json_file(json_file)
33+
assert model._debug_str() == snapshot

0 commit comments

Comments
 (0)