ethspecify 0.2.4__py3-none-any.whl → 0.3.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ethspecify/cli.py +103 -20
- ethspecify/core.py +1073 -25
- ethspecify-0.3.2.dist-info/METADATA +237 -0
- ethspecify-0.3.2.dist-info/RECORD +9 -0
- ethspecify-0.2.4.dist-info/METADATA +0 -351
- ethspecify-0.2.4.dist-info/RECORD +0 -9
- {ethspecify-0.2.4.dist-info → ethspecify-0.3.2.dist-info}/WHEEL +0 -0
- {ethspecify-0.2.4.dist-info → ethspecify-0.3.2.dist-info}/entry_points.txt +0 -0
- {ethspecify-0.2.4.dist-info → ethspecify-0.3.2.dist-info}/licenses/LICENSE +0 -0
- {ethspecify-0.2.4.dist-info → ethspecify-0.3.2.dist-info}/top_level.txt +0 -0
ethspecify/core.py
CHANGED
|
@@ -11,6 +11,91 @@ import tokenize
|
|
|
11
11
|
import yaml
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
def validate_exception_items(exceptions, version):
|
|
15
|
+
"""
|
|
16
|
+
Validate that exception items actually exist in the spec.
|
|
17
|
+
Raises an exception if any item doesn't exist.
|
|
18
|
+
"""
|
|
19
|
+
if not exceptions:
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
# Get the pyspec data
|
|
23
|
+
try:
|
|
24
|
+
pyspec = get_pyspec(version)
|
|
25
|
+
except Exception as e:
|
|
26
|
+
print(f"Warning: Could not validate exceptions - failed to load pyspec: {e}")
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
# Map exception keys to pyspec keys
|
|
30
|
+
exception_to_pyspec_map = {
|
|
31
|
+
'functions': 'functions',
|
|
32
|
+
'fn': 'functions',
|
|
33
|
+
'constants': 'constant_vars',
|
|
34
|
+
'constant_variables': 'constant_vars',
|
|
35
|
+
'constant_var': 'constant_vars',
|
|
36
|
+
'configs': 'config_vars',
|
|
37
|
+
'config_variables': 'config_vars',
|
|
38
|
+
'config_var': 'config_vars',
|
|
39
|
+
'presets': 'preset_vars',
|
|
40
|
+
'preset_variables': 'preset_vars',
|
|
41
|
+
'preset_var': 'preset_vars',
|
|
42
|
+
'ssz_objects': 'ssz_objects',
|
|
43
|
+
'containers': 'ssz_objects',
|
|
44
|
+
'container': 'ssz_objects',
|
|
45
|
+
'dataclasses': 'dataclasses',
|
|
46
|
+
'dataclass': 'dataclasses',
|
|
47
|
+
'custom_types': 'custom_types',
|
|
48
|
+
'custom_type': 'custom_types'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
errors = []
|
|
52
|
+
|
|
53
|
+
for exception_key, exception_items in exceptions.items():
|
|
54
|
+
# Get the corresponding pyspec key
|
|
55
|
+
pyspec_key = exception_to_pyspec_map.get(exception_key)
|
|
56
|
+
if not pyspec_key:
|
|
57
|
+
errors.append(f"Unknown exception type: '{exception_key}'")
|
|
58
|
+
continue
|
|
59
|
+
|
|
60
|
+
# Ensure exception_items is a list
|
|
61
|
+
if not isinstance(exception_items, list):
|
|
62
|
+
exception_items = [exception_items]
|
|
63
|
+
|
|
64
|
+
for item in exception_items:
|
|
65
|
+
# Parse item#fork format
|
|
66
|
+
if '#' in item:
|
|
67
|
+
item_name, fork = item.split('#', 1)
|
|
68
|
+
else:
|
|
69
|
+
# If no fork specified, we'll check if it exists in any fork
|
|
70
|
+
item_name = item
|
|
71
|
+
fork = None
|
|
72
|
+
|
|
73
|
+
# Check if the item exists
|
|
74
|
+
item_found = False
|
|
75
|
+
if fork:
|
|
76
|
+
# Check specific fork
|
|
77
|
+
if ('mainnet' in pyspec and
|
|
78
|
+
fork in pyspec['mainnet'] and
|
|
79
|
+
pyspec_key in pyspec['mainnet'][fork] and
|
|
80
|
+
item_name in pyspec['mainnet'][fork][pyspec_key]):
|
|
81
|
+
item_found = True
|
|
82
|
+
else:
|
|
83
|
+
# Check if item exists in any fork
|
|
84
|
+
if 'mainnet' in pyspec:
|
|
85
|
+
for check_fork in pyspec['mainnet']:
|
|
86
|
+
if (pyspec_key in pyspec['mainnet'][check_fork] and
|
|
87
|
+
item_name in pyspec['mainnet'][check_fork][pyspec_key]):
|
|
88
|
+
item_found = True
|
|
89
|
+
break
|
|
90
|
+
|
|
91
|
+
if not item_found:
|
|
92
|
+
fork_suffix = f"#{fork}" if fork else ""
|
|
93
|
+
errors.append(f"invalid key: {exception_key}.{item_name}{fork_suffix}")
|
|
94
|
+
|
|
95
|
+
if errors:
|
|
96
|
+
error_msg = "Invalid exception items in configuration:\n" + "\n".join(f" - {e}" for e in errors)
|
|
97
|
+
raise Exception(error_msg)
|
|
98
|
+
|
|
14
99
|
def load_config(directory=None):
|
|
15
100
|
"""
|
|
16
101
|
Load configuration from .ethspecify.yml file in the specified directory.
|
|
@@ -25,7 +110,22 @@ def load_config(directory=None):
|
|
|
25
110
|
try:
|
|
26
111
|
with open(config_path, 'r') as f:
|
|
27
112
|
config = yaml.safe_load(f)
|
|
28
|
-
|
|
113
|
+
if not config:
|
|
114
|
+
return {}
|
|
115
|
+
|
|
116
|
+
# Get version from config, default to 'nightly'
|
|
117
|
+
version = config.get('version', 'nightly')
|
|
118
|
+
|
|
119
|
+
# Validate exceptions in root config
|
|
120
|
+
if 'exceptions' in config:
|
|
121
|
+
validate_exception_items(config['exceptions'], version)
|
|
122
|
+
|
|
123
|
+
# Also validate exceptions in specrefs section if present
|
|
124
|
+
if 'specrefs' in config and isinstance(config['specrefs'], dict):
|
|
125
|
+
if 'exceptions' in config['specrefs']:
|
|
126
|
+
validate_exception_items(config['specrefs']['exceptions'], version)
|
|
127
|
+
|
|
128
|
+
return config
|
|
29
129
|
except (yaml.YAMLError, IOError) as e:
|
|
30
130
|
print(f"Warning: Error reading .ethspecify.yml file: {e}")
|
|
31
131
|
return {}
|
|
@@ -232,10 +332,16 @@ def get_spec(attributes, preset, fork, version="nightly"):
|
|
|
232
332
|
+ " = "
|
|
233
333
|
+ pyspec[preset][fork]["custom_types"][attributes["custom_type"]]
|
|
234
334
|
)
|
|
235
|
-
elif "ssz_object" in attributes:
|
|
335
|
+
elif "ssz_object" in attributes or "container" in attributes:
|
|
236
336
|
if spec is not None:
|
|
237
337
|
raise Exception(f"Tag can only specify one spec item")
|
|
238
|
-
|
|
338
|
+
if "ssz_object" in attributes and "container" in attributes:
|
|
339
|
+
raise Exception(f"cannot contain 'ssz_object' and 'container'")
|
|
340
|
+
if "ssz_object" in attributes:
|
|
341
|
+
object_name = attributes["ssz_object"]
|
|
342
|
+
else:
|
|
343
|
+
object_name = attributes["container"]
|
|
344
|
+
spec = pyspec[preset][fork]["ssz_objects"][object_name]
|
|
239
345
|
elif "dataclass" in attributes:
|
|
240
346
|
if spec is not None:
|
|
241
347
|
raise Exception(f"Tag can only specify one spec item")
|
|
@@ -471,6 +577,140 @@ def extract_attributes(tag):
|
|
|
471
577
|
return dict(attr_pattern.findall(tag))
|
|
472
578
|
|
|
473
579
|
|
|
580
|
+
def sort_specref_yaml(yaml_file):
|
|
581
|
+
"""
|
|
582
|
+
Sort specref entries in a YAML file by their name field.
|
|
583
|
+
Preserves formatting and adds single blank lines between entries.
|
|
584
|
+
"""
|
|
585
|
+
if not os.path.exists(yaml_file):
|
|
586
|
+
return False
|
|
587
|
+
|
|
588
|
+
try:
|
|
589
|
+
with open(yaml_file, 'r') as f:
|
|
590
|
+
content_str = f.read()
|
|
591
|
+
|
|
592
|
+
# Try to fix common YAML issues with unquoted search strings containing colons
|
|
593
|
+
# This is the same fix used in check_source_files
|
|
594
|
+
content_str = re.sub(r'(\s+search:\s+)([^"\n]+:)(\s*$)', r'\1"\2"\3', content_str, flags=re.MULTILINE)
|
|
595
|
+
|
|
596
|
+
try:
|
|
597
|
+
content = yaml.safe_load(content_str)
|
|
598
|
+
except yaml.YAMLError:
|
|
599
|
+
# Fall back to FullLoader if safe_load fails
|
|
600
|
+
content = yaml.load(content_str, Loader=yaml.FullLoader)
|
|
601
|
+
|
|
602
|
+
if not content:
|
|
603
|
+
return False
|
|
604
|
+
|
|
605
|
+
# Handle both array of objects and single object formats
|
|
606
|
+
if isinstance(content, list):
|
|
607
|
+
# Sort the list by 'name' field if it exists
|
|
608
|
+
# Special handling for fork ordering within the same item
|
|
609
|
+
def sort_key(x):
|
|
610
|
+
name = x.get('name', '') if isinstance(x, dict) else str(x)
|
|
611
|
+
|
|
612
|
+
# Known fork names for ordering
|
|
613
|
+
forks = ['phase0', 'altair', 'bellatrix', 'capella', 'deneb', 'electra']
|
|
614
|
+
|
|
615
|
+
# Check if name contains a # separator (like "slash_validator#phase0")
|
|
616
|
+
if '#' in name:
|
|
617
|
+
base_name, fork = name.rsplit('#', 1)
|
|
618
|
+
# Define fork order based on known forks list
|
|
619
|
+
fork_lower = fork.lower()
|
|
620
|
+
if fork_lower in forks:
|
|
621
|
+
fork_order = forks.index(fork_lower)
|
|
622
|
+
else:
|
|
623
|
+
# Unknown forks go after known ones, sorted alphabetically
|
|
624
|
+
fork_order = len(forks)
|
|
625
|
+
return (base_name, fork_order, fork)
|
|
626
|
+
else:
|
|
627
|
+
# Check if name ends with a fork name (like "BeaconStatePhase0")
|
|
628
|
+
name_lower = name.lower()
|
|
629
|
+
for i, fork in enumerate(forks):
|
|
630
|
+
if name_lower.endswith(fork):
|
|
631
|
+
# Extract base name
|
|
632
|
+
base_name = name[:-len(fork)]
|
|
633
|
+
return (base_name, i, name)
|
|
634
|
+
|
|
635
|
+
# No fork pattern found, just sort by name
|
|
636
|
+
return (name, 0, '')
|
|
637
|
+
|
|
638
|
+
sorted_content = sorted(content, key=sort_key)
|
|
639
|
+
|
|
640
|
+
# Custom YAML writing to preserve formatting
|
|
641
|
+
output_lines = []
|
|
642
|
+
for i, item in enumerate(sorted_content):
|
|
643
|
+
if i > 0:
|
|
644
|
+
# Add a single blank line between entries
|
|
645
|
+
output_lines.append("")
|
|
646
|
+
|
|
647
|
+
# Start each entry with a dash
|
|
648
|
+
first_line = True
|
|
649
|
+
for key, value in item.items():
|
|
650
|
+
if first_line:
|
|
651
|
+
prefix = "- "
|
|
652
|
+
first_line = False
|
|
653
|
+
else:
|
|
654
|
+
prefix = " "
|
|
655
|
+
|
|
656
|
+
if key == 'spec':
|
|
657
|
+
# Preserve spec content as-is, using literal style
|
|
658
|
+
output_lines.append(f"{prefix}{key}: |")
|
|
659
|
+
# Indent the spec content - preserve it exactly as-is
|
|
660
|
+
spec_lines = value.rstrip().split('\n') if isinstance(value, str) else str(value).rstrip().split('\n')
|
|
661
|
+
for spec_line in spec_lines:
|
|
662
|
+
output_lines.append(f" {spec_line}")
|
|
663
|
+
elif key == 'sources':
|
|
664
|
+
if isinstance(value, list) and len(value) == 0:
|
|
665
|
+
# Keep empty lists on the same line for clarity
|
|
666
|
+
output_lines.append(f"{prefix}{key}: []")
|
|
667
|
+
else:
|
|
668
|
+
output_lines.append(f"{prefix}{key}:")
|
|
669
|
+
if isinstance(value, list):
|
|
670
|
+
for source in value:
|
|
671
|
+
if isinstance(source, dict):
|
|
672
|
+
output_lines.append(f" - file: {source.get('file', '')}")
|
|
673
|
+
if 'search' in source:
|
|
674
|
+
search_val = source['search']
|
|
675
|
+
# Only quote if:
|
|
676
|
+
# 1. Contains a colon followed by space or end of string (YAML mapping issue)
|
|
677
|
+
# 2. Not already quoted
|
|
678
|
+
# 3. Doesn't contain internal quotes (like regex patterns)
|
|
679
|
+
if ((':' in search_val or search_val.endswith(':')) and
|
|
680
|
+
not (search_val.startswith('"') and search_val.endswith('"')) and
|
|
681
|
+
'"' not in search_val):
|
|
682
|
+
# Only quote simple strings with colons, not complex patterns
|
|
683
|
+
if not any(char in search_val for char in ['\\', '*', '|', '[', ']', '(', ')']):
|
|
684
|
+
search_val = f'"{search_val}"'
|
|
685
|
+
output_lines.append(f" search: {search_val}")
|
|
686
|
+
if 'regex' in source:
|
|
687
|
+
# Keep boolean values lowercase for consistency
|
|
688
|
+
regex_val = str(source['regex']).lower()
|
|
689
|
+
output_lines.append(f" regex: {regex_val}")
|
|
690
|
+
else:
|
|
691
|
+
output_lines.append(f" - {source}")
|
|
692
|
+
else:
|
|
693
|
+
# Handle other fields - don't escape or modify the value
|
|
694
|
+
output_lines.append(f"{prefix}{key}: {value}")
|
|
695
|
+
|
|
696
|
+
# Strip trailing whitespace from all lines
|
|
697
|
+
output_lines = [line.rstrip() for line in output_lines]
|
|
698
|
+
|
|
699
|
+
# Write back the sorted content
|
|
700
|
+
with open(yaml_file, 'w') as f:
|
|
701
|
+
f.write('\n'.join(output_lines))
|
|
702
|
+
f.write('\n') # End file with newline
|
|
703
|
+
|
|
704
|
+
return True
|
|
705
|
+
elif isinstance(content, dict):
|
|
706
|
+
# If it's a single dict, we can't sort it
|
|
707
|
+
return False
|
|
708
|
+
except (yaml.YAMLError, IOError) as e:
|
|
709
|
+
print(f"Error sorting {yaml_file}: {e}")
|
|
710
|
+
return False
|
|
711
|
+
|
|
712
|
+
return False
|
|
713
|
+
|
|
474
714
|
def replace_spec_tags(file_path, config=None):
|
|
475
715
|
with open(file_path, 'r') as file:
|
|
476
716
|
content = file.read()
|
|
@@ -485,6 +725,9 @@ def replace_spec_tags(file_path, config=None):
|
|
|
485
725
|
re.DOTALL
|
|
486
726
|
)
|
|
487
727
|
|
|
728
|
+
# Collect processed spec items for potential YAML updates
|
|
729
|
+
processed_items = []
|
|
730
|
+
|
|
488
731
|
def rebuild_opening_tag(attributes, hash_value):
|
|
489
732
|
# Rebuild a fresh opening tag from attributes, overriding any existing hash.
|
|
490
733
|
new_opening = "<spec"
|
|
@@ -523,6 +766,17 @@ def replace_spec_tags(file_path, config=None):
|
|
|
523
766
|
spec = get_spec(attributes, preset, fork, version)
|
|
524
767
|
hash_value = hashlib.sha256(spec.encode('utf-8')).hexdigest()[:8]
|
|
525
768
|
|
|
769
|
+
# Collect this item for potential YAML updates
|
|
770
|
+
processed_items.append({
|
|
771
|
+
'attributes': attributes,
|
|
772
|
+
'preset': preset,
|
|
773
|
+
'fork': fork,
|
|
774
|
+
'style': style,
|
|
775
|
+
'version': version,
|
|
776
|
+
'spec': spec,
|
|
777
|
+
'hash': hash_value
|
|
778
|
+
})
|
|
779
|
+
|
|
526
780
|
if style == "hash":
|
|
527
781
|
# Rebuild a fresh self-closing tag.
|
|
528
782
|
updated_tag = rebuild_self_closing_tag(attributes, hash_value)
|
|
@@ -534,7 +788,7 @@ def replace_spec_tags(file_path, config=None):
|
|
|
534
788
|
prefix = content[:match.start()].splitlines()[-1]
|
|
535
789
|
prefixed_spec = "\n".join(
|
|
536
790
|
f"{prefix}{line}" if line.rstrip() else prefix.rstrip()
|
|
537
|
-
for line in spec_content.
|
|
791
|
+
for line in spec_content.split("\n")
|
|
538
792
|
)
|
|
539
793
|
updated_tag = f"{new_opening}\n{prefixed_spec}\n{prefix}</spec>"
|
|
540
794
|
return updated_tag
|
|
@@ -547,6 +801,151 @@ def replace_spec_tags(file_path, config=None):
|
|
|
547
801
|
with open(file_path, 'w') as file:
|
|
548
802
|
file.write(updated_content)
|
|
549
803
|
|
|
804
|
+
# Return processed items for potential YAML updates
|
|
805
|
+
return processed_items
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
def get_yaml_filename_for_spec_attr(spec_attr):
|
|
809
|
+
"""Map spec attribute to YAML filename."""
|
|
810
|
+
attr_to_file = {
|
|
811
|
+
'fn': 'functions.yml',
|
|
812
|
+
'function': 'functions.yml',
|
|
813
|
+
'constant_var': 'constants.yml',
|
|
814
|
+
'config_var': 'configs.yml',
|
|
815
|
+
'preset_var': 'presets.yml',
|
|
816
|
+
'container': 'containers.yml',
|
|
817
|
+
'ssz_object': 'containers.yml',
|
|
818
|
+
'dataclass': 'dataclasses.yml',
|
|
819
|
+
'custom_type': 'types.yml',
|
|
820
|
+
}
|
|
821
|
+
return attr_to_file.get(spec_attr)
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
def get_spec_attr_and_name(attributes):
|
|
825
|
+
"""Extract the spec attribute and item name from tag attributes."""
|
|
826
|
+
spec_attrs = ['fn', 'function', 'constant_var', 'config_var', 'preset_var',
|
|
827
|
+
'container', 'ssz_object', 'dataclass', 'custom_type']
|
|
828
|
+
for attr in spec_attrs:
|
|
829
|
+
if attr in attributes:
|
|
830
|
+
return attr, attributes[attr]
|
|
831
|
+
return None, None
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
def load_yaml_entries(yaml_file):
|
|
835
|
+
"""Load existing entries from a YAML file."""
|
|
836
|
+
if not os.path.exists(yaml_file):
|
|
837
|
+
return []
|
|
838
|
+
|
|
839
|
+
try:
|
|
840
|
+
with open(yaml_file, 'r') as f:
|
|
841
|
+
content_str = f.read()
|
|
842
|
+
|
|
843
|
+
# Try to fix common YAML issues with unquoted search strings containing colons
|
|
844
|
+
content_str = re.sub(r'(\s+search:\s+)([^"\n]+:)(\s*$)', r'\1"\2"\3', content_str, flags=re.MULTILINE)
|
|
845
|
+
|
|
846
|
+
try:
|
|
847
|
+
content = yaml.safe_load(content_str)
|
|
848
|
+
except yaml.YAMLError:
|
|
849
|
+
content = yaml.load(content_str, Loader=yaml.FullLoader)
|
|
850
|
+
|
|
851
|
+
if isinstance(content, list):
|
|
852
|
+
return content
|
|
853
|
+
return []
|
|
854
|
+
except (yaml.YAMLError, IOError):
|
|
855
|
+
return []
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
def extract_spec_tag_key(spec_content):
|
|
859
|
+
"""Extract a unique key from spec tag to identify duplicates."""
|
|
860
|
+
if not spec_content:
|
|
861
|
+
return None
|
|
862
|
+
|
|
863
|
+
# Extract the opening spec tag
|
|
864
|
+
match = re.search(r'<spec\b([^>]*)>', spec_content)
|
|
865
|
+
if not match:
|
|
866
|
+
return None
|
|
867
|
+
|
|
868
|
+
# Extract attributes from the tag
|
|
869
|
+
attributes = extract_attributes(match.group(0))
|
|
870
|
+
|
|
871
|
+
# Build a key from the spec attribute and fork
|
|
872
|
+
# e.g., "constant_var:DOMAIN_PTC_ATTESTER:fork:gloas"
|
|
873
|
+
key_parts = []
|
|
874
|
+
for attr in ['fn', 'function', 'constant_var', 'config_var', 'preset_var',
|
|
875
|
+
'container', 'ssz_object', 'dataclass', 'custom_type']:
|
|
876
|
+
if attr in attributes:
|
|
877
|
+
key_parts.append(f"{attr}:{attributes[attr]}")
|
|
878
|
+
break
|
|
879
|
+
|
|
880
|
+
if 'fork' in attributes:
|
|
881
|
+
key_parts.append(f"fork:{attributes['fork']}")
|
|
882
|
+
|
|
883
|
+
return ':'.join(key_parts) if key_parts else None
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
def add_missing_entries_to_yaml(yaml_file, new_entries):
|
|
887
|
+
"""Add new entries to a YAML file and sort it."""
|
|
888
|
+
if not new_entries:
|
|
889
|
+
return
|
|
890
|
+
|
|
891
|
+
# Load existing entries
|
|
892
|
+
existing_entries = load_yaml_entries(yaml_file)
|
|
893
|
+
|
|
894
|
+
# Build a set of existing spec tag keys
|
|
895
|
+
existing_spec_keys = set()
|
|
896
|
+
for entry in existing_entries:
|
|
897
|
+
if isinstance(entry, dict) and 'spec' in entry:
|
|
898
|
+
spec_key = extract_spec_tag_key(entry['spec'])
|
|
899
|
+
if spec_key:
|
|
900
|
+
existing_spec_keys.add(spec_key)
|
|
901
|
+
|
|
902
|
+
# Filter out entries that already exist (based on spec tag, not name)
|
|
903
|
+
entries_to_add = []
|
|
904
|
+
for entry in new_entries:
|
|
905
|
+
spec_key = extract_spec_tag_key(entry.get('spec', ''))
|
|
906
|
+
if spec_key and spec_key not in existing_spec_keys:
|
|
907
|
+
entries_to_add.append(entry)
|
|
908
|
+
existing_spec_keys.add(spec_key) # Avoid duplicates within new entries
|
|
909
|
+
|
|
910
|
+
if not entries_to_add:
|
|
911
|
+
return
|
|
912
|
+
|
|
913
|
+
# Combine and write
|
|
914
|
+
all_entries = existing_entries + entries_to_add
|
|
915
|
+
|
|
916
|
+
# Ensure directory exists
|
|
917
|
+
os.makedirs(os.path.dirname(yaml_file) if os.path.dirname(yaml_file) else '.', exist_ok=True)
|
|
918
|
+
|
|
919
|
+
# Write combined entries using the same format as generate_specref_files
|
|
920
|
+
with open(yaml_file, 'w') as f:
|
|
921
|
+
for i, entry in enumerate(all_entries):
|
|
922
|
+
if i > 0:
|
|
923
|
+
f.write('\n')
|
|
924
|
+
f.write(f'- name: {entry["name"]}\n')
|
|
925
|
+
if 'sources' in entry:
|
|
926
|
+
if isinstance(entry['sources'], list) and len(entry['sources']) == 0:
|
|
927
|
+
f.write(' sources: []\n')
|
|
928
|
+
else:
|
|
929
|
+
f.write(' sources:\n')
|
|
930
|
+
for source in entry['sources']:
|
|
931
|
+
if isinstance(source, dict):
|
|
932
|
+
f.write(f' - file: {source.get("file", "")}\n')
|
|
933
|
+
if 'search' in source:
|
|
934
|
+
f.write(f' search: {source["search"]}\n')
|
|
935
|
+
if 'regex' in source:
|
|
936
|
+
f.write(f' regex: {source["regex"]}\n')
|
|
937
|
+
else:
|
|
938
|
+
f.write(f' - {source}\n')
|
|
939
|
+
if 'spec' in entry:
|
|
940
|
+
f.write(' spec: |\n')
|
|
941
|
+
for line in entry['spec'].split('\n'):
|
|
942
|
+
f.write(f' {line}\n')
|
|
943
|
+
|
|
944
|
+
# Sort the file
|
|
945
|
+
sort_specref_yaml(yaml_file)
|
|
946
|
+
|
|
947
|
+
print(f"Added {len(entries_to_add)} new entries to {yaml_file}")
|
|
948
|
+
|
|
550
949
|
|
|
551
950
|
def check_source_files(yaml_file, project_root, exceptions=None):
|
|
552
951
|
"""
|
|
@@ -614,6 +1013,7 @@ def check_source_files(yaml_file, project_root, exceptions=None):
|
|
|
614
1013
|
'config_var': 'configs',
|
|
615
1014
|
'preset_var': 'presets',
|
|
616
1015
|
'ssz_object': 'ssz_objects',
|
|
1016
|
+
'container': 'ssz_objects',
|
|
617
1017
|
'dataclass': 'dataclasses',
|
|
618
1018
|
'custom_type': 'custom_types'
|
|
619
1019
|
}
|
|
@@ -625,19 +1025,23 @@ def check_source_files(yaml_file, project_root, exceptions=None):
|
|
|
625
1025
|
if not spec_ref and 'name' in item:
|
|
626
1026
|
spec_ref = item['name']
|
|
627
1027
|
|
|
1028
|
+
# Extract item name and fork for exception checking
|
|
1029
|
+
item_name_for_exception = None
|
|
1030
|
+
fork_for_exception = None
|
|
1031
|
+
if spec_ref and '#' in spec_ref and '.' in spec_ref:
|
|
1032
|
+
# Format: "functions.item_name#fork"
|
|
1033
|
+
_, item_with_fork = spec_ref.split('.', 1)
|
|
1034
|
+
if '#' in item_with_fork:
|
|
1035
|
+
item_name_for_exception, fork_for_exception = item_with_fork.split('#', 1)
|
|
1036
|
+
|
|
628
1037
|
# Check if sources list is empty
|
|
629
1038
|
if not item['sources']:
|
|
630
1039
|
if spec_ref:
|
|
631
|
-
#
|
|
632
|
-
if
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
item_name, fork = item_with_fork.split('#', 1)
|
|
637
|
-
# Check if this item is in exceptions
|
|
638
|
-
if is_excepted(item_name, fork, exceptions):
|
|
639
|
-
total_count += 1
|
|
640
|
-
continue
|
|
1040
|
+
# Check if this item is in exceptions
|
|
1041
|
+
if item_name_for_exception and fork_for_exception:
|
|
1042
|
+
if is_excepted(item_name_for_exception, fork_for_exception, exceptions):
|
|
1043
|
+
total_count += 1
|
|
1044
|
+
continue
|
|
641
1045
|
|
|
642
1046
|
errors.append(f"EMPTY SOURCES: {spec_ref}")
|
|
643
1047
|
else:
|
|
@@ -647,6 +1051,13 @@ def check_source_files(yaml_file, project_root, exceptions=None):
|
|
|
647
1051
|
total_count += 1
|
|
648
1052
|
continue
|
|
649
1053
|
|
|
1054
|
+
# Check if item has non-empty sources but is in exceptions
|
|
1055
|
+
if item_name_for_exception and fork_for_exception:
|
|
1056
|
+
if is_excepted(item_name_for_exception, fork_for_exception, exceptions):
|
|
1057
|
+
errors.append(f"EXCEPTION CONFLICT: {spec_ref} has a specref")
|
|
1058
|
+
total_count += 1
|
|
1059
|
+
continue
|
|
1060
|
+
|
|
650
1061
|
for source in item['sources']:
|
|
651
1062
|
# All sources now use the standardized dict format with file and optional search
|
|
652
1063
|
if not isinstance(source, dict) or 'file' not in source:
|
|
@@ -758,7 +1169,7 @@ def extract_spec_tags_from_yaml(yaml_file, tag_type=None):
|
|
|
758
1169
|
|
|
759
1170
|
# Known tag type attributes
|
|
760
1171
|
tag_attributes = ['fn', 'function', 'constant_var', 'config_var', 'preset_var',
|
|
761
|
-
'ssz_object', 'dataclass', 'custom_type']
|
|
1172
|
+
'ssz_object', 'container', 'dataclass', 'custom_type']
|
|
762
1173
|
|
|
763
1174
|
try:
|
|
764
1175
|
with open(yaml_file, 'r') as f:
|
|
@@ -807,6 +1218,9 @@ def extract_spec_tags_from_yaml(yaml_file, tag_type=None):
|
|
|
807
1218
|
# Normalize function to fn
|
|
808
1219
|
if found_tag_type == 'function':
|
|
809
1220
|
found_tag_type = 'fn'
|
|
1221
|
+
# Normalize container to ssz_object
|
|
1222
|
+
if found_tag_type == 'container':
|
|
1223
|
+
found_tag_type = 'ssz_object'
|
|
810
1224
|
break
|
|
811
1225
|
|
|
812
1226
|
if found_tag_type and 'fork' in attrs:
|
|
@@ -822,7 +1236,210 @@ def extract_spec_tags_from_yaml(yaml_file, tag_type=None):
|
|
|
822
1236
|
return tag_types_found, pairs
|
|
823
1237
|
|
|
824
1238
|
|
|
825
|
-
def
|
|
1239
|
+
def generate_specrefs_from_files(files_with_spec_tags, project_dir):
|
|
1240
|
+
"""
|
|
1241
|
+
Generate specrefs data from files containing spec tags.
|
|
1242
|
+
Returns a dict with spec tag info and their source locations.
|
|
1243
|
+
"""
|
|
1244
|
+
specrefs = {}
|
|
1245
|
+
|
|
1246
|
+
for file_path in files_with_spec_tags:
|
|
1247
|
+
try:
|
|
1248
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
1249
|
+
content = f.read()
|
|
1250
|
+
|
|
1251
|
+
# Find all spec tags in the file
|
|
1252
|
+
spec_tag_pattern = r'<spec\s+([^>]+?)(?:\s*/>|>)'
|
|
1253
|
+
matches = re.finditer(spec_tag_pattern, content)
|
|
1254
|
+
|
|
1255
|
+
for match in matches:
|
|
1256
|
+
tag_attrs_str = match.group(1)
|
|
1257
|
+
attrs = extract_attributes(f"<spec {tag_attrs_str}>")
|
|
1258
|
+
|
|
1259
|
+
# Determine the spec type and name
|
|
1260
|
+
spec_type = None
|
|
1261
|
+
spec_name = None
|
|
1262
|
+
fork = attrs.get('fork', None)
|
|
1263
|
+
|
|
1264
|
+
# Check each possible spec attribute
|
|
1265
|
+
for attr_name in ['fn', 'function', 'constant_var', 'config_var',
|
|
1266
|
+
'preset_var', 'ssz_object', 'container', 'dataclass', 'custom_type']:
|
|
1267
|
+
if attr_name in attrs:
|
|
1268
|
+
spec_type = attr_name
|
|
1269
|
+
spec_name = attrs[attr_name]
|
|
1270
|
+
break
|
|
1271
|
+
|
|
1272
|
+
if spec_type and spec_name:
|
|
1273
|
+
# Normalize container to ssz_object for consistency
|
|
1274
|
+
if spec_type == 'container':
|
|
1275
|
+
spec_type = 'ssz_object'
|
|
1276
|
+
# Create a unique key for this spec reference
|
|
1277
|
+
key = f"{spec_type}.{spec_name}"
|
|
1278
|
+
if fork:
|
|
1279
|
+
key += f"#{fork}"
|
|
1280
|
+
|
|
1281
|
+
if key not in specrefs:
|
|
1282
|
+
specrefs[key] = {
|
|
1283
|
+
'name': spec_name,
|
|
1284
|
+
'type': spec_type,
|
|
1285
|
+
'fork': fork,
|
|
1286
|
+
'sources': []
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
# Add this source location
|
|
1290
|
+
rel_path = os.path.relpath(file_path, project_dir)
|
|
1291
|
+
|
|
1292
|
+
# Get line number of the match
|
|
1293
|
+
lines_before = content[:match.start()].count('\n')
|
|
1294
|
+
line_num = lines_before + 1
|
|
1295
|
+
|
|
1296
|
+
specrefs[key]['sources'].append({
|
|
1297
|
+
'file': rel_path,
|
|
1298
|
+
'line': line_num
|
|
1299
|
+
})
|
|
1300
|
+
|
|
1301
|
+
except (IOError, UnicodeDecodeError):
|
|
1302
|
+
continue
|
|
1303
|
+
|
|
1304
|
+
return specrefs
|
|
1305
|
+
|
|
1306
|
+
|
|
1307
|
+
def process_generated_specrefs(specrefs, exceptions, version):
|
|
1308
|
+
"""
|
|
1309
|
+
Process the generated specrefs and check coverage.
|
|
1310
|
+
Returns (success, results)
|
|
1311
|
+
"""
|
|
1312
|
+
results = {}
|
|
1313
|
+
overall_success = True
|
|
1314
|
+
|
|
1315
|
+
# Group specrefs by type for coverage checking
|
|
1316
|
+
specrefs_by_type = {}
|
|
1317
|
+
for _, data in specrefs.items():
|
|
1318
|
+
spec_type = data['type']
|
|
1319
|
+
if spec_type not in specrefs_by_type:
|
|
1320
|
+
specrefs_by_type[spec_type] = []
|
|
1321
|
+
specrefs_by_type[spec_type].append(data)
|
|
1322
|
+
|
|
1323
|
+
# Map spec types to history keys
|
|
1324
|
+
type_to_history_key = {
|
|
1325
|
+
'fn': 'functions',
|
|
1326
|
+
'function': 'functions',
|
|
1327
|
+
'constant_var': 'constant_vars',
|
|
1328
|
+
'config_var': 'config_vars',
|
|
1329
|
+
'preset_var': 'preset_vars',
|
|
1330
|
+
'ssz_object': 'ssz_objects',
|
|
1331
|
+
'container': 'ssz_objects',
|
|
1332
|
+
'dataclass': 'dataclasses',
|
|
1333
|
+
'custom_type': 'custom_types'
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
# Map to exception keys
|
|
1337
|
+
type_to_exception_key = {
|
|
1338
|
+
'fn': 'functions',
|
|
1339
|
+
'function': 'functions',
|
|
1340
|
+
'constant_var': 'constants',
|
|
1341
|
+
'config_var': 'configs',
|
|
1342
|
+
'preset_var': 'presets',
|
|
1343
|
+
'ssz_object': 'ssz_objects',
|
|
1344
|
+
'container': 'ssz_objects',
|
|
1345
|
+
'dataclass': 'dataclasses',
|
|
1346
|
+
'custom_type': 'custom_types'
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
# Get spec history for coverage checking
|
|
1350
|
+
history = get_spec_item_history("mainnet", version)
|
|
1351
|
+
|
|
1352
|
+
# Check coverage for each type
|
|
1353
|
+
total_found = 0
|
|
1354
|
+
total_expected = 0
|
|
1355
|
+
all_missing = []
|
|
1356
|
+
|
|
1357
|
+
for spec_type, items in specrefs_by_type.items():
|
|
1358
|
+
history_key = type_to_history_key.get(spec_type, spec_type)
|
|
1359
|
+
exception_key = type_to_exception_key.get(spec_type, spec_type)
|
|
1360
|
+
|
|
1361
|
+
# Get exceptions for this type - handle both singular and plural keys
|
|
1362
|
+
type_exceptions = []
|
|
1363
|
+
if exception_key in exceptions:
|
|
1364
|
+
type_exceptions = exceptions[exception_key]
|
|
1365
|
+
# Also check plural forms
|
|
1366
|
+
elif exception_key + 's' in exceptions:
|
|
1367
|
+
type_exceptions = exceptions[exception_key + 's']
|
|
1368
|
+
# Check if singular form exists when we have plural
|
|
1369
|
+
elif exception_key.endswith('s') and exception_key[:-1] in exceptions:
|
|
1370
|
+
type_exceptions = exceptions[exception_key[:-1]]
|
|
1371
|
+
|
|
1372
|
+
# Special handling for ssz_objects/containers
|
|
1373
|
+
if spec_type in ['ssz_object', 'container'] and not type_exceptions:
|
|
1374
|
+
# Check for 'containers' as an alternative key
|
|
1375
|
+
if 'containers' in exceptions:
|
|
1376
|
+
type_exceptions = exceptions['containers']
|
|
1377
|
+
elif 'container' in exceptions:
|
|
1378
|
+
type_exceptions = exceptions['container']
|
|
1379
|
+
|
|
1380
|
+
# Build set of what we found
|
|
1381
|
+
found_items = set()
|
|
1382
|
+
for item in items:
|
|
1383
|
+
if item['fork']:
|
|
1384
|
+
found_items.add(f"{item['name']}#{item['fork']}")
|
|
1385
|
+
else:
|
|
1386
|
+
# If no fork specified, we need to check all forks
|
|
1387
|
+
if history_key in history and item['name'] in history[history_key]:
|
|
1388
|
+
for fork in history[history_key][item['name']]:
|
|
1389
|
+
found_items.add(f"{item['name']}#{fork}")
|
|
1390
|
+
|
|
1391
|
+
# Check what's expected
|
|
1392
|
+
if history_key in history:
|
|
1393
|
+
for item_name, forks in history[history_key].items():
|
|
1394
|
+
for fork in forks:
|
|
1395
|
+
expected_key = f"{item_name}#{fork}"
|
|
1396
|
+
total_expected += 1
|
|
1397
|
+
|
|
1398
|
+
# Check if excepted
|
|
1399
|
+
if is_excepted(item_name, fork, type_exceptions):
|
|
1400
|
+
total_found += 1
|
|
1401
|
+
continue
|
|
1402
|
+
|
|
1403
|
+
if expected_key in found_items:
|
|
1404
|
+
total_found += 1
|
|
1405
|
+
else:
|
|
1406
|
+
# Use the proper type prefix for the missing item
|
|
1407
|
+
type_prefix_map = {
|
|
1408
|
+
'functions': 'functions',
|
|
1409
|
+
'constant_vars': 'constants',
|
|
1410
|
+
'config_vars': 'configs',
|
|
1411
|
+
'preset_vars': 'presets',
|
|
1412
|
+
'ssz_objects': 'ssz_objects',
|
|
1413
|
+
'dataclasses': 'dataclasses',
|
|
1414
|
+
'custom_types': 'custom_types'
|
|
1415
|
+
}
|
|
1416
|
+
prefix = type_prefix_map.get(history_key, history_key)
|
|
1417
|
+
all_missing.append(f"{prefix}.{expected_key}")
|
|
1418
|
+
|
|
1419
|
+
# Count total spec references found
|
|
1420
|
+
total_refs = len(specrefs)
|
|
1421
|
+
|
|
1422
|
+
# Store results
|
|
1423
|
+
results['Project Coverage'] = {
|
|
1424
|
+
'source_files': {
|
|
1425
|
+
'valid': total_refs,
|
|
1426
|
+
'total': total_refs,
|
|
1427
|
+
'errors': []
|
|
1428
|
+
},
|
|
1429
|
+
'coverage': {
|
|
1430
|
+
'found': total_found,
|
|
1431
|
+
'expected': total_expected,
|
|
1432
|
+
'missing': all_missing
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
if all_missing:
|
|
1437
|
+
overall_success = False
|
|
1438
|
+
|
|
1439
|
+
return overall_success, results
|
|
1440
|
+
|
|
1441
|
+
|
|
1442
|
+
def check_coverage(yaml_file, tag_type, exceptions, preset="mainnet", version="nightly"):
|
|
826
1443
|
"""
|
|
827
1444
|
Check that all spec items from ethspecify have corresponding tags in the YAML file.
|
|
828
1445
|
Returns (found_count, total_count, missing_items)
|
|
@@ -830,6 +1447,7 @@ def check_coverage(yaml_file, tag_type, exceptions, preset="mainnet"):
|
|
|
830
1447
|
# Map tag types to history keys
|
|
831
1448
|
history_key_map = {
|
|
832
1449
|
'ssz_object': 'ssz_objects',
|
|
1450
|
+
'container': 'ssz_objects',
|
|
833
1451
|
'config_var': 'config_vars',
|
|
834
1452
|
'preset_var': 'preset_vars',
|
|
835
1453
|
'dataclass': 'dataclasses',
|
|
@@ -839,7 +1457,7 @@ def check_coverage(yaml_file, tag_type, exceptions, preset="mainnet"):
|
|
|
839
1457
|
}
|
|
840
1458
|
|
|
841
1459
|
# Get expected items from ethspecify
|
|
842
|
-
history = get_spec_item_history(preset)
|
|
1460
|
+
history = get_spec_item_history(preset, version)
|
|
843
1461
|
expected_pairs = set()
|
|
844
1462
|
|
|
845
1463
|
history_key = history_key_map.get(tag_type, tag_type)
|
|
@@ -876,6 +1494,9 @@ def run_checks(project_dir, config):
|
|
|
876
1494
|
results = {}
|
|
877
1495
|
overall_success = True
|
|
878
1496
|
|
|
1497
|
+
# Get version from config
|
|
1498
|
+
version = config.get('version', 'nightly')
|
|
1499
|
+
|
|
879
1500
|
# Get specrefs config
|
|
880
1501
|
specrefs_config = config.get('specrefs', {})
|
|
881
1502
|
|
|
@@ -887,16 +1508,53 @@ def run_checks(project_dir, config):
|
|
|
887
1508
|
else:
|
|
888
1509
|
# New format: specrefs: { files: [...], exceptions: {...} }
|
|
889
1510
|
specrefs_files = specrefs_config.get('files', [])
|
|
890
|
-
exceptions = specrefs_config.get('exceptions', {})
|
|
891
1511
|
|
|
1512
|
+
# Support exceptions in either specrefs section or root, but not both
|
|
1513
|
+
specrefs_exceptions = specrefs_config.get('exceptions', {})
|
|
1514
|
+
root_exceptions = config.get('exceptions', {})
|
|
1515
|
+
|
|
1516
|
+
if specrefs_exceptions and root_exceptions:
|
|
1517
|
+
print("Warning: Exceptions found in both root and specrefs sections. Using specrefs exceptions.")
|
|
1518
|
+
exceptions = specrefs_exceptions
|
|
1519
|
+
elif specrefs_exceptions:
|
|
1520
|
+
exceptions = specrefs_exceptions
|
|
1521
|
+
else:
|
|
1522
|
+
exceptions = root_exceptions
|
|
1523
|
+
|
|
1524
|
+
# If no files specified, search the whole project for spec tags
|
|
892
1525
|
if not specrefs_files:
|
|
893
|
-
print("
|
|
894
|
-
|
|
895
|
-
|
|
1526
|
+
print("No specific files configured, searching entire project for spec tags...")
|
|
1527
|
+
|
|
1528
|
+
# Determine search root - configurable in specrefs section
|
|
1529
|
+
if 'search_root' in specrefs_config:
|
|
1530
|
+
# Use configured search_root (relative to project_dir)
|
|
1531
|
+
search_root_rel = specrefs_config['search_root']
|
|
1532
|
+
search_root = os.path.join(project_dir, search_root_rel) if not os.path.isabs(search_root_rel) else search_root_rel
|
|
1533
|
+
search_root = os.path.abspath(search_root)
|
|
1534
|
+
else:
|
|
1535
|
+
# Default behavior: if we're in a specrefs directory, search in the parent directory
|
|
1536
|
+
search_root = os.path.dirname(project_dir) if os.path.basename(project_dir) == 'specrefs' else project_dir
|
|
1537
|
+
|
|
1538
|
+
print(f"Searching for spec tags in: {search_root}")
|
|
1539
|
+
|
|
1540
|
+
# Use grep to find all files containing spec tags
|
|
1541
|
+
files_with_spec_tags = grep(search_root, r'<spec\b[^>]*>', [])
|
|
1542
|
+
|
|
1543
|
+
if not files_with_spec_tags:
|
|
1544
|
+
print(f"No files with spec tags found in the project")
|
|
1545
|
+
return True, {}
|
|
1546
|
+
|
|
1547
|
+
# Generate in-memory specrefs data from the found spec tags
|
|
1548
|
+
all_specrefs = generate_specrefs_from_files(files_with_spec_tags, search_root)
|
|
1549
|
+
|
|
1550
|
+
# Process the generated specrefs
|
|
1551
|
+
return process_generated_specrefs(all_specrefs, exceptions, version)
|
|
1552
|
+
|
|
896
1553
|
|
|
897
1554
|
# Map tag types to exception keys (support both singular and plural)
|
|
898
1555
|
exception_key_map = {
|
|
899
|
-
'ssz_object': ['ssz_objects', 'ssz_object'],
|
|
1556
|
+
'ssz_object': ['ssz_objects', 'ssz_object', 'containers', 'container'],
|
|
1557
|
+
'container': ['ssz_objects', 'ssz_object', 'containers', 'container'],
|
|
900
1558
|
'config_var': ['configs', 'config_variables', 'config_var'],
|
|
901
1559
|
'preset_var': ['presets', 'preset_variables', 'preset_var'],
|
|
902
1560
|
'dataclass': ['dataclasses', 'dataclass'],
|
|
@@ -925,7 +1583,16 @@ def run_checks(project_dir, config):
|
|
|
925
1583
|
# Process each tag type found in the file
|
|
926
1584
|
if not tag_types_found:
|
|
927
1585
|
# No spec tags found, still check source files
|
|
928
|
-
|
|
1586
|
+
# Determine source root - use search_root if configured, otherwise use default behavior
|
|
1587
|
+
if 'search_root' in specrefs_config:
|
|
1588
|
+
search_root_rel = specrefs_config['search_root']
|
|
1589
|
+
source_root = os.path.join(project_dir, search_root_rel) if not os.path.isabs(search_root_rel) else search_root_rel
|
|
1590
|
+
source_root = os.path.abspath(source_root)
|
|
1591
|
+
else:
|
|
1592
|
+
# Default behavior: parent directory
|
|
1593
|
+
source_root = os.path.dirname(project_dir)
|
|
1594
|
+
|
|
1595
|
+
valid_count, total_count, source_errors = check_source_files(yaml_path, source_root, [])
|
|
929
1596
|
|
|
930
1597
|
# Store results using filename as section name
|
|
931
1598
|
section_name = filename.replace('.yml', '').replace('-', ' ').title()
|
|
@@ -963,7 +1630,7 @@ def run_checks(project_dir, config):
|
|
|
963
1630
|
break
|
|
964
1631
|
|
|
965
1632
|
# Check coverage for this specific tag type
|
|
966
|
-
found_count, expected_count, missing_items = check_coverage(yaml_path, tag_type, section_exceptions, preset)
|
|
1633
|
+
found_count, expected_count, missing_items = check_coverage(yaml_path, tag_type, section_exceptions, preset, version)
|
|
967
1634
|
total_found += found_count
|
|
968
1635
|
total_expected += expected_count
|
|
969
1636
|
all_missing_items.extend(missing_items)
|
|
@@ -977,7 +1644,16 @@ def run_checks(project_dir, config):
|
|
|
977
1644
|
if key in exceptions:
|
|
978
1645
|
all_exceptions.extend(exceptions[key])
|
|
979
1646
|
|
|
980
|
-
|
|
1647
|
+
# Determine source root - use search_root if configured, otherwise use default behavior
|
|
1648
|
+
if 'search_root' in specrefs_config:
|
|
1649
|
+
search_root_rel = specrefs_config['search_root']
|
|
1650
|
+
source_root = os.path.join(project_dir, search_root_rel) if not os.path.isabs(search_root_rel) else search_root_rel
|
|
1651
|
+
source_root = os.path.abspath(source_root)
|
|
1652
|
+
else:
|
|
1653
|
+
# Default behavior: parent directory
|
|
1654
|
+
source_root = os.path.dirname(project_dir)
|
|
1655
|
+
|
|
1656
|
+
valid_count, total_count, source_errors = check_source_files(yaml_path, source_root, all_exceptions)
|
|
981
1657
|
|
|
982
1658
|
# Store results using filename as section name
|
|
983
1659
|
section_name = filename.replace('.yml', '').replace('-', ' ').title()
|
|
@@ -1002,3 +1678,375 @@ def run_checks(project_dir, config):
|
|
|
1002
1678
|
overall_success = False
|
|
1003
1679
|
|
|
1004
1680
|
return overall_success, results
|
|
1681
|
+
|
|
1682
|
+
|
|
1683
|
+
def update_entry_names_in_yaml_files(project_dir, specrefs_files):
|
|
1684
|
+
"""
|
|
1685
|
+
Update all entry names to use the format <spec_item>#<fork>.
|
|
1686
|
+
"""
|
|
1687
|
+
for yaml_file in specrefs_files:
|
|
1688
|
+
yaml_path = os.path.join(project_dir, yaml_file)
|
|
1689
|
+
|
|
1690
|
+
if not os.path.exists(yaml_path):
|
|
1691
|
+
continue
|
|
1692
|
+
|
|
1693
|
+
# Load existing entries
|
|
1694
|
+
existing_entries = load_yaml_entries(yaml_path)
|
|
1695
|
+
if not existing_entries:
|
|
1696
|
+
continue
|
|
1697
|
+
|
|
1698
|
+
updated = False
|
|
1699
|
+
for entry in existing_entries:
|
|
1700
|
+
if not isinstance(entry, dict) or 'spec' not in entry:
|
|
1701
|
+
continue
|
|
1702
|
+
|
|
1703
|
+
# Extract spec tag attributes
|
|
1704
|
+
spec_content = entry['spec']
|
|
1705
|
+
match = re.search(r'<spec\b([^>]*)>', spec_content)
|
|
1706
|
+
if not match:
|
|
1707
|
+
continue
|
|
1708
|
+
|
|
1709
|
+
attributes = extract_attributes(match.group(0))
|
|
1710
|
+
|
|
1711
|
+
# Get the spec item name and fork
|
|
1712
|
+
spec_attr, item_name = get_spec_attr_and_name(attributes)
|
|
1713
|
+
fork = attributes.get('fork')
|
|
1714
|
+
|
|
1715
|
+
if item_name and fork:
|
|
1716
|
+
# Build the expected name
|
|
1717
|
+
expected_name = f'{item_name}#{fork}'
|
|
1718
|
+
|
|
1719
|
+
# Update if different
|
|
1720
|
+
if entry.get('name') != expected_name:
|
|
1721
|
+
entry['name'] = expected_name
|
|
1722
|
+
updated = True
|
|
1723
|
+
|
|
1724
|
+
# Write back if updated
|
|
1725
|
+
if updated:
|
|
1726
|
+
with open(yaml_path, 'w') as f:
|
|
1727
|
+
for i, entry in enumerate(existing_entries):
|
|
1728
|
+
if i > 0:
|
|
1729
|
+
f.write('\n')
|
|
1730
|
+
f.write(f'- name: {entry["name"]}\n')
|
|
1731
|
+
if 'sources' in entry:
|
|
1732
|
+
if isinstance(entry['sources'], list) and len(entry['sources']) == 0:
|
|
1733
|
+
f.write(' sources: []\n')
|
|
1734
|
+
else:
|
|
1735
|
+
f.write(' sources:\n')
|
|
1736
|
+
for source in entry['sources']:
|
|
1737
|
+
if isinstance(source, dict):
|
|
1738
|
+
f.write(f' - file: {source.get("file", "")}\n')
|
|
1739
|
+
if 'search' in source:
|
|
1740
|
+
f.write(f' search: {source["search"]}\n')
|
|
1741
|
+
if 'regex' in source:
|
|
1742
|
+
f.write(f' regex: {source["regex"]}\n')
|
|
1743
|
+
else:
|
|
1744
|
+
f.write(f' - {source}\n')
|
|
1745
|
+
if 'spec' in entry:
|
|
1746
|
+
f.write(' spec: |\n')
|
|
1747
|
+
for line in entry['spec'].split('\n'):
|
|
1748
|
+
f.write(f' {line}\n')
|
|
1749
|
+
|
|
1750
|
+
# Sort the file
|
|
1751
|
+
sort_specref_yaml(yaml_path)
|
|
1752
|
+
print(f"Updated entry names in {yaml_file}")
|
|
1753
|
+
|
|
1754
|
+
|
|
1755
|
+
def add_missing_spec_items_to_yaml_files(project_dir, config, specrefs_files):
|
|
1756
|
+
"""
|
|
1757
|
+
Add missing spec items to existing YAML files.
|
|
1758
|
+
Ensures all spec items from the specification exist in YAML files with sources: []
|
|
1759
|
+
"""
|
|
1760
|
+
version = config.get('version', 'nightly')
|
|
1761
|
+
preset = 'mainnet' # Could make this configurable
|
|
1762
|
+
|
|
1763
|
+
# Get all spec items
|
|
1764
|
+
pyspec = get_pyspec(version)
|
|
1765
|
+
if preset not in pyspec:
|
|
1766
|
+
print(f"Error: Preset '{preset}' not found")
|
|
1767
|
+
return
|
|
1768
|
+
|
|
1769
|
+
# Get all forks in chronological order, excluding EIP forks
|
|
1770
|
+
all_forks = sorted(
|
|
1771
|
+
[fork for fork in pyspec[preset].keys() if not fork.startswith("eip")],
|
|
1772
|
+
key=lambda x: (x != "phase0", x)
|
|
1773
|
+
)
|
|
1774
|
+
|
|
1775
|
+
# Map YAML filenames to category keys and spec attribute names
|
|
1776
|
+
filename_to_category = {
|
|
1777
|
+
'constants.yml': ('constant_vars', 'constant_var'),
|
|
1778
|
+
'configs.yml': ('config_vars', 'config_var'),
|
|
1779
|
+
'presets.yml': ('preset_vars', 'preset_var'),
|
|
1780
|
+
'functions.yml': ('functions', 'fn'),
|
|
1781
|
+
'containers.yml': ('ssz_objects', 'container'),
|
|
1782
|
+
'dataclasses.yml': ('dataclasses', 'dataclass'),
|
|
1783
|
+
'types.yml': ('custom_types', 'custom_type'),
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
for yaml_file in specrefs_files:
|
|
1787
|
+
yaml_path = os.path.join(project_dir, yaml_file)
|
|
1788
|
+
yaml_basename = os.path.basename(yaml_file)
|
|
1789
|
+
|
|
1790
|
+
if yaml_basename not in filename_to_category:
|
|
1791
|
+
continue
|
|
1792
|
+
|
|
1793
|
+
category, spec_attr = filename_to_category[yaml_basename]
|
|
1794
|
+
|
|
1795
|
+
# Collect all items in this category organized by name and fork
|
|
1796
|
+
items_by_name = {}
|
|
1797
|
+
for fork in all_forks:
|
|
1798
|
+
if fork not in pyspec[preset]:
|
|
1799
|
+
continue
|
|
1800
|
+
fork_data = pyspec[preset][fork]
|
|
1801
|
+
|
|
1802
|
+
if category not in fork_data:
|
|
1803
|
+
continue
|
|
1804
|
+
|
|
1805
|
+
for item_name, item_data in fork_data[category].items():
|
|
1806
|
+
if item_name not in items_by_name:
|
|
1807
|
+
items_by_name[item_name] = []
|
|
1808
|
+
items_by_name[item_name].append((fork, item_data))
|
|
1809
|
+
|
|
1810
|
+
# Build entries for missing items
|
|
1811
|
+
new_entries = []
|
|
1812
|
+
for item_name in sorted(items_by_name.keys()):
|
|
1813
|
+
forks_data = items_by_name[item_name]
|
|
1814
|
+
|
|
1815
|
+
# Find all unique versions of this item (where content differs)
|
|
1816
|
+
versions = [] # List of (fork, item_data, spec_content)
|
|
1817
|
+
prev_content = None
|
|
1818
|
+
|
|
1819
|
+
for fork, item_data in forks_data:
|
|
1820
|
+
# Build the spec content based on category
|
|
1821
|
+
if category == 'functions':
|
|
1822
|
+
spec_content = item_data
|
|
1823
|
+
elif category in ['constant_vars', 'config_vars', 'preset_vars']:
|
|
1824
|
+
# item_data is a list: [type, value, ...]
|
|
1825
|
+
if isinstance(item_data, (list, tuple)) and len(item_data) >= 2:
|
|
1826
|
+
type_info = item_data[0]
|
|
1827
|
+
value = item_data[1]
|
|
1828
|
+
if type_info:
|
|
1829
|
+
spec_content = f"{item_name}: {type_info} = {value}"
|
|
1830
|
+
else:
|
|
1831
|
+
spec_content = f"{item_name} = {value}"
|
|
1832
|
+
else:
|
|
1833
|
+
spec_content = str(item_data)
|
|
1834
|
+
elif category == 'ssz_objects':
|
|
1835
|
+
spec_content = item_data
|
|
1836
|
+
elif category == 'dataclasses':
|
|
1837
|
+
spec_content = item_data.replace("@dataclass\n", "")
|
|
1838
|
+
elif category == 'custom_types':
|
|
1839
|
+
# custom_types are simple type aliases: TypeName = SomeType
|
|
1840
|
+
spec_content = f"{item_name} = {item_data}"
|
|
1841
|
+
else:
|
|
1842
|
+
spec_content = str(item_data)
|
|
1843
|
+
|
|
1844
|
+
# Only add this version if it's different from the previous one
|
|
1845
|
+
if prev_content is None or spec_content != prev_content:
|
|
1846
|
+
versions.append((fork, item_data, spec_content))
|
|
1847
|
+
prev_content = spec_content
|
|
1848
|
+
|
|
1849
|
+
# Create entries based on number of unique versions
|
|
1850
|
+
use_fork_suffix = len(versions) > 1
|
|
1851
|
+
|
|
1852
|
+
for idx, (fork, item_data, spec_content) in enumerate(versions):
|
|
1853
|
+
# Calculate hash of current version
|
|
1854
|
+
hash_value = hashlib.sha256(spec_content.encode('utf-8')).hexdigest()[:8]
|
|
1855
|
+
|
|
1856
|
+
# For multiple versions after the first, use diff style
|
|
1857
|
+
if use_fork_suffix and idx > 0:
|
|
1858
|
+
# Get previous version for diff
|
|
1859
|
+
prev_fork, _, prev_spec_content = versions[idx - 1]
|
|
1860
|
+
|
|
1861
|
+
# Generate diff
|
|
1862
|
+
diff_content = diff(prev_fork, strip_comments(prev_spec_content), fork, strip_comments(spec_content))
|
|
1863
|
+
|
|
1864
|
+
# Build spec tag with style="diff"
|
|
1865
|
+
spec_tag = f'<spec {spec_attr}="{item_name}" fork="{fork}" style="diff" hash="{hash_value}">'
|
|
1866
|
+
content = diff_content
|
|
1867
|
+
else:
|
|
1868
|
+
# Build spec tag without style="diff"
|
|
1869
|
+
spec_tag = f'<spec {spec_attr}="{item_name}" fork="{fork}" hash="{hash_value}">'
|
|
1870
|
+
content = spec_content
|
|
1871
|
+
|
|
1872
|
+
# Create entry
|
|
1873
|
+
entry_name = f'{item_name}#{fork}' if use_fork_suffix else item_name
|
|
1874
|
+
entry = {
|
|
1875
|
+
'name': entry_name,
|
|
1876
|
+
'sources': [],
|
|
1877
|
+
'spec': f'{spec_tag}\n{content}\n</spec>'
|
|
1878
|
+
}
|
|
1879
|
+
new_entries.append(entry)
|
|
1880
|
+
|
|
1881
|
+
# Add missing entries to the YAML file
|
|
1882
|
+
if new_entries:
|
|
1883
|
+
add_missing_entries_to_yaml(yaml_path, new_entries)
|
|
1884
|
+
|
|
1885
|
+
|
|
1886
|
+
def generate_specref_files(output_dir, version="nightly", preset="mainnet"):
|
|
1887
|
+
"""
|
|
1888
|
+
Generate specref YAML files without sources for manual mapping.
|
|
1889
|
+
Creates a basic directory structure with empty sources.
|
|
1890
|
+
"""
|
|
1891
|
+
# Create output directory if it doesn't exist
|
|
1892
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
1893
|
+
|
|
1894
|
+
# Get all spec items
|
|
1895
|
+
pyspec = get_pyspec(version)
|
|
1896
|
+
if preset not in pyspec:
|
|
1897
|
+
raise ValueError(f"Preset '{preset}' not found")
|
|
1898
|
+
|
|
1899
|
+
# Get all forks in chronological order, excluding EIP forks
|
|
1900
|
+
all_forks = sorted(
|
|
1901
|
+
[fork for fork in pyspec[preset].keys() if not fork.startswith("eip")],
|
|
1902
|
+
key=lambda x: (x != "phase0", x)
|
|
1903
|
+
)
|
|
1904
|
+
|
|
1905
|
+
# Map history keys to file names and spec attribute names
|
|
1906
|
+
category_map = {
|
|
1907
|
+
'constant_vars': ('constants.yml', 'constant_var'),
|
|
1908
|
+
'config_vars': ('configs.yml', 'config_var'),
|
|
1909
|
+
'preset_vars': ('presets.yml', 'preset_var'),
|
|
1910
|
+
'functions': ('functions.yml', 'fn'),
|
|
1911
|
+
'ssz_objects': ('containers.yml', 'container'),
|
|
1912
|
+
'dataclasses': ('dataclasses.yml', 'dataclass'),
|
|
1913
|
+
'custom_types': ('types.yml', 'custom_type'),
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
# Collect all items organized by category
|
|
1917
|
+
items_by_category = {cat: {} for cat in category_map.keys()}
|
|
1918
|
+
|
|
1919
|
+
for fork in all_forks:
|
|
1920
|
+
if fork not in pyspec[preset]:
|
|
1921
|
+
continue
|
|
1922
|
+
fork_data = pyspec[preset][fork]
|
|
1923
|
+
|
|
1924
|
+
for category in items_by_category.keys():
|
|
1925
|
+
if category not in fork_data:
|
|
1926
|
+
continue
|
|
1927
|
+
|
|
1928
|
+
for item_name, item_data in fork_data[category].items():
|
|
1929
|
+
# Track which forks have this item
|
|
1930
|
+
if item_name not in items_by_category[category]:
|
|
1931
|
+
items_by_category[category][item_name] = []
|
|
1932
|
+
items_by_category[category][item_name].append((fork, item_data))
|
|
1933
|
+
|
|
1934
|
+
# Generate YAML files for each category
|
|
1935
|
+
for category, (filename, spec_attr) in category_map.items():
|
|
1936
|
+
if not items_by_category[category]:
|
|
1937
|
+
continue
|
|
1938
|
+
|
|
1939
|
+
output_path = os.path.join(output_dir, filename)
|
|
1940
|
+
entries = []
|
|
1941
|
+
|
|
1942
|
+
# Sort items alphabetically
|
|
1943
|
+
for item_name in sorted(items_by_category[category].keys()):
|
|
1944
|
+
forks_data = items_by_category[category][item_name]
|
|
1945
|
+
|
|
1946
|
+
# Find all unique versions of this item (where content differs)
|
|
1947
|
+
versions = [] # List of (fork, item_data, spec_content)
|
|
1948
|
+
prev_content = None
|
|
1949
|
+
|
|
1950
|
+
for fork, item_data in forks_data:
|
|
1951
|
+
# Build the spec content based on category
|
|
1952
|
+
if category == 'functions':
|
|
1953
|
+
spec_content = item_data
|
|
1954
|
+
elif category in ['constant_vars', 'config_vars', 'preset_vars']:
|
|
1955
|
+
# item_data is a list: [type, value, ...]
|
|
1956
|
+
if isinstance(item_data, (list, tuple)) and len(item_data) >= 2:
|
|
1957
|
+
type_info = item_data[0]
|
|
1958
|
+
value = item_data[1]
|
|
1959
|
+
if type_info:
|
|
1960
|
+
spec_content = f"{item_name}: {type_info} = {value}"
|
|
1961
|
+
else:
|
|
1962
|
+
spec_content = f"{item_name} = {value}"
|
|
1963
|
+
else:
|
|
1964
|
+
spec_content = str(item_data)
|
|
1965
|
+
elif category == 'ssz_objects':
|
|
1966
|
+
spec_content = item_data
|
|
1967
|
+
elif category == 'dataclasses':
|
|
1968
|
+
spec_content = item_data.replace("@dataclass\n", "")
|
|
1969
|
+
elif category == 'custom_types':
|
|
1970
|
+
# custom_types are simple type aliases: TypeName = SomeType
|
|
1971
|
+
spec_content = f"{item_name} = {item_data}"
|
|
1972
|
+
else:
|
|
1973
|
+
spec_content = str(item_data)
|
|
1974
|
+
|
|
1975
|
+
# Only add this version if it's different from the previous one
|
|
1976
|
+
if prev_content is None or spec_content != prev_content:
|
|
1977
|
+
versions.append((fork, item_data, spec_content))
|
|
1978
|
+
prev_content = spec_content
|
|
1979
|
+
|
|
1980
|
+
# Create entries based on number of unique versions
|
|
1981
|
+
use_fork_suffix = len(versions) > 1
|
|
1982
|
+
|
|
1983
|
+
for idx, (fork, item_data, spec_content) in enumerate(versions):
|
|
1984
|
+
# Calculate hash of current version
|
|
1985
|
+
hash_value = hashlib.sha256(spec_content.encode('utf-8')).hexdigest()[:8]
|
|
1986
|
+
|
|
1987
|
+
# For multiple versions after the first, use diff style
|
|
1988
|
+
if use_fork_suffix and idx > 0:
|
|
1989
|
+
# Get previous version for diff
|
|
1990
|
+
prev_fork, _, prev_spec_content = versions[idx - 1]
|
|
1991
|
+
|
|
1992
|
+
# Generate diff
|
|
1993
|
+
diff_content = diff(prev_fork, strip_comments(prev_spec_content), fork, strip_comments(spec_content))
|
|
1994
|
+
|
|
1995
|
+
# Build spec tag with style="diff"
|
|
1996
|
+
spec_tag = f'<spec {spec_attr}="{item_name}" fork="{fork}" style="diff" hash="{hash_value}">'
|
|
1997
|
+
content = diff_content
|
|
1998
|
+
else:
|
|
1999
|
+
# Build spec tag without style="diff"
|
|
2000
|
+
spec_tag = f'<spec {spec_attr}="{item_name}" fork="{fork}" hash="{hash_value}">'
|
|
2001
|
+
content = spec_content
|
|
2002
|
+
|
|
2003
|
+
# Create entry
|
|
2004
|
+
entry_name = f'{item_name}#{fork}' if use_fork_suffix else item_name
|
|
2005
|
+
entry = {
|
|
2006
|
+
'name': entry_name,
|
|
2007
|
+
'sources': [],
|
|
2008
|
+
'spec': f'{spec_tag}\n{content}\n</spec>'
|
|
2009
|
+
}
|
|
2010
|
+
entries.append(entry)
|
|
2011
|
+
|
|
2012
|
+
# Write YAML file
|
|
2013
|
+
if entries:
|
|
2014
|
+
with open(output_path, 'w') as f:
|
|
2015
|
+
for i, entry in enumerate(entries):
|
|
2016
|
+
if i > 0:
|
|
2017
|
+
f.write('\n')
|
|
2018
|
+
f.write(f'- name: {entry["name"]}\n')
|
|
2019
|
+
f.write(' sources: []\n')
|
|
2020
|
+
f.write(' spec: |\n')
|
|
2021
|
+
for line in entry['spec'].split('\n'):
|
|
2022
|
+
f.write(f' {line}\n')
|
|
2023
|
+
|
|
2024
|
+
# Create .ethspecify.yml config file
|
|
2025
|
+
config_path = os.path.join(output_dir, '.ethspecify.yml')
|
|
2026
|
+
with open(config_path, 'w') as f:
|
|
2027
|
+
f.write(f'version: {version}\n')
|
|
2028
|
+
f.write('style: full\n')
|
|
2029
|
+
f.write('\n')
|
|
2030
|
+
f.write('specrefs:\n')
|
|
2031
|
+
f.write(' files:\n')
|
|
2032
|
+
for category, (filename, _) in category_map.items():
|
|
2033
|
+
if items_by_category[category]:
|
|
2034
|
+
f.write(f' - {filename}\n')
|
|
2035
|
+
f.write('\n')
|
|
2036
|
+
f.write(' exceptions:\n')
|
|
2037
|
+
f.write(' # Add any exceptions here\n')
|
|
2038
|
+
|
|
2039
|
+
# Strip trailing whitespace from all generated files
|
|
2040
|
+
all_files = [config_path]
|
|
2041
|
+
for category, (filename, _) in category_map.items():
|
|
2042
|
+
if items_by_category[category]:
|
|
2043
|
+
all_files.append(os.path.join(output_dir, filename))
|
|
2044
|
+
|
|
2045
|
+
for file_path in all_files:
|
|
2046
|
+
with open(file_path, 'r') as f:
|
|
2047
|
+
lines = f.readlines()
|
|
2048
|
+
with open(file_path, 'w') as f:
|
|
2049
|
+
for line in lines:
|
|
2050
|
+
f.write(line.rstrip() + '\n')
|
|
2051
|
+
|
|
2052
|
+
return list(category_map.values())
|