Skip to content

Commit 48e286c

Browse files
committed
Convert chipflow.toml parsing to pydantic
1 parent 77b4b89 commit 48e286c

File tree

5 files changed

+152
-113
lines changed

5 files changed

+152
-113
lines changed

chipflow_lib/__init__.py

Lines changed: 21 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import importlib.metadata
2-
import jsonschema
32
import os
43
import sys
54
import tomli
5+
from pathlib import Path
6+
from pydantic import ValidationError
67

78
__version__ = importlib.metadata.version("chipflow_lib")
89

@@ -21,7 +22,7 @@ def _get_cls_by_reference(reference, context):
2122
return getattr(module_obj, class_ref)
2223
except AttributeError as e:
2324
raise ChipFlowError(f"Module `{module_ref}` referenced by {context} does not define "
24-
f"`{class_ref}`") from e
25+
f"`{class_ref}`") from e
2526

2627

2728
def _ensure_chipflow_root():
@@ -32,114 +33,31 @@ def _ensure_chipflow_root():
3233
return os.environ["CHIPFLOW_ROOT"]
3334

3435

35-
# TODO: convert to pydantic, one truth of source for the schema
36-
config_schema = {
37-
"$schema": "https://json-schema.org/draft/2020-12/schema",
38-
"$id": "https://chipflow.io/meta/chipflow.toml.schema.json",
39-
"title": "chipflow.toml",
40-
"type": "object",
41-
"required": [
42-
"chipflow"
43-
],
44-
"properties": {
45-
"chipflow": {
46-
"type": "object",
47-
"required": [
48-
"steps",
49-
"silicon"
50-
],
51-
"additionalProperties": False,
52-
"properties": {
53-
"project_name": {
54-
"type": "string",
55-
},
56-
"top": {
57-
"type": "object",
58-
},
59-
"steps": {
60-
"type": "object",
61-
},
62-
"clocks": {
63-
"type": "object",
64-
"patternPropertues": {
65-
".+": {"type": "string"}
66-
},
67-
},
68-
"resets": {
69-
"type": "object",
70-
"patternPropertues": {
71-
".+": {"type": "string"}
72-
},
73-
},
74-
"silicon": {
75-
"type": "object",
76-
"required": [
77-
"process",
78-
"package",
79-
],
80-
"additionalProperties": False,
81-
"properties": {
82-
"process": {
83-
"type": "string",
84-
"enum": ["sky130", "gf180", "customer1", "gf130bcd", "ihp_sg13g2"]
85-
},
86-
"package": {
87-
"enum": ["caravel", "cf20", "pga144"]
88-
},
89-
"pads": {"$ref": "#/$defs/pin"},
90-
"power": {"$ref": "#/$defs/pin"},
91-
"debug": {
92-
"type": "object",
93-
"properties": {
94-
"heartbeat": {"type": "boolean"}
95-
}
96-
}
97-
},
98-
},
99-
},
100-
},
101-
},
102-
"$defs": {
103-
"pin": {
104-
"type": "object",
105-
"additionalProperties": False,
106-
"minProperties": 1,
107-
"patternProperties": {
108-
".+": {
109-
"type": "object",
110-
"required": [
111-
"type",
112-
"loc",
113-
],
114-
"additionalProperties": False,
115-
"properties": {
116-
"type": {
117-
"enum": ["io", "i", "o", "oe", "clock", "reset", "power", "ground"]
118-
},
119-
"loc": {
120-
"type": "string",
121-
"pattern": "^[NSWE]?[0-9]+$"
122-
},
123-
}
124-
}
125-
}
126-
}
127-
}
128-
}
129-
130-
13136
def _parse_config():
37+
"""Parse the chipflow.toml configuration file."""
13238
chipflow_root = _ensure_chipflow_root()
133-
config_file = f"{chipflow_root}/chipflow.toml"
39+
config_file = Path(chipflow_root) / "chipflow.toml"
13440
return _parse_config_file(config_file)
13541

13642

13743
def _parse_config_file(config_file):
44+
"""Parse a specific chipflow.toml configuration file."""
45+
from .config_models import Config
46+
13847
with open(config_file, "rb") as f:
13948
config_dict = tomli.load(f)
14049

