Skip to content

Commit 51fedf4

Browse files
authored
Add docs to IR and JSON export (#484)
Structurally, this adds docs as a metadata field. indexed by the port or param name, with the block docstring itself being the empty string as the root. For json export, this only applies to blocks, not links. This changes the export format for port-arrays to allow a doc field. This also excludes all None (null) values from the export to reduce clutter but provides a default for importability.
1 parent 3f0f5ce commit 51fedf4

6 files changed

Lines changed: 88 additions & 23 deletions

File tree

edg/BoardCompiler.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@ def compile_board(design: Type[Block], target_dir_name: Optional[Tuple[str, str]
6565

6666
with open(compiled_json_filename, "w", encoding="utf-8") as compiled_json_file:
6767
compiled_json_file.write(
68-
CompiledDesignExportTransform.postprocess_serialized_json(compiled_json.model_dump_json(indent=2))
68+
CompiledDesignExportTransform.postprocess_serialized_json(
69+
compiled_json.model_dump_json(indent=2, exclude_none=True)
70+
)
6971
)
7072

7173
return compiled

edg/core/Blocks.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,19 @@ def _elaborated_def_to_proto(self) -> edgir.BlockLikeTypes:
345345

346346
return self._def_to_proto()
347347

348+
def _add_doc_metadata(self) -> None:
349+
"""Adds docstrings to metadata"""
350+
metadata_dict: Dict[str, str] = {}
351+
if self.__doc__:
352+
metadata_dict[""] = inspect.cleandoc(self.__doc__)
353+
for name, param in self._parameters.items():
354+
if param in self._param_docs:
355+
metadata_dict[name] = self._param_docs[param]
356+
for name, port in self._ports.items():
357+
if port in self._port_docs:
358+
metadata_dict[name] = self._port_docs[port]
359+
self._docs = self.Metadata(metadata_dict)
360+
348361
def _populate_def_proto_block_base(self, pb: edgir.BlockLikeTypes) -> None:
349362
"""Populates the structural parts of a block proto: parameters, ports, superclasses"""
350363
assert (
@@ -392,6 +405,7 @@ def _populate_def_proto_block_base(self, pb: edgir.BlockLikeTypes) -> None:
392405

393406
self._constraints.finalize() # needed for source locator generation
394407

408+
self._add_doc_metadata()
395409
self._populate_metadata(pb.meta, self._metadata, ref_map)
396410

397411
def _populate_def_proto_port_init(self, pb: edgir.BlockLikeTypes, ref_map: Refable.RefMapType) -> None:

edg/core/CompiledDesignExport.py

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,23 @@
1818
class CompiledParam(BaseModel):
1919
# this is minimalistic so the output json is more compact
2020
type: str
21-
value: Optional[Any] # solved value, if available
21+
value: Optional[Any] = None # solved value, if available
22+
value_excluded: Optional[bool] = None # true if value excluded, None otherwise to skip the field
23+
doc: Optional[str] = None # doc specified by its parent block, if available
2224

2325

24-
class CompiledPortArray(RootModel[Dict[str, Union["CompiledPort", "CompiledPortArray"]]]):
25-
pass
26+
class CompiledPortArray(BaseModel):
27+
doc: Optional[str] = None # doc specified by its parent block, if available
28+
elts: Dict[str, Union["CompiledPort", "CompiledPortArray"]]
2629

2730

2831
class CompiledPort(BaseModel):
2932
path: PathType # provide the full path to allow searchability
3033
cls: str # self class
3134
# path of connected port, if connected
3235
# for block ports, this is the link, if connected to one
33-
connected_path: Optional[Union[PathType, List[PathType]]]
36+
connected_path: Optional[Union[PathType, List[PathType]]] = None
37+
doc: Optional[str] = None # doc specified by its parent block, if available
3438
# note, link ports do not have parameters (they inherit parameters from connected ports and are deduplicated here)
3539
params: Dict[str, CompiledParam]
3640
ports: Dict[str, Union["CompiledPort", CompiledPortArray]]
@@ -48,6 +52,7 @@ class CompiledBlock(BaseModel):
4852
path: PathType # provide the full path to allow searchability
4953
cls: str # self class
5054
superclasses: List[str] # all superclasses
55+
doc: Optional[str] = None # docstring, if available
5156
params: Dict[str, CompiledParam]
5257
ports: Dict[str, Union[CompiledPortArray, CompiledPort]]
5358
blocks: Dict[str, "CompiledBlock"] # sub-blocks
@@ -108,14 +113,10 @@ def _param_value_to_json(cls, value: Any) -> Any:
108113

109114
def _param_to_compiled(self, path: Path, elt: edgir.ValInit) -> CompiledParam:
110115
if path.params[-1] in self._EXCLUDED_PARAM_VALUES:
111-
value: Optional[Any] = "<excluded>"
116+
return CompiledParam(type=self._param_to_type(elt), value_excluded=True)
112117
else:
113118
value = self._param_value_to_json(self._design.get_value(path.to_local_path()))
114-
115-
return CompiledParam(
116-
type=self._param_to_type(elt),
117-
value=value,
118-
)
119+
return CompiledParam(type=self._param_to_type(elt), value=value)
119120

120121
@override
121122
def transform_block(
@@ -126,16 +127,40 @@ def transform_block(
126127
blocks: Mapping[str, CompiledBlock],
127128
links: Mapping[str, CompiledLink],
128129
) -> CompiledBlock:
130+
ports_dict = dict(ports)
131+
params_dict = {
132+
param_pair.name: self._param_to_compiled(context.path.append_param(param_pair.name), param_pair.value)
133+
for param_pair in elt.params
134+
}
135+
136+
meta_docs = elt.meta.members.node.get("_docs")
137+
if meta_docs is not None:
138+
block_doc = meta_docs.members.node.get("")
139+
if block_doc is not None:
140+
block_doc = block_doc.text_leaf
141+
142+
for param_name, param_value in params_dict.items():
143+
if param_name in meta_docs.members.node:
144+
param_doc = meta_docs.members.node.get(param_name)
145+
if param_doc is not None:
146+
param_value.doc = param_doc.text_leaf
147+
148+
for port_name, port_value in ports_dict.items():
149+
if port_name in meta_docs.members.node:
150+
port_doc = meta_docs.members.node.get(port_name)
151+
if port_doc is not None:
152+
port_value.doc = port_doc.text_leaf
153+
else:
154+
block_doc = None
155+
129156
return CompiledBlock(
130157
path=self._path_to_path(context.path),
131158
cls=self._libpath_to_str(elt.self_class),
132159
superclasses=[self._libpath_to_str(cls) for cls in elt.superclasses]
133160
+ [self._libpath_to_str(cls) for cls in elt.super_superclasses],
134-
params={
135-
param_pair.name: self._param_to_compiled(context.path.append_param(param_pair.name), param_pair.value)
136-
for param_pair in elt.params
137-
},
138-
ports=dict(ports),
161+
doc=block_doc,
162+
params=params_dict,
163+
ports=ports_dict,
139164
blocks=dict(blocks),
140165
links=dict(links),
141166
)
@@ -179,7 +204,7 @@ def transform_port(
179204
ports=dict(ports),
180205
)
181206
elif isinstance(elt, edgir.PortArray):
182-
return CompiledPortArray(dict(ports))
207+
return CompiledPortArray(elts=dict(ports))
183208
else:
184209
raise ValueError(f"unknown port type {type(elt)}")
185210

@@ -219,8 +244,13 @@ def postprocess_serialized_json(json_str: str) -> str:
219244
json_str,
220245
)
221246
json_str = re.sub(
222-
r'\{\s*"type":\s*"(\S+)",\s*"value":\s*(.+)\s*\}',
223-
lambda m: rf'{{ "type": "{m.group(1)}", "value": {m.group(2)} }}',
247+
r'\{\s*("type":\s*"\S+"),\s*("value":\s*.+)\s*\}',
248+
lambda m: rf"{{ {m.group(1)}, {m.group(2)} }}",
249+
json_str,
250+
)
251+
json_str = re.sub(
252+
r'\{\s*("type":\s*"\S+"),\s*("value":\s*.+),\s*("doc":\s*.+)\s*\}',
253+
lambda m: rf"{{ {m.group(1)}, {m.group(2)}, {m.group(3)} }}",
224254
json_str,
225255
)
226256
return json_str

edg/core/test_block.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,14 @@ class TestBlockSecondNonLibrary(TestBlockSecondSub): # test that it can skip th
3131

3232

3333
class TestBlock(TestBlockBase, TestBlockSecondNonLibrary):
34+
"""Test docstring"""
35+
3436
def __init__(self) -> None:
3537
super().__init__()
36-
self.range_init = self.Parameter(RangeExpr((-4.2, -1.3)))
38+
self.range_init = self.Parameter(RangeExpr((-4.2, -1.3)), doc="range with initializer")
3739
self.array_init = self.Parameter(ArrayBoolExpr([False, True, False]))
3840
self.array_empty = self.Parameter(ArrayStringExpr([]))
39-
self.port_lit = self.Port(TestPortBase(117), optional=True)
41+
self.port_lit = self.Port(TestPortBase(117), optional=True, doc="port with initializer")
4042

4143

4244
class BlockBaseProtoTestCase(unittest.TestCase):
@@ -153,3 +155,10 @@ def test_param_init(self) -> None:
153155
expected_assign.assign.src.array.SetInParent()
154156
self.assertEqual(self.pb.constraints[5].name, "(init)array_empty")
155157
self.assertEqual(self.pb.constraints[5].value, expected_assign)
158+
159+
def test_docs(self) -> None:
160+
self.assertEqual(self.pb.meta.members.node["_docs"].members.node[""].text_leaf, "Test docstring")
161+
self.assertEqual(
162+
self.pb.meta.members.node["_docs"].members.node["range_init"].text_leaf, "range with initializer"
163+
)
164+
self.assertEqual(self.pb.meta.members.node["_docs"].members.node["port_lit"].text_leaf, "port with initializer")

edg/core/test_common.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,11 @@ class TestPortSink(TestPortBase):
3535

3636

3737
class TestBlockSink(Block):
38+
"""Sink block"""
39+
3840
def __init__(self) -> None:
3941
super().__init__()
40-
self.sink = self.Port(TestPortSink(), optional=True)
42+
self.sink = self.Port(TestPortSink(), optional=True, doc="Sink port")
4143

4244

4345
ImplicitSink = PortTag(TestPortSink)
@@ -50,9 +52,11 @@ def __init__(self) -> None:
5052

5153

5254
class TestBlockSource(Block):
55+
"""Source block"""
56+
5357
def __init__(self) -> None:
5458
super().__init__()
55-
self.source = self.Port(TestPortSource(), optional=True)
59+
self.source = self.Port(TestPortSource(), optional=True, doc="Source port")
5660

5761

5862
def problem_name_from_module_file(file: str) -> str:

edg/core/test_compiled_design_export.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,18 @@ def test_hierarchy(self) -> None:
1212
compiled = ScalaCompiler.compile(TopHierarchyBlock)
1313
result = CompiledDesignExportTransform(compiled).transform()
1414
self.assertEqual(result.blocks["source"].cls, "edg.core.test_common.TestBlockSource")
15+
self.assertEqual(result.blocks["source"].doc, "Source block")
1516
self.assertEqual(result.blocks["sink1"].cls, "edg.core.test_common.TestBlockSink")
17+
self.assertEqual(result.blocks["sink1"].doc, "Sink block")
1618
self.assertEqual(result.blocks["sink2"].cls, "edg.core.test_common.TestBlockSink")
19+
self.assertEqual(result.blocks["sink2"].doc, "Sink block")
1720
self.assertEqual(result.links["test_net"].cls, "edg.core.test_common.TestLink")
1821
self.assertEqual(cast(CompiledPort, result.blocks["source"].ports["source"]).connected_path, "test_net.source")
22+
self.assertEqual(cast(CompiledPort, result.blocks["source"].ports["source"]).doc, "Source port")
1923
self.assertEqual(cast(CompiledPort, result.blocks["sink1"].ports["sink"]).connected_path, "test_net.sinks.0")
24+
self.assertEqual(cast(CompiledPort, result.blocks["sink1"].ports["sink"]).doc, "Sink port")
2025
self.assertEqual(cast(CompiledPort, result.blocks["sink2"].ports["sink"]).connected_path, "test_net.sinks.1")
26+
self.assertEqual(cast(CompiledPort, result.blocks["sink2"].ports["sink"]).doc, "Sink port")
2127

2228
def test_param_values(self) -> None:
2329
from .test_simple_expr_eval import TestEvalReductionBlock

0 commit comments

Comments
 (0)