ethspecify 0.2.9__py3-none-any.whl → 0.3.1__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.

Potentially problematic release.


This version of ethspecify might be problematic. Click here for more details.

ethspecify/cli.py CHANGED
@@ -3,11 +3,11 @@ import json
3
3
  import os
4
4
  import sys
5
5
 
6
- from .core import grep, replace_spec_tags, get_pyspec, get_latest_fork, get_spec_item_history, load_config, run_checks
6
+ from .core import grep, replace_spec_tags, get_pyspec, get_latest_fork, get_spec_item_history, load_config, run_checks, sort_specref_yaml, generate_specref_files
7
7
 
8
8
 
9
9
  def process(args):
10
- """Process all spec tags."""
10
+ """Process all spec tags and sort specref YAML files."""
11
11
  project_dir = os.path.abspath(os.path.expanduser(args.path))
12
12
  if not os.path.isdir(project_dir):
13
13
  print(f"Error: The directory {repr(project_dir)} does not exist.")
@@ -16,10 +16,26 @@ def process(args):
16
16
  # Load config once from the project directory
17
17
  config = load_config(project_dir)
18
18
 
19
+ # Process spec tags in files
19
20
  for f in grep(project_dir, r"<spec\b.*?>", args.exclude):
20
21
  print(f"Processing file: {f}")
21
22
  replace_spec_tags(f, config)
22
23
 
24
+ # Sort specref YAML files if they exist in config
25
+ specrefs_config = config.get('specrefs', {})
26
+ if isinstance(specrefs_config, dict):
27
+ specrefs_files = specrefs_config.get('files', [])
28
+ elif isinstance(specrefs_config, list):
29
+ specrefs_files = specrefs_config
30
+ else:
31
+ specrefs_files = []
32
+
33
+ for yaml_file in specrefs_files:
34
+ yaml_path = os.path.join(project_dir, yaml_file)
35
+ if os.path.exists(yaml_path):
36
+ if not sort_specref_yaml(yaml_path):
37
+ print(f"Error sorting: {yaml_file}")
38
+
23
39
  return 0
24
40
 
25
41
 
@@ -166,6 +182,28 @@ def list_forks(args):
166
182
  return 0
167
183
 
168
184
 
185
+ def init(args):
186
+ """Initialize a specrefs directory with basic configuration and empty source mappings."""
187
+ output_dir = args.path or "specrefs"
188
+ version = args.version
189
+
190
+ # Check if output directory already exists
191
+ if os.path.exists(output_dir):
192
+ print(f"Error: directory {repr(output_dir)} already exists.")
193
+ print("Please specify a different directory or remove the existing one.")
194
+ return 1
195
+
196
+ try:
197
+ # Generate the specref files
198
+ print(f"Initializing specrefs directory: {version}")
199
+ generate_specref_files(output_dir, version, "mainnet")
200
+ print(f"Successfully created specrefs directory at: {output_dir}")
201
+ return 0
202
+ except Exception as e:
203
+ print(f"Error: {e}")
204
+ return 1
205
+
206
+
169
207
  def main():
170
208
  parser = argparse.ArgumentParser(
171
209
  description="Process files containing <spec> tags."
@@ -240,6 +278,21 @@ def main():
240
278
  help="Output format (text or json)",
241
279
  )
242
280
 
281
+ # Parser for 'init' command
282
+ init_parser = subparsers.add_parser("init", help="Initialize a specrefs directory")
283
+ init_parser.set_defaults(func=init)
284
+ init_parser.add_argument(
285
+ "version",
286
+ type=str,
287
+ help="Specification version (e.g., 'nightly' or 'v1.6.0-alpha.5')",
288
+ )
289
+ init_parser.add_argument(
290
+ "--path",
291
+ type=str,
292
+ help="Output directory for specrefs (default: specrefs)",
293
+ default="specrefs",
294
+ )
295
+
243
296
  # Default to 'process' if no args are provided
244
297
  if len(sys.argv) == 1:
245
298
  sys.argv.insert(1, "process")
