Skip to content

Commit bfd90bd

Browse files
authored
Properly order types from tables and python code blocks (#4625)
Currently, the pyspec build system forces a specific order where types defined using table syntax are always emitted before those defined in Python code blocks, even across forks. Properly ordering types defined in tables vs code blocks improves spec clarity as it enables `List` typealiases to be defined in tables that use an element type that is originally defined in a code block.
1 parent f7cd994 commit bfd90bd

File tree

5 files changed

+94
-59
lines changed

5 files changed

+94
-59
lines changed

pysetup/generate_specs.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
from pysetup.helpers import (
4444
combine_spec_objects,
4545
dependency_order_class_objects,
46+
finalized_spec_object,
4647
objects_to_spec,
4748
parse_config_vars,
4849
)
@@ -118,6 +119,7 @@ def build_spec(
118119
spec_object = all_specs[0]
119120
for value in all_specs[1:]:
120121
spec_object = combine_spec_objects(spec_object, value)
122+
spec_object = finalized_spec_object(spec_object)
121123

122124
class_objects = {**spec_object.ssz_objects, **spec_object.dataclasses}
123125

@@ -127,7 +129,7 @@ def build_spec(
127129
new_objects = copy.deepcopy(class_objects)
128130
dependency_order_class_objects(
129131
class_objects,
130-
spec_object.custom_types | spec_object.preset_dep_custom_types,
132+
spec_object.custom_types,
131133
)
132134

133135
return objects_to_spec(preset_name, spec_object, fork, class_objects)

pysetup/helpers.py

Lines changed: 60 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,19 @@ def collect_prev_forks(fork: str) -> list[str]:
2323

2424

2525
def requires_mypy_type_ignore(value: str) -> bool:
26-
return value.startswith("ByteVector") or (
27-
value.startswith("Vector") and any(k in value for k in ["ceillog2", "floorlog2"])
26+
return (
27+
value.startswith("Bitlist")
28+
or value.startswith("ByteVector")
29+
or (value.startswith("List") and not re.match(r"^List\[\w+,\s*\w+\]$", value))
30+
or (value.startswith("Vector") and any(k in value for k in ["ceillog2", "floorlog2"]))
31+
)
32+
33+
34+
def gen_new_type_definition(name: str, value: str) -> str:
35+
return (
36+
f"class {name}({value}):\n pass"
37+
if not requires_mypy_type_ignore(value)
38+
else f"class {name}(\n {value} # type: ignore\n):\n pass"
2839
)
2940

3041

@@ -41,19 +52,11 @@ def objects_to_spec(
4152
"""
4253

4354
def gen_new_type_definitions(custom_types: dict[str, str]) -> str:
44-
return "\n\n".join(
45-
[
46-
(
47-
f"class {key}({value}):\n pass\n"
48-
if not requires_mypy_type_ignore(value)
49-
else f"class {key}({value}): # type: ignore\n pass\n"
50-
)
51-
for key, value in custom_types.items()
52-
]
55+
return "\n\n\n".join(
56+
[gen_new_type_definition(key, value) for key, value in custom_types.items()]
5357
)
5458

5559
new_type_definitions = gen_new_type_definitions(spec_object.custom_types)
56-
preset_dep_new_type_definitions = gen_new_type_definitions(spec_object.preset_dep_custom_types)
5760

5861
# Collect builders with the reversed previous forks
5962
# e.g. `[bellatrix, altair, phase0]` -> `[phase0, altair, bellatrix]`
@@ -224,10 +227,9 @@ def format_constant(name: str, vardef: VariableDefinition) -> str:
224227
ssz_dep_constants,
225228
new_type_definitions,
226229
constant_vars_spec,
227-
# The presets that some SSZ types require. Need to be defined before `preset_dep_new_type_definitions`
230+
# The presets that some SSZ types require.
228231
preset_vars_spec,
229232
preset_dep_constant_vars_spec,
230-
preset_dep_new_type_definitions,
231233
config_spec,
232234
# Custom classes which are not required to be SSZ containers.
233235
classes,
@@ -267,8 +269,6 @@ def combine_dicts(old_dict: dict[str, T], new_dict: dict[str, T]) -> dict[str, T
267269
"bit",
268270
"Bitlist",
269271
"Bitvector",
270-
"BLSPubkey",
271-
"BLSSignature",
272272
"boolean",
273273
"byte",
274274
"ByteList",
@@ -292,6 +292,8 @@ def combine_dicts(old_dict: dict[str, T], new_dict: dict[str, T]) -> dict[str, T
292292
"floorlog2",
293293
"List",
294294
"Optional",
295+
"ProgressiveBitlist",
296+
"ProgressiveList",
295297
"Sequence",
296298
"Set",
297299
"Tuple",
@@ -312,10 +314,14 @@ def dependency_order_class_objects(objects: dict[str, str], custom_types: dict[s
312314
items = list(objects.items())
313315
for key, value in items:
314316
dependencies = []
315-
for line in value.split("\n"):
316-
if not re.match(r"\s+\w+: .+", line):
317+
for i, line in enumerate(value.split("\n")):
318+
if i == 0:
319+
match = re.match(r".+\((.+)\):", line)
320+
else:
321+
match = re.match(r"\s+\w+: (.+)", line)
322+
if not match:
317323
continue # skip whitespace etc.
318-
line = line[line.index(":") + 1 :] # strip of field name
324+
line = match.group(1)
319325
if "#" in line:
320326
line = line[: line.index("#")] # strip of comment
321327
dependencies.extend(
@@ -349,9 +355,6 @@ def combine_spec_objects(spec0: SpecObject, spec1: SpecObject) -> SpecObject:
349355
protocols = combine_protocols(spec0.protocols, spec1.protocols)
350356
functions = combine_dicts(spec0.functions, spec1.functions)
351357
custom_types = combine_dicts(spec0.custom_types, spec1.custom_types)
352-
preset_dep_custom_types = combine_dicts(
353-
spec0.preset_dep_custom_types, spec1.preset_dep_custom_types
354-
)
355358
constant_vars = combine_dicts(spec0.constant_vars, spec1.constant_vars)
356359
preset_dep_constant_vars = combine_dicts(
357360
spec0.preset_dep_constant_vars, spec1.preset_dep_constant_vars
@@ -366,7 +369,6 @@ def combine_spec_objects(spec0: SpecObject, spec1: SpecObject) -> SpecObject:
366369
functions=functions,
367370
protocols=protocols,
368371
custom_types=custom_types,
369-
preset_dep_custom_types=preset_dep_custom_types,
370372
constant_vars=constant_vars,
371373
preset_dep_constant_vars=preset_dep_constant_vars,
372374
preset_vars=preset_vars,
@@ -378,6 +380,41 @@ def combine_spec_objects(spec0: SpecObject, spec1: SpecObject) -> SpecObject:
378380
)
379381

380382

383+
def finalized_spec_object(spec_object: SpecObject) -> SpecObject:
384+
all_config_dependencies = {
385+
vardef.type_name or vardef.type_hint
386+
for vardef in (
387+
spec_object.constant_vars
388+
| spec_object.preset_dep_constant_vars
389+
| spec_object.preset_vars
390+
| spec_object.config_vars
391+
).values()
392+
if (vardef.type_name or vardef.type_hint) is not None
393+
}
394+
395+
custom_types = {}
396+
ssz_objects = spec_object.ssz_objects
397+
for name, value in spec_object.custom_types.items():
398+
if any(k in name for k in all_config_dependencies):
399+
custom_types[name] = value
400+
else:
401+
ssz_objects[name] = gen_new_type_definition(name, value)
402+
403+
return SpecObject(
404+
functions=spec_object.functions,
405+
protocols=spec_object.protocols,
406+
custom_types=custom_types,
407+
constant_vars=spec_object.constant_vars,
408+
preset_dep_constant_vars=spec_object.preset_dep_constant_vars,
409+
preset_vars=spec_object.preset_vars,
410+
config_vars=spec_object.config_vars,
411+
ssz_dep_constants=spec_object.ssz_dep_constants,
412+
func_dep_presets=spec_object.func_dep_presets,
413+
ssz_objects=ssz_objects,
414+
dataclasses=spec_object.dataclasses,
415+
)
416+
417+
381418
def parse_config_vars(conf: dict[str, str]) -> dict[str, str | list[dict[str, str]]]:
382419
"""
383420
Parses a dict of basic str/int/list types into a dict for insertion into the spec code.

pysetup/md_to_spec.py

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ def __init__(
3232
self.preset_name = preset_name
3333

3434
self.document_iterator: Iterator[Element] = self._parse_document(file_name)
35-
self.all_custom_types: dict[str, str] = {}
3635
self.current_heading_name: str | None = None
3736

3837
# Use a single dict to hold all SpecObject fields
@@ -44,7 +43,6 @@ def __init__(
4443
"func_dep_presets": {},
4544
"functions": {},
4645
"preset_dep_constant_vars": {},
47-
"preset_dep_custom_types": {},
4846
"preset_vars": {},
4947
"protocols": {},
5048
"ssz_dep_constants": {},
@@ -180,8 +178,12 @@ def _process_code_class(self, source: str, cls: ast.ClassDef) -> None:
180178
if class_name != self.current_heading_name:
181179
raise Exception(f"class_name {class_name} != current_name {self.current_heading_name}")
182180

183-
if parent_class:
184-
assert parent_class == "Container"
181+
if parent_class == "ProgressiveContainer":
182+
source = re.sub(
183+
r"^(.*ProgressiveContainer.*)$", r"\1 # type: ignore", source, flags=re.MULTILINE
184+
)
185+
else:
186+
assert parent_class is None or parent_class == "Container"
185187
self.spec["ssz_objects"][class_name] = source
186188

187189
def _process_table(self, table: Table) -> None:
@@ -202,9 +204,21 @@ def _process_table(self, table: Table) -> None:
202204
if not _is_constant_id(name):
203205
# Check for short type declarations
204206
if value.startswith(
205-
("uint", "Bytes", "ByteList", "Union", "Vector", "List", "ByteVector")
207+
(
208+
"uint",
209+
"Bitlist",
210+
"Bitvector",
211+
"ByteList",
212+
"ByteVector",
213+
"Bytes",
214+
"List",
215+
"ProgressiveBitlist",
216+
"ProgressiveList",
217+
"Union",
218+
"Vector",
219+
)
206220
):
207-
self.all_custom_types[name] = value
221+
self.spec["custom_types"][name] = value
208222
continue
209223

210224
# It is a constant name and a generalized index
@@ -418,7 +432,6 @@ def _process_html_block(self, html: HTMLBlock) -> None:
418432

419433
def _finalize_types(self) -> None:
420434
"""
421-
Processes all_custom_types into custom_types and preset_dep_custom_types.
422435
Calls helper functions to update KZG and CURDLEPROOFS setups if needed.
423436
"""
424437
# Update KZG trusted setup if needed
@@ -433,17 +446,6 @@ def _finalize_types(self) -> None:
433446
self.spec["constant_vars"], self.preset_name
434447
)
435448

436-
# Split all_custom_types into custom_types and preset_dep_custom_types
437-
self.spec["custom_types"] = {}
438-
self.spec["preset_dep_custom_types"] = {}
439-
for name, value in self.all_custom_types.items():
440-
if any(k in value for k in self.preset) or any(
441-
k in value for k in self.spec["preset_dep_constant_vars"]
442-
):
443-
self.spec["preset_dep_custom_types"][name] = value
444-
else:
445-
self.spec["custom_types"][name] = value
446-
447449
def _build_spec_object(self) -> SpecObject:
448450
"""
449451
Returns the SpecObject using all collected data.
@@ -456,7 +458,6 @@ def _build_spec_object(self) -> SpecObject:
456458
func_dep_presets=self.spec["func_dep_presets"],
457459
functions=self.spec["functions"],
458460
preset_dep_constant_vars=self.spec["preset_dep_constant_vars"],
459-
preset_dep_custom_types=self.spec["preset_dep_custom_types"],
460461
preset_vars=self.spec["preset_vars"],
461462
protocols=self.spec["protocols"],
462463
ssz_dep_constants=self.spec["ssz_dep_constants"],
@@ -496,6 +497,8 @@ def _get_class_info_from_ast(cls: ast.ClassDef) -> tuple[str, str | None]:
496497
parent_class = base.id
497498
elif isinstance(base, ast.Subscript):
498499
parent_class = base.value.id
500+
elif isinstance(base, ast.Call):
501+
parent_class = base.func.id
499502
else:
500503
# NOTE: SSZ definition derives from earlier phase...
501504
# e.g. `phase0.SignedBeaconBlock`

pysetup/typing.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ class SpecObject(NamedTuple):
1818
functions: dict[str, str]
1919
protocols: dict[str, ProtocolDefinition]
2020
custom_types: dict[str, str]
21-
preset_dep_custom_types: dict[str, str] # the types that depend on presets
2221
constant_vars: dict[str, VariableDefinition]
2322
preset_dep_constant_vars: dict[str, VariableDefinition]
2423
preset_vars: dict[str, VariableDefinition]

tests/infra/test_md_to_spec.py

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import pytest
44

5+
from pysetup.helpers import finalized_spec_object
56
from pysetup.md_to_spec import MarkdownToSpec
67
from pysetup.typing import SpecObject
78

@@ -35,7 +36,6 @@ def test_constructor_initializes_fields(dummy_file, dummy_preset, dummy_config):
3536
assert m2s.config == dummy_config
3637
assert m2s.preset_name == preset_name
3738
assert isinstance(m2s.spec, dict)
38-
assert isinstance(m2s.all_custom_types, dict)
3939
assert hasattr(m2s, "document_iterator")
4040
assert m2s.current_heading_name is None
4141

@@ -313,7 +313,7 @@ class PayloadAttributes(object):
313313
assert "PayloadAttributes" not in spec_obj.dataclasses
314314

315315

316-
def test_finalize_types_called_and_updates_custom_types(
316+
def test_finalized_spec_object_updates_custom_types(
317317
tmp_path, dummy_preset, dummy_config, monkeypatch
318318
):
319319
# Minimal markdown with a type definition
@@ -324,6 +324,11 @@ def test_finalize_types_called_and_updates_custom_types(
324324
| ---------------- | -------------- | --------------------------------- |
325325
| `Slot` | `uint64` | a slot number |
326326
| `Epoch` | `uint64` | an epoch number |
327+
328+
| Name | Value |
329+
| --------------- | ---------- |
330+
| `GENESIS_SLOT` | `Slot(0)` |
331+
| `GENESIS_EPOCH` | `Epoch(0)` |
327332
"""
328333
file = tmp_path / "types.md"
329334
file.write_text(md_content)
@@ -334,18 +339,7 @@ def test_finalize_types_called_and_updates_custom_types(
334339
preset_name="mainnet",
335340
)
336341

337-
# Spy on _finalize_types
338-
called = {}
339-
orig_finalize_types = m2s._finalize_types
340-
341-
def spy_finalize_types():
342-
called["ran"] = True
343-
return orig_finalize_types()
344-
345-
monkeypatch.setattr(m2s, "_finalize_types", spy_finalize_types)
346-
347-
spec_obj = m2s.run()
348-
assert called.get("ran") is True
349-
# After _finalize_types, custom_types should include 'Slot' and 'Epoch'
342+
spec_obj = finalized_spec_object(m2s.run())
343+
# After finalized_spec_object, custom_types should include 'Slot' and 'Epoch'
350344
assert spec_obj.custom_types["Slot"] == "uint64"
351345
assert spec_obj.custom_types["Epoch"] == "uint64"

0 commit comments

Comments
 (0)