14150
try:
142-
jsonschema.validate(config_dict, config_schema)
143-
return config_dict
144-
except jsonschema.ValidationError as e:
145-
raise ChipFlowError(f"Syntax error in `chipflow.toml` at `{'.'.join(e.path)}`: {e.message}")
51+
# Validate with Pydantic
52+
config = Config.model_validate(config_dict)
53+
return config_dict # Return the original dict for backward compatibility
54+
except ValidationError as e:
55+
# Format Pydantic validation errors in a user-friendly way
56+
error_messages = []
57+
for error in e.errors():
58+
location = ".".join(str(loc) for loc in error["loc"])
59+
message = error["msg"]
60+
error_messages.append(f"Error at '{location}': {message}")
61+
62+
error_str = "\n".join(error_messages)
63+
raise ChipFlowError(f"Validation error in chipflow.toml:\n{error_str}")

docs/chipflow-toml-guide.rst

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,12 @@ You probably won't need to change these if you're starting from an example repos
5757
.. code-block:: TOML
5858
5959
[chipflow.silicon]
60-
processes = [
61-
"ihp_sg13g2",
62-
"gf130bcd"
63-
]
60+
process = "ihp_sg13g2"
6461
package = "pga144"
6562
6663
67-
The ``silicon`` section sets the Foundry ``processes`` (i.e. PDKs) that we are targeting for manufacturing, and the physical ``package`` we want to place our design inside.
68-
You'll choose the ``processes`` and ``package`` based in the requirements of your design.
64+
The ``silicon`` section sets the Foundry ``process`` (i.e. PDK) that we are targeting for manufacturing, and the physical ``package`` we want to place our design inside.
65+
You'll choose the ``process`` and ``package`` based in the requirements of your design.
6966

7067
Available processes
7168
-------------------

tests/test_config_models.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# SPDX-License-Identifier: BSD-2-Clause
2+
import os
3+
import unittest
4+
5+
from chipflow_lib.config_models import Config, PadConfig
6+
from chipflow_lib.platforms.utils import Process
7+
8+
9+
class ConfigModelsTestCase(unittest.TestCase):
10+
def setUp(self):
11+
os.environ["CHIPFLOW_ROOT"] = os.path.dirname(os.path.dirname(__file__))
12+
13+
# Create a valid config dict directly to test the model
14+
self.valid_config_dict = {
15+
"chipflow": {
16+
"project_name": "test-chip",
17+
"steps": {
18+
"silicon": "chipflow_lib.steps.silicon:SiliconStep"
19+
},
20+
"top": {},
21+
"silicon": {
22+
"process": "sky130",
23+
"package": "cf20",
24+
"pads": {
25+
"sys_clk": {"type": "clock", "loc": "114"}
26+
},
27+
"power": {
28+
"vdd": {"type": "power", "loc": "1"}
29+
}
30+
}
31+
}
32+
}
33+
34+
def test_config_validation(self):
35+
"""Test that the Config model validates a known-good config."""
36+
config = Config.model_validate(self.valid_config_dict)
37+
self.assertEqual(config.chipflow.project_name, "test-chip")
38+
self.assertEqual(config.chipflow.silicon.package, "cf20")
39+
self.assertEqual(config.chipflow.silicon.process, Process.SKY130)
40+
41+
def test_pad_config(self):
42+
"""Test validation of pad configuration."""
43+
pad = PadConfig(type="clock", loc="114")
44+
self.assertEqual(pad.type, "clock")
45+
self.assertEqual(pad.loc, "114")
46+
47+
# Test validation of loc format
48+
with self.assertRaises(ValueError):
49+
PadConfig(type="clock", loc="invalid-format")
50+
51+
def test_nested_structure(self):
52+
"""Test the nested structure of the Config model."""
53+
config = Config.model_validate(self.valid_config_dict)
54+
55+
# Test silicon configuration
56+
silicon = config.chipflow.silicon
57+
self.assertEqual(silicon.package, "cf20")
58+
59+
# Test pads
60+
self.assertEqual(len(silicon.pads), 1)
61+
pad = silicon.pads["sys_clk"]
62+
self.assertEqual(pad.type, "clock")
63+
self.assertEqual(pad.loc, "114")
64+
65+
# Test power
66+
self.assertEqual(len(silicon.power), 1)
67+
power = silicon.power["vdd"]
68+
self.assertEqual(power.type, "power")
69+
self.assertEqual(power.loc, "1")