ethspecify/core.py CHANGED
@@ -11,6 +11,90 @@ 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
+ errors.append(f"invalid key: {exception_key}.{item_name}{"#" + fork if fork else ""}")
93
+
94
+ if errors:
95
+ error_msg = "Invalid exception items in configuration:\n" + "\n".join(f" - {e}" for e in errors)
96
+ raise Exception(error_msg)
97
+
14
98
  def load_config(directory=None):
15
99
  """
16
100
  Load configuration from .ethspecify.yml file in the specified directory.
@@ -25,7 +109,22 @@ def load_config(directory=None):
25
109
  try:
26
110
  with open(config_path, 'r') as f:
27
111
  config = yaml.safe_load(f)
28
- return config if config else {}
112
+ if not config:
113
+ return {}
114
+
115
+ # Get version from config, default to 'nightly'
116
+ version = config.get('version', 'nightly')
117
+
118
+ # Validate exceptions in root config
119
+ if 'exceptions' in config:
120
+ validate_exception_items(config['exceptions'], version)
121
+
122
+ # Also validate exceptions in specrefs section if present
123
+ if 'specrefs' in config and isinstance(config['specrefs'], dict):
124
+ if 'exceptions' in config['specrefs']:
125
+ validate_exception_items(config['specrefs']['exceptions'], version)
126
+
127
+ return config
29
128
  except (yaml.YAMLError, IOError) as e:
30
129
  print(f"Warning: Error reading .ethspecify.yml file: {e}")
31
130
  return {}
@@ -477,6 +576,140 @@ def extract_attributes(tag):
477
576
  return dict(attr_pattern.findall(tag))
478
577
 
479
578
 
579
+ def sort_specref_yaml(yaml_file):
580
+ """
581
+ Sort specref entries in a YAML file by their name field.
582
+ Preserves formatting and adds single blank lines between entries.
583
+ """
584
+ if not os.path.exists(yaml_file):
585
+ return False
586
+
587
+ try:
588
+ with open(yaml_file, 'r') as f:
589
+ content_str = f.read()
590
+
591
+ # Try to fix common YAML issues with unquoted search strings containing colons
592
+ # This is the same fix used in check_source_files
593
+ content_str = re.sub(r'(\s+search:\s+)([^"\n]+:)(\s*$)', r'\1"\2"\3', content_str, flags=re.MULTILINE)
594
+
595
+ try:
596
+ content = yaml.safe_load(content_str)
597
+ except yaml.YAMLError:
598
+ # Fall back to FullLoader if safe_load fails
599
+ content = yaml.load(content_str, Loader=yaml.FullLoader)
600
+
601
+ if not content:
602
+ return False
603
+
604
+ # Handle both array of objects and single object formats
605
+ if isinstance(content, list):
606
+ # Sort the list by 'name' field if it exists
607
+ # Special handling for fork ordering within the same item
608
+ def sort_key(x):
609
+ name = x.get('name', '') if isinstance(x, dict) else str(x)
610
+
611
+ # Known fork names for ordering
612
+ forks = ['phase0', 'altair', 'bellatrix', 'capella', 'deneb', 'electra']
613
+
614
+ # Check if name contains a # separator (like "slash_validator#phase0")
615
+ if '#' in name:
616
+ base_name, fork = name.rsplit('#', 1)
617
+ # Define fork order based on known forks list
618
+ fork_lower = fork.lower()
619
+ if fork_lower in forks:
620
+ fork_order = forks.index(fork_lower)
621
+ else:
622
+ # Unknown forks go after known ones, sorted alphabetically
623
+ fork_order = len(forks)
624
+ return (base_name, fork_order, fork)
625
+ else:
626
+ # Check if name ends with a fork name (like "BeaconStatePhase0")
627
+ name_lower = name.lower()
628
+ for i, fork in enumerate(forks):
629
+ if name_lower.endswith(fork):
630
+ # Extract base name
631
+ base_name = name[:-len(fork)]
632
+ return (base_name, i, name)
633
+
634
+ # No fork pattern found, just sort by name
635
+ return (name, 0, '')
636
+
637
+ sorted_content = sorted(content, key=sort_key)
638
+
639
+ # Custom YAML writing to preserve formatting
640
+ output_lines = []
641
+ for i, item in enumerate(sorted_content):
642
+ if i > 0:
643
+ # Add a single blank line between entries
644
+ output_lines.append("")
645
+
646
+ # Start each entry with a dash
647
+ first_line = True
648
+ for key, value in item.items():
649
+ if first_line:
650
+ prefix = "- "
651
+ first_line = False
652
+ else:
653
+ prefix = " "
654
+
655
+ if key == 'spec':
656
+ # Preserve spec content as-is, using literal style
657
+ output_lines.append(f"{prefix}{key}: |")
658
+ # Indent the spec content - preserve it exactly as-is
659
+ spec_lines = value.rstrip().split('\n') if isinstance(value, str) else str(value).rstrip().split('\n')
660
+ for spec_line in spec_lines:
661
+ output_lines.append(f" {spec_line}")
662
+ elif key == 'sources':
663
+ if isinstance(value, list) and len(value) == 0:
664
+ # Keep empty lists on the same line for clarity
665
+ output_lines.append(f"{prefix}{key}: []")
666
+ else:
667
+ output_lines.append(f"{prefix}{key}:")
668
+ if isinstance(value, list):
669
+ for source in value:
670
+ if isinstance(source, dict):
671
+ output_lines.append(f" - file: {source.get('file', '')}")
672
+ if 'search' in source:
673
+ search_val = source['search']
674
+ # Only quote if:
675
+ # 1. Contains a colon followed by space or end of string (YAML mapping issue)
676
+ # 2. Not already quoted
677
+ # 3. Doesn't contain internal quotes (like regex patterns)
678
+ if ((':' in search_val or search_val.endswith(':')) and
679
+ not (search_val.startswith('"') and search_val.endswith('"')) and
680
+ '"' not in search_val):
681
+ # Only quote simple strings with colons, not complex patterns
682
+ if not any(char in search_val for char in ['\\', '*', '|', '[', ']', '(', ')']):
683
+ search_val = f'"{search_val}"'
684
+ output_lines.append(f" search: {search_val}")
685
+ if 'regex' in source:
686
+ # Keep boolean values lowercase for consistency
687
+ regex_val = str(source['regex']).lower()
688
+ output_lines.append(f" regex: {regex_val}")
689
+ else:
690
+ output_lines.append(f" - {source}")
691
+ else:
692
+ # Handle other fields - don't escape or modify the value
693
+ output_lines.append(f"{prefix}{key}: {value}")
694
+
695
+ # Strip trailing whitespace from all lines
696
+ output_lines = [line.rstrip() for line in output_lines]
697
+
698
+ # Write back the sorted content
699
+ with open(yaml_file, 'w') as f:
700
+ f.write('\n'.join(output_lines))
701
+ f.write('\n') # End file with newline
702
+
703
+ return True
704
+ elif isinstance(content, dict):
705
+ # If it's a single dict, we can't sort it
706
+ return False
707
+ except (yaml.YAMLError, IOError) as e:
708
+ print(f"Error sorting {yaml_file}: {e}")
709
+ return False
710
+
711
+ return False
712
+
480
713
  def replace_spec_tags(file_path, config=None):
481
714
  with open(file_path, 'r') as file:
482
715
  content = file.read()
@@ -540,7 +773,7 @@ def replace_spec_tags(file_path, config=None):
540
773
  prefix = content[:match.start()].splitlines()[-1]
541
774
  prefixed_spec = "\n".join(
542
775
  f"{prefix}{line}" if line.rstrip() else prefix.rstrip()
543
- for line in spec_content.rstrip().split("\n")
776
+ for line in spec_content.split("\n")
544
777
  )
545
778
  updated_tag = f"{new_opening}\n{prefixed_spec}\n{prefix}</spec>"
546
779
  return updated_tag
@@ -632,19 +865,23 @@ def check_source_files(yaml_file, project_root, exceptions=None):
632
865
  if not spec_ref and 'name' in item:
633
866
  spec_ref = item['name']
634
867
 
868
+ # Extract item name and fork for exception checking
869
+ item_name_for_exception = None
870
+ fork_for_exception = None
871
+ if spec_ref and '#' in spec_ref and '.' in spec_ref:
872
+ # Format: "functions.item_name#fork"
873
+ _, item_with_fork = spec_ref.split('.', 1)
874
+ if '#' in item_with_fork:
875
+ item_name_for_exception, fork_for_exception = item_with_fork.split('#', 1)
876
+
635
877
  # Check if sources list is empty
636
878
  if not item['sources']:
637
879
  if spec_ref:
638
- # Extract item name and fork from spec_ref for exception checking
639
- if '#' in spec_ref and '.' in spec_ref:
640
- # Format: "functions.item_name#fork"
641
- _, item_with_fork = spec_ref.split('.', 1)
642
- if '#' in item_with_fork:
643
- item_name, fork = item_with_fork.split('#', 1)
644
- # Check if this item is in exceptions
645
- if is_excepted(item_name, fork, exceptions):
646
- total_count += 1
647
- continue
880
+ # Check if this item is in exceptions
881
+ if item_name_for_exception and fork_for_exception:
882
+ if is_excepted(item_name_for_exception, fork_for_exception, exceptions):
883
+ total_count += 1
884
+ continue
648
885
 
649
886
  errors.append(f"EMPTY SOURCES: {spec_ref}")
650
887
  else:
@@ -654,6 +891,13 @@ def check_source_files(yaml_file, project_root, exceptions=None):
654
891
  total_count += 1
655
892
  continue
656
893
 
894
+ # Check if item has non-empty sources but is in exceptions
895
+ if item_name_for_exception and fork_for_exception:
896
+ if is_excepted(item_name_for_exception, fork_for_exception, exceptions):
897
+ errors.append(f"EXCEPTION CONFLICT: {spec_ref} has a specref")
898
+ total_count += 1
899
+ continue
900
+
657
901
  for source in item['sources']:
658
902
  # All sources now use the standardized dict format with file and optional search
659
903
  if not isinstance(source, dict) or 'file' not in source:
@@ -964,7 +1208,7 @@ def process_generated_specrefs(specrefs, exceptions, version):
964
1208
  # Check if singular form exists when we have plural
965
1209
  elif exception_key.endswith('s') and exception_key[:-1] in exceptions:
966
1210
  type_exceptions = exceptions[exception_key[:-1]]
967
-
1211
+
968
1212
  # Special handling for ssz_objects/containers
969
1213
  if spec_type in ['ssz_object', 'container'] and not type_exceptions:
970
1214
  # Check for 'containers' as an alternative key
@@ -1274,3 +1518,172 @@ def run_checks(project_dir, config):
1274
1518
  overall_success = False
1275
1519
 
1276
1520
  return overall_success, results
1521
+
1522
+
1523
+ def generate_specref_files(output_dir, version="nightly", preset="mainnet"):
1524
+ """
1525
+ Generate specref YAML files without sources for manual mapping.
1526
+ Creates a basic directory structure with empty sources.
1527
+ """
1528
+ # Create output directory if it doesn't exist
1529
+ os.makedirs(output_dir, exist_ok=True)
1530
+
1531
+ # Get all spec items
1532
+ pyspec = get_pyspec(version)
1533
+ if preset not in pyspec:
1534
+ raise ValueError(f"Preset '{preset}' not found")
1535
+
1536
+ # Get all forks in chronological order, excluding EIP forks
1537
+ all_forks = sorted(
1538
+ [fork for fork in pyspec[preset].keys() if not fork.startswith("eip")],
1539
+ key=lambda x: (x != "phase0", x)
1540
+ )
1541
+
1542
+ # Map history keys to file names and spec attribute names
1543
+ category_map = {
1544
+ 'constant_vars': ('constants.yml', 'constant_var'),
1545
+ 'config_vars': ('configs.yml', 'config_var'),
1546
+ 'preset_vars': ('presets.yml', 'preset_var'),
1547
+ 'functions': ('functions.yml', 'fn'),
1548
+ 'ssz_objects': ('containers.yml', 'container'),
1549
+ 'dataclasses': ('dataclasses.yml', 'dataclass'),
1550
+ 'custom_types': ('types.yml', 'custom_type'),
1551
+ }
1552
+
1553
+ # Collect all items organized by category
1554
+ items_by_category = {cat: {} for cat in category_map.keys()}
1555
+
1556
+ for fork in all_forks:
1557
+ if fork not in pyspec[preset]:
1558
+ continue
1559
+ fork_data = pyspec[preset][fork]
1560
+
1561
+ for category in items_by_category.keys():
1562
+ if category not in fork_data:
1563
+ continue
1564
+
1565
+ for item_name, item_data in fork_data[category].items():
1566
+ # Track which forks have this item
1567
+ if item_name not in items_by_category[category]:
1568
+ items_by_category[category][item_name] = []
1569
+ items_by_category[category][item_name].append((fork, item_data))
1570
+
1571
+ # Generate YAML files for each category
1572
+ for category, (filename, spec_attr) in category_map.items():
1573
+ if not items_by_category[category]:
1574
+ continue
1575
+
1576
+ output_path = os.path.join(output_dir, filename)
1577
+ entries = []
1578
+
1579
+ # Sort items alphabetically
1580
+ for item_name in sorted(items_by_category[category].keys()):
1581
+ forks_data = items_by_category[category][item_name]
1582
+
1583
+ # Find all unique versions of this item (where content differs)
1584
+ versions = [] # List of (fork, item_data, spec_content)
1585
+ prev_content = None
1586
+
1587
+ for fork, item_data in forks_data:
1588
+ # Build the spec content based on category
1589
+ if category == 'functions':
1590
+ spec_content = item_data
1591
+ elif category in ['constant_vars', 'config_vars', 'preset_vars']:
1592
+ # item_data is a list: [type, value, ...]
1593
+ if isinstance(item_data, (list, tuple)) and len(item_data) >= 2:
1594
+ type_info = item_data[0]
1595
+ value = item_data[1]
1596
+ if type_info:
1597
+ spec_content = f"{item_name}: {type_info} = {value}"
1598
+ else:
1599
+ spec_content = f"{item_name} = {value}"
1600
+ else:
1601
+ spec_content = str(item_data)
1602
+ elif category == 'ssz_objects':
1603
+ spec_content = item_data
1604
+ elif category == 'dataclasses':
1605
+ spec_content = item_data.replace("@dataclass\n", "")
1606
+ elif category == 'custom_types':
1607
+ # custom_types are simple type aliases: TypeName = SomeType
1608
+ spec_content = f"{item_name} = {item_data}"
1609
+ else:
1610
+ spec_content = str(item_data)
1611
+
1612
+ # Only add this version if it's different from the previous one
1613
+ if prev_content is None or spec_content != prev_content:
1614
+ versions.append((fork, item_data, spec_content))
1615
+ prev_content = spec_content
1616
+
1617
+ # Create entries based on number of unique versions
1618
+ use_fork_suffix = len(versions) > 1
1619
+
1620
+ for idx, (fork, item_data, spec_content) in enumerate(versions):
1621
+ # Calculate hash of current version
1622
+ hash_value = hashlib.sha256(spec_content.encode('utf-8')).hexdigest()[:8]
1623
+
1624
+ # For multiple versions after the first, use diff style
1625
+ if use_fork_suffix and idx > 0:
1626
+ # Get previous version for diff
1627
+ prev_fork, _, prev_spec_content = versions[idx - 1]
1628
+
1629
+ # Generate diff
1630
+ diff_content = diff(prev_fork, strip_comments(prev_spec_content), fork, strip_comments(spec_content))
1631
+
1632
+ # Build spec tag with style="diff"
1633
+ spec_tag = f'<spec {spec_attr}="{item_name}" fork="{fork}" style="diff" hash="{hash_value}">'
1634
+ content = diff_content
1635
+ else:
1636
+ # Build spec tag without style="diff"
1637
+ spec_tag = f'<spec {spec_attr}="{item_name}" fork="{fork}" hash="{hash_value}">'
1638
+ content = spec_content
1639
+
1640
+ # Create entry
1641
+ entry_name = f'{item_name}#{fork}' if use_fork_suffix else item_name
1642
+ entry = {
1643
+ 'name': entry_name,
1644
+ 'sources': [],
1645
+ 'spec': f'{spec_tag}\n{content}\n</spec>'
1646
+ }
1647
+ entries.append(entry)
1648
+
1649
+ # Write YAML file
1650
+ if entries:
1651
+ with open(output_path, 'w') as f:
1652
+ for i, entry in enumerate(entries):
1653
+ if i > 0:
1654
+ f.write('\n')
1655
+ f.write(f'- name: {entry["name"]}\n')
1656
+ f.write(' sources: []\n')
1657
+ f.write(' spec: |\n')
1658
+ for line in entry['spec'].split('\n'):
1659
+ f.write(f' {line}\n')
1660
+
1661
+ # Create .ethspecify.yml config file
1662
+ config_path = os.path.join(output_dir, '.ethspecify.yml')
1663
+ with open(config_path, 'w') as f:
1664
+ f.write(f'version: {version}\n')
1665
+ f.write('style: full\n')
1666
+ f.write('\n')
1667
+ f.write('specrefs:\n')
1668
+ f.write(' files:\n')
1669
+ for category, (filename, _) in category_map.items():
1670
+ if items_by_category[category]:
1671
+ f.write(f' - {filename}\n')
1672
+ f.write('\n')
1673
+ f.write(' exceptions:\n')
1674
+ f.write(' # Add any exceptions here\n')
1675
+
1676
+ # Strip trailing whitespace from all generated files
1677
+ all_files = [config_path]
1678
+ for category, (filename, _) in category_map.items():
1679
+ if items_by_category[category]:
1680
+ all_files.append(os.path.join(output_dir, filename))
1681
+
1682
+ for file_path in all_files:
1683
+ with open(file_path, 'r') as f:
1684
+ lines = f.readlines()
1685
+ with open(file_path, 'w') as f:
1686
+ for line in lines:
1687
+ f.write(line.rstrip() + '\n')
1688
+
1689
+ return list(category_map.values())
@@ -0,0 +1,237 @@
1
+ Metadata-Version: 2.4
2
+ Name: ethspecify
3
+ Version: 0.3.1
4
+ Summary: A utility for processing Ethereum specification tags.
5
+ Home-page: https://github.com/jtraglia/ethspecify
6
+ Author: Justin Traglia
7
+ Author-email: jtraglia@pm.me
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.6
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: requests==2.32.3
15
+ Requires-Dist: PyYAML>=6.0
16
+ Dynamic: author
17
+ Dynamic: author-email
18
+ Dynamic: classifier
19
+ Dynamic: description
20
+ Dynamic: description-content-type
21
+ Dynamic: home-page
22
+ Dynamic: license-file
23
+ Dynamic: requires-dist
24
+ Dynamic: requires-python
25
+ Dynamic: summary
26
+
27
+ # ethspecify
28
+
29
+ A tool for referencing the Ethereum specifications in clients.
30
+
31
+ The idea is that ethspecify will help developers keep track of when the specification changes. It
32
+ will also help auditors verify that the client implementations match the specifications. Ideally,
33
+ this is configured as a CI check which notifies client developers when the specification changes.
34
+ When that happens, they can update the implementations appropriately.
35
+
36
+ ## Getting started
37
+
38
+ ### Installation
39
+
40
+ ```
41
+ pipx install ethspecify
42
+ ```
43
+
44
+ ### Initialize specrefs
45
+
46
+ From the root of the client source directory, initialize a directory for specrefs:
47
+
48
+ ```
49
+ $ ethspecify init v1.6.0-beta.0
50
+ Initializing specrefs directory: v1.6.0-beta.0
51
+ Successfully created specrefs directory at: specrefs
52
+ ```
53
+
54
+ This creates a `specrefs` directory with `.ethspecify.yml` and YAML files for each spec category
55
+ (constants, configs, presets, functions, containers, dataclasses, types).
56
+
57
+ ### Map sources
58
+
59
+ Edit the YAML files to add sources for where each spec item is implemented.
60
+
61
+ If it's the entire file:
62
+
63
+ ```yaml
64
+ - name: BlobParameters
65
+ sources:
66
+ - file: ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/fulu/helpers/BlobParameters.java
67
+ spec: |
68
+ <spec dataclass="BlobParameters" fork="fulu" hash="a4575aa8">
69
+ class BlobParameters:
70
+ epoch: Epoch
71
+ max_blobs_per_block: uint64
72
+ </spec>
73
+ ```
74
+
75
+ If it's multiple entire files:
76
+
77
+ ```yaml
78
+ - name: BlobsBundleDeneb
79
+ sources:
80
+ - file: ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/execution/BlobsBundle.java
81
+ - file: ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/builder/BlobsBundleSchema.java
82
+ - file: ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/builder/versions/deneb/BlobsBundleDeneb.java
83
+ - file: ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/builder/versions/deneb/BlobsBundleSchemaDeneb.java
84
+ spec: |
85
+ <spec dataclass="BlobsBundle" fork="deneb" hash="8d6e7be6">
86
+ class BlobsBundle(object):
87
+ commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK]
88
+ proofs: List[KZGProof, MAX_BLOB_COMMITMENTS_PER_BLOCK]
89
+ blobs: List[Blob, MAX_BLOB_COMMITMENTS_PER_BLOCK]
90
+ </spec>
91
+ ```
92
+
93
+ If it's a specific part of a file:
94
+
95
+ ```yaml
96
+ - name: EFFECTIVE_BALANCE_INCREMENT
97
+ sources:
98
+ - file: ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/presets/mainnet/phase0.yaml
99
+ search: "EFFECTIVE_BALANCE_INCREMENT:"
100
+ spec: |
101
+ <spec preset_var="EFFECTIVE_BALANCE_INCREMENT" fork="phase0" hash="23dfe52c">
102
+ EFFECTIVE_BALANCE_INCREMENT: Gwei = 1000000000
103
+ </spec>
104
+ ```
105
+
106
+ You can also use regex in the searches if that is necessary:
107
+
108
+ ```yaml
109
+ - name: ATTESTATION_DUE_BPS
110
+ sources:
111
+ - file: ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/mainnet.yaml
112
+ search: "^ATTESTATION_DUE_BPS:"
113
+ regex: true
114
+ spec: |
115
+ <spec config_var="ATTESTATION_DUE_BPS" fork="phase0" hash="929dd1c9">
116
+ ATTESTATION_DUE_BPS: uint64 = 3333
117
+ </spec>
118
+ ```
119
+
120
+ ### Check specrefs
121
+
122
+ Run the check command in CI to verify all spec items are properly mapped:
123
+
124
+ ```
125
+ $ ethspecify check --path=specrefs
126
+ MISSING: constants.BLS_MODULUS#deneb
127
+ ```
128
+
129
+ ### Add exceptions
130
+
131
+ Some spec items may not be implemented in your client. Add them to the exceptions list in
132
+ `specrefs/.ethspecify.yml`:
133
+
134
+ ```yaml
135
+ specrefs:
136
+ files:
137
+ - containers.yml
138
+ - functions.yml
139
+ # ...
140
+
141
+ exceptions:
142
+ containers:
143
+ # Not defined, unnecessary
144
+ - Eth1Block
145
+
146
+ functions:
147
+ # No light client support
148
+ - is_valid_light_client_header
149
+ - process_light_client_update
150
+ ```
151
+
152
+ ## Style Options
153
+
154
+ This attribute can be used to change how the specification content is shown.
155
+
156
+ ### `hash` (default)
157
+
158
+ This style adds a hash of the specification content to the spec tag, without showing the content.
159
+
160
+ ```
161
+ <spec fn="apply_deposit" fork="electra" hash="c723ce7b" />
162
+ ```
163
+
164
+ > [!NOTE]
165
+ > The hash is the first 8 characters of the specification content's SHA256 digest.
166
+
167
+ ### `full`
168
+
169
+ This style displays the whole content of this specification item, including comments.
170
+
171
+ ```
172
+ <spec fn="is_fully_withdrawable_validator" fork="deneb" style="full">
173
+ def is_fully_withdrawable_validator(validator: Validator, balance: Gwei, epoch: Epoch) -> bool:
174
+ """
175
+ Check if ``validator`` is fully withdrawable.
176
+ """
177
+ return (
178
+ has_eth1_withdrawal_credential(validator)
179
+ and validator.withdrawable_epoch <= epoch
180
+ and balance > 0
181
+ )
182
+ </spec>
183
+ ```
184
+
185
+ ### `link`
186
+
187
+ This style displays a GitHub link to the specification item.
188
+
189
+ ```
190
+ <spec fn="apply_pending_deposit" fork="electra" style="link" hash="83ee9126">
191
+ https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#new-apply_pending_deposit
192
+ </spec>
193
+ ```
194
+
195
+ ### `diff`
196
+
197
+ This style displays a diff with the previous fork's version of the specification.
198
+
199
+ ```
200
+ <spec ssz_object="BeaconState" fork="electra" style="diff">
201
+ --- deneb
202
+ +++ electra
203
+ @@ -27,3 +27,12 @@
204
+ next_withdrawal_index: WithdrawalIndex
205
+ next_withdrawal_validator_index: ValidatorIndex
206
+ historical_summaries: List[HistoricalSummary, HISTORICAL_ROOTS_LIMIT]
207
+ + deposit_requests_start_index: uint64
208
+ + deposit_balance_to_consume: Gwei
209
+ + exit_balance_to_consume: Gwei
210
+ + earliest_exit_epoch: Epoch
211
+ + consolidation_balance_to_consume: Gwei
212
+ + earliest_consolidation_epoch: Epoch
213
+ + pending_deposits: List[PendingDeposit, PENDING_DEPOSITS_LIMIT]
214
+ + pending_partial_withdrawals: List[PendingPartialWithdrawal, PENDING_PARTIAL_WITHDRAWALS_LIMIT]
215
+ + pending_consolidations: List[PendingConsolidation, PENDING_CONSOLIDATIONS_LIMIT]
216
+ </spec>
217
+ ```
218
+
219
+ > [!NOTE]
220
+ > Comments are stripped from the specifications when the `diff` style is used. We do this because
221
+ > these complicate the diff; the "[Modified in Fork]" comments aren't valuable here.
222
+
223
+ This can be used with any specification item, like functions too:
224
+
225
+ ```
226
+ <spec fn="is_eligible_for_activation_queue" fork="electra" style="diff">
227
+ --- phase0
228
+ +++ electra
229
+ @@ -4,5 +4,5 @@
230
+ """
231
+ return (
232
+ validator.activation_eligibility_epoch == FAR_FUTURE_EPOCH
233
+ - and validator.effective_balance == MAX_EFFECTIVE_BALANCE
234
+ + and validator.effective_balance >= MIN_ACTIVATION_BALANCE
235
+ )
236
+ </spec>
237
+ ```
@@ -0,0 +1,9 @@
1
+ ethspecify/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ ethspecify/cli.py,sha256=fbHtny1-VS6IZZlDzPSvkyLr4NDYDYcBXzOiGUGOlyU,10093
3
+ ethspecify/core.py,sha256=gfDBeYzrhNM6wHGtoyy_7SnKUQF_w5iFGljQcdUmaJQ,66694
4
+ ethspecify-0.3.1.dist-info/licenses/LICENSE,sha256=Awxsr73mm9YMBVhBYnzeI7bNdRd-bH6RDtO5ItG0DaM,1071
5
+ ethspecify-0.3.1.dist-info/METADATA,sha256=mI1j8bbLAKBH_dvLHKuP1kgNsdjXnUMcGnS2l2SPh_s,6996
6
+ ethspecify-0.3.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
+ ethspecify-0.3.1.dist-info/entry_points.txt,sha256=09viGkCg9J3h0c9BFRN-BKaJUEaIc4JyULNgBP5EL_g,51
8
+ ethspecify-0.3.1.dist-info/top_level.txt,sha256=0klaMvlVyOkXW09fwZTijJpdybITEp2c9zQKV5v30VM,11
9
+ ethspecify-0.3.1.dist-info/RECORD,,
@@ -1,351 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: ethspecify
3
- Version: 0.2.9
4
- Summary: A utility for processing Ethereum specification tags.
5
- Home-page: https://github.com/jtraglia/ethspecify
6
- Author: Justin Traglia
7
- Author-email: jtraglia@pm.me
8
- Classifier: Programming Language :: Python :: 3
9
- Classifier: License :: OSI Approved :: MIT License
10
- Classifier: Operating System :: OS Independent
11
- Requires-Python: >=3.6
12
- Description-Content-Type: text/markdown
13
- License-File: LICENSE
14
- Requires-Dist: requests==2.32.3
15
- Requires-Dist: PyYAML>=6.0
16
- Dynamic: author
17
- Dynamic: author-email
18
- Dynamic: classifier
19
- Dynamic: description
20
- Dynamic: description-content-type
21
- Dynamic: home-page
22
- Dynamic: license-file
23
- Dynamic: requires-dist
24
- Dynamic: requires-python
25
- Dynamic: summary
26
-
27
- # ethspecify
28
-
29
- A tool for referencing the Ethereum specifications in clients.
30
-
31
- The idea is that ethspecify will help developers keep track of when the specification changes. It
32
- will also help auditors verify that the client implementations match the specifications. Ideally,
33
- this is configured as a CI check which notifies client developers when the specification changes.
34
- When that happens, they can update the implementations appropriately.
35
-
36
- ## Getting Started
37
-
38
- ### Installation
39
-
40
- ```
41
- python3 -mpip install ethspecify
42
- ```
43
-
44
- ### Adding Spec Tags
45
-
46
- In your client, add HTML tags like this:
47
-
48
- ```
49
- /*
50
- * <spec fn="is_fully_withdrawable_validator" fork="deneb" />
51
- */
52
- ```
53
-
54
- ```
55
- /*
56
- * <spec ssz_object="BeaconState" fork="electra" style="diff" />
57
- */
58
- ```
59
-
60
- ### Populating Spec Tags
61
-
62
- Then, navigate to your codebase and run `ethspecify`:
63
-
64
- ```
65
- ethspecify
66
- ```
67
-
68
- ## Specification Options
69
-
70
- ### Version
71
-
72
- This attribute specifies which version of the consensus specifications to use. Default is `nightly`.
73
-
74
- - `nightly` (default) - Uses the latest nightly build from the master branch
75
- - `v1.6.0-alpha.2`, `v1.6.0-alpha.3`, etc. - Uses a specific tagged release version
76
-
77
- Example:
78
- ```
79
- /*
80
- * <spec fn="apply_deposit" fork="electra" version="v1.6.0-alpha.3" />
81
- */
82
- ```
83
-
84
- ### Fork
85
-
86
- This attribute can be any of the [executable
87
- specifications](https://github.com/ethereum/consensus-specs/blob/e6bddd966214a19d2b97199bbe3c02577a22a8b4/Makefile#L3-L15)
88
- in the consensus-specs. At the time of writing, these are: phase0, altair, bellatrix, capella,
89
- deneb, electra, fulu, whisk, eip6800, and eip7732.
90
-
91
- ### Style
92
-
93
- This attribute can be used to change how the specification content is shown.
94
-
95
- #### `hash` (default)
96
-
97
- This style adds a hash of the specification content to the spec tag, without showing the content.
98
-
99
- ```
100
- /*
101
- * <spec fn="apply_deposit" fork="electra" hash="c723ce7b" />
102
- */
103
- ```
104
-
105
- > [!NOTE]
106
- > The hash is the first 8 characters of the specification content's SHA256 digest.
107
-
108
- #### `full`
109
-
110
- This style displays the whole content of this specification item, including comments.
111
-
112
- ```
113
- /*
114
- * <spec fn="is_fully_withdrawable_validator" fork="deneb" style="full">
115
- * def is_fully_withdrawable_validator(validator: Validator, balance: Gwei, epoch: Epoch) -> bool:
116
- * """
117
- * Check if ``validator`` is fully withdrawable.
118
- * """
119
- * return (
120
- * has_eth1_withdrawal_credential(validator)
121
- * and validator.withdrawable_epoch <= epoch
122
- * and balance > 0
123
- * )
124
- * </spec>
125
- */
126
- ```
127
-
128
- #### `link`
129
-
130
- This style displays a GitHub link to the specification item.
131
-
132
- ```
133
- /*
134
- * <spec fn="apply_pending_deposit" fork="electra" style="link" hash="83ee9126">
135
- * https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#new-apply_pending_deposit
136
- * </spec>
137
- */
138
- ```
139
-
140
- #### `diff`
141
-
142
- This style displays a diff with the previous fork's version of the specification.
143
-
144
- ```
145
- /*
146
- * <spec ssz_object="BeaconState" fork="electra" style="diff">
147
- * --- deneb
148
- * +++ electra
149
- * @@ -27,3 +27,12 @@
150
- * next_withdrawal_index: WithdrawalIndex
151
- * next_withdrawal_validator_index: ValidatorIndex
152
- * historical_summaries: List[HistoricalSummary, HISTORICAL_ROOTS_LIMIT]
153
- * + deposit_requests_start_index: uint64
154
- * + deposit_balance_to_consume: Gwei
155
- * + exit_balance_to_consume: Gwei
156
- * + earliest_exit_epoch: Epoch
157
- * + consolidation_balance_to_consume: Gwei
158
- * + earliest_consolidation_epoch: Epoch
159
- * + pending_deposits: List[PendingDeposit, PENDING_DEPOSITS_LIMIT]
160
- * + pending_partial_withdrawals: List[PendingPartialWithdrawal, PENDING_PARTIAL_WITHDRAWALS_LIMIT]
161
- * + pending_consolidations: List[PendingConsolidation, PENDING_CONSOLIDATIONS_LIMIT]
162
- * </spec>
163
- */
164
- ```
165
-
166
- > [!NOTE]
167
- > Comments are stripped from the specifications when the `diff` style is used. We do this because
168
- > these complicate the diff; the "[Modified in Fork]" comments aren't valuable here.
169
-
170
- This can be used with any specification item, like functions too:
171
-
172
- ```
173
- /*
174
- * <spec fn="is_eligible_for_activation_queue" fork="electra" style="diff">
175
- * --- phase0
176
- * +++ electra
177
- * @@ -4,5 +4,5 @@
178
- * """
179
- * return (
180
- * validator.activation_eligibility_epoch == FAR_FUTURE_EPOCH
181
- * - and validator.effective_balance == MAX_EFFECTIVE_BALANCE
182
- * + and validator.effective_balance >= MIN_ACTIVATION_BALANCE
183
- * )
184
- * </spec>
185
- */
186
- ```
187
-
188
- ## Supported Specification Items
189
-
190
- ### Constants
191
-
192
- These are items found in the `Constants` section of the specifications.
193
-
194
- ```
195
- /*
196
- * <spec constant_var="COMPOUNDING_WITHDRAWAL_PREFIX" fork="electra" style="full">
197
- * COMPOUNDING_WITHDRAWAL_PREFIX: Bytes1 = '0x02'
198
- * </spec>
199
- */
200
- ```
201
-
202
- ### Custom Types
203
-
204
- These are items found in the `Custom types` section of the specifications.
205
-
206
- ```
207
- /*
208
- * <spec custom_type="Blob" fork="electra" style="full">
209
- * Blob = ByteVector[BYTES_PER_FIELD_ELEMENT * FIELD_ELEMENTS_PER_BLOB]
210
- * </spec>
211
- */
212
- ```
213
-
214
- ### Preset Variables
215
-
216
- These are items found in the
217
- [`presets`](https://github.com/ethereum/consensus-specs/tree/dev/presets) directory.
218
-
219
- For preset variables, in addition to the `preset_var` attribute, you can specify a `preset`
220
- attribute: minimal or mainnet.
221
-
222
- ```
223
- /*
224
- * <spec preset="minimal" preset_var="PENDING_CONSOLIDATIONS_LIMIT" fork="electra" style="full">
225
- * PENDING_CONSOLIDATIONS_LIMIT: uint64 = 64
226
- * </spec>
227
- *
228
- * <spec preset="mainnet" preset_var="PENDING_CONSOLIDATIONS_LIMIT" fork="electra" style="full">
229
- * PENDING_CONSOLIDATIONS_LIMIT: uint64 = 262144
230
- * </spec>
231
- */
232
- ```
233
-
234
- It's not strictly necessary to specify the preset attribute. The default preset is mainnet.
235
-
236
- ```
237
- /*
238
- * <spec preset_var="FIELD_ELEMENTS_PER_BLOB" fork="electra" style="full">
239
- * FIELD_ELEMENTS_PER_BLOB: uint64 = 4096
240
- * </spec>
241
- */
242
- ```
243
-
244
- ### Config Variables
245
-
246
- These are items found in the
247
- [`configs`](https://github.com/ethereum/consensus-specs/tree/dev/presets) directory.
248
-
249
- ```
250
- /*
251
- * <spec config_var="MAX_REQUEST_BLOB_SIDECARS" fork="electra" style="full">
252
- * MAX_REQUEST_BLOB_SIDECARS = 768
253
- * </spec>
254
- */
255
- ```
256
-
257
- ### SSZ Objects
258
-
259
- These are items found in the `Containers` section of the specifications.
260
-
261
- ```
262
- /*
263
- * <spec ssz_object="ConsolidationRequest" fork="electra" style="full">
264
- * class ConsolidationRequest(Container):
265
- * source_address: ExecutionAddress
266
- * source_pubkey: BLSPubkey
267
- * target_pubkey: BLSPubkey
268
- * </spec>
269
- */
270
- ```
271
-
272
- ### Dataclasses
273
-
274
- These are classes with the `@dataclass` decorator.
275
-
276
- ```
277
- /*
278
- * <spec dataclass="PayloadAttributes" fork="electra" style="full">
279
- * class PayloadAttributes(object):
280
- * timestamp: uint64
281
- * prev_randao: Bytes32
282
- * suggested_fee_recipient: ExecutionAddress
283
- * withdrawals: Sequence[Withdrawal]
284
- * parent_beacon_block_root: Root # [New in Deneb:EIP4788]
285
- * </spec>
286
- */
287
- ```
288
-
289
- ### Functions
290
-
291
- These are all the functions found in the specifications.
292
-
293
- For example, two versions of the same function:
294
-
295
- ```
296
- /*
297
- * <spec fn="is_fully_withdrawable_validator" fork="deneb" style="full">
298
- * def is_fully_withdrawable_validator(validator: Validator, balance: Gwei, epoch: Epoch) -> bool:
299
- * """
300
- * Check if ``validator`` is fully withdrawable.
301
- * """
302
- * return (
303
- * has_eth1_withdrawal_credential(validator)
304
- * and validator.withdrawable_epoch <= epoch
305
- * and balance > 0
306
- * )
307
- * </spec>
308
- */
309
- ```
310
-
311
- ```
312
- /*
313
- * <spec fn="is_fully_withdrawable_validator" fork="electra" style="full">
314
- * def is_fully_withdrawable_validator(validator: Validator, balance: Gwei, epoch: Epoch) -> bool:
315
- * """
316
- * Check if ``validator`` is fully withdrawable.
317
- * """
318
- * return (
319
- * has_execution_withdrawal_credential(validator) # [Modified in Electra:EIP7251]
320
- * and validator.withdrawable_epoch <= epoch
321
- * and balance > 0
322
- * )
323
- * </spec>
324
- */
325
- ```
326
-
327
- With functions, it's possible to specify which line/lines should be displayed. For example:
328
-
329
- ```
330
- /*
331
- * <spec fn="is_fully_withdrawable_validator" fork="electra" style="full" lines="5-9">
332
- * return (
333
- * has_execution_withdrawal_credential(validator) # [Modified in Electra:EIP7251]
334
- * and validator.withdrawable_epoch <= epoch
335
- * and balance > 0
336
- * )
337
- * </spec>
338
- */
339
- ```
340
-
341
- Note that the content is automatically dedented.
342
-
343
- Or, to display just a single line, only specify a single number. For example:
344
-
345
- ```
346
- /*
347
- * <spec fn="is_fully_withdrawable_validator" fork="electra" style="full" lines="6">
348
- * has_execution_withdrawal_credential(validator) # [Modified in Electra:EIP7251]
349
- * </spec>
350
- */
351
- ```
@@ -1,9 +0,0 @@
1
- ethspecify/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- ethspecify/cli.py,sha256=Ida4n7BSjZrkvwINyzh3M61JzFqdLPc93i62JIQ_64g,8183
3
- ethspecify/core.py,sha256=GwXLoCWKJ-BTyjkVP58eR1Gay2bGegdH6RvAaqLUwas,48832
4
- ethspecify-0.2.9.dist-info/licenses/LICENSE,sha256=Awxsr73mm9YMBVhBYnzeI7bNdRd-bH6RDtO5ItG0DaM,1071
5
- ethspecify-0.2.9.dist-info/METADATA,sha256=AhTvvhzxBaGj3gjs1dvsubn0YFTIznMIUWP5cF9-anA,9212
6
- ethspecify-0.2.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
- ethspecify-0.2.9.dist-info/entry_points.txt,sha256=09viGkCg9J3h0c9BFRN-BKaJUEaIc4JyULNgBP5EL_g,51
8
- ethspecify-0.2.9.dist-info/top_level.txt,sha256=0klaMvlVyOkXW09fwZTijJpdybITEp2c9zQKV5v30VM,11
9
- ethspecify-0.2.9.dist-info/RECORD,,