tests/test_init.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ def test_parse_config_file_invalid_schema(self):
121121
with self.assertRaises(ChipFlowError) as cm:
122122
_parse_config_file(config_path)
123123

124-
self.assertIn("Syntax error in `chipflow.toml`", str(cm.exception))
124+
self.assertIn("Validation error in chipflow.toml", str(cm.exception))
125125

126126
@mock.patch("chipflow_lib._ensure_chipflow_root")
127127
@mock.patch("chipflow_lib._parse_config_file")
@@ -133,6 +133,9 @@ def test_parse_config(self, mock_parse_config_file, mock_ensure_chipflow_root):
133133
config = _parse_config()
134134

135135
mock_ensure_chipflow_root.assert_called_once()
136-
# We're expecting a string, not a Path
137-
mock_parse_config_file.assert_called_once_with("/mock/chipflow/root/chipflow.toml")
138-
self.assertEqual(config, {"chipflow": {"test": "value"}})
136+
# Accept either string or Path object
137+
self.assertEqual(mock_parse_config_file.call_args[0][0].as_posix()
138+
if hasattr(mock_parse_config_file.call_args[0][0], 'as_posix')
139+
else mock_parse_config_file.call_args[0][0],
140+
"/mock/chipflow/root/chipflow.toml")
141+
self.assertEqual(config, {"chipflow": {"test": "value"}})

tests/test_parse_config.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# SPDX-License-Identifier: BSD-2-Clause
2+
import os
3+
import unittest
4+
from pathlib import Path
5+
6+
from chipflow_lib import _parse_config_file
7+
from chipflow_lib.config_models import Config
8+
9+
10+
class ParseConfigTestCase(unittest.TestCase):
11+
def setUp(self):
12+
os.environ["CHIPFLOW_ROOT"] = os.path.dirname(os.path.dirname(__file__))
13+
current_dir = os.path.dirname(__file__)
14+
self.example_config = Path(os.environ["CHIPFLOW_ROOT"]) / "docs" / "example-chipflow.toml"
15+
self.mock_config = Path(current_dir) / "fixtures" / "mock.toml"
16+
17+
def test_example_config_parsing(self):
18+
"""Test that the example chipflow.toml can be parsed with our Pydantic models."""
19+
if self.example_config.exists():
20+
config_dict = _parse_config_file(self.example_config)
21+
self.assertIn("chipflow", config_dict)
22+
self.assertIn("silicon", config_dict["chipflow"])
23+
24+
# Validate using Pydantic model
25+
config = Config.model_validate(config_dict)
26+
self.assertEqual(config.chipflow.project_name, "test-chip")
27+
self.assertEqual(config.chipflow.silicon.package, "pga144")
28+
self.assertEqual(str(config.chipflow.silicon.process), "GF130BCD")
29+
30+
def test_mock_config_parsing(self):
31+
"""Test that the mock chipflow.toml can be parsed with our Pydantic models."""
32+
if self.mock_config.exists():
33+
config_dict = _parse_config_file(self.mock_config)
34+
self.assertIn("chipflow", config_dict)
35+
self.assertIn("silicon", config_dict["chipflow"])
36+
37+
# Validate using Pydantic model
38+
config = Config.model_validate(config_dict)
39+
self.assertEqual(config.chipflow.project_name, "proj-name")
40+
self.assertEqual(config.chipflow.silicon.package, "pga144")
41+
42+
# Check that our model correctly handles the legacy format
43+
self.assertIn("sys_clk", config.chipflow.silicon.pads)
44+
self.assertEqual(config.chipflow.silicon.pads["sys_clk"].type, "clock")
45+
46+
# Check power pins (should be auto-assigned type='power')
47+
self.assertIn("vss", config.chipflow.silicon.power)
48+
self.assertEqual(config.chipflow.silicon.power["vss"].type, "power")
49+
50+
51+
if __name__ == "__main__":
52+
unittest.main()

0 commit comments

Comments
 (0)