ethspecify 0.3.1__tar.gz → 0.3.2__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ethspecify
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: A utility for processing Ethereum specification tags.
5
5
  Home-page: https://github.com/jtraglia/ethspecify
6
6
  Author: Justin Traglia
@@ -3,7 +3,7 @@ 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, sort_specref_yaml, generate_specref_files
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, get_yaml_filename_for_spec_attr, get_spec_attr_and_name, add_missing_entries_to_yaml
7
7
 
8
8
 
9
9
  def process(args):
@@ -16,20 +16,37 @@ 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
20
- for f in grep(project_dir, r"<spec\b.*?>", args.exclude):
21
- print(f"Processing file: {f}")
22
- replace_spec_tags(f, config)
23
-
24
- # Sort specref YAML files if they exist in config
19
+ # Check if auto_add_missing_entries and auto_standardize_names are enabled
25
20
  specrefs_config = config.get('specrefs', {})
26
21
  if isinstance(specrefs_config, dict):
22
+ auto_add_missing = specrefs_config.get('auto_add_missing_entries', False)
23
+ auto_standardize_names = specrefs_config.get('auto_standardize_names', False)
27
24
  specrefs_files = specrefs_config.get('files', [])
28
25
  elif isinstance(specrefs_config, list):
26
+ auto_add_missing = False
27
+ auto_standardize_names = False
29
28
  specrefs_files = specrefs_config
30
29
  else:
30
+ auto_add_missing = False
31
+ auto_standardize_names = False
31
32
  specrefs_files = []
32
33
 
34
+ # Process spec tags in files
35
+ for f in grep(project_dir, r"<spec\b.*?>", args.exclude):
36
+ print(f"Processing file: {f}")
37
+ replace_spec_tags(f, config)
38
+
39
+ # Add missing spec items to YAML files if enabled
40
+ if auto_add_missing:
41
+ from .core import add_missing_spec_items_to_yaml_files
42
+ add_missing_spec_items_to_yaml_files(project_dir, config, specrefs_files)
43
+
44
+ # Update entry names to <spec_item>#<fork> format if enabled
45
+ if auto_standardize_names:
46
+ from .core import update_entry_names_in_yaml_files
47
+ update_entry_names_in_yaml_files(project_dir, specrefs_files)
48
+
49
+ # Sort specref YAML files if they exist in config
33
50
  for yaml_file in specrefs_files:
34
51
  yaml_path = os.path.join(project_dir, yaml_file)
35
52
  if os.path.exists(yaml_path):
@@ -89,7 +89,8 @@ def validate_exception_items(exceptions, version):
89
89
  break
90
90
 
91
91
  if not item_found:
92
- errors.append(f"invalid key: {exception_key}.{item_name}{"#" + fork if fork else ""}")
92
+ fork_suffix = f"#{fork}" if fork else ""
93
+ errors.append(f"invalid key: {exception_key}.{item_name}{fork_suffix}")
93
94
 
94
95
  if errors:
95
96
  error_msg = "Invalid exception items in configuration:\n" + "\n".join(f" - {e}" for e in errors)
@@ -724,6 +725,9 @@ def replace_spec_tags(file_path, config=None):
724
725
  re.DOTALL
725
726
  )
726
727
 
728
+ # Collect processed spec items for potential YAML updates
729
+ processed_items = []
730
+
727
731
  def rebuild_opening_tag(attributes, hash_value):
728
732
  # Rebuild a fresh opening tag from attributes, overriding any existing hash.
729
733
  new_opening = "<spec"
@@ -762,6 +766,17 @@ def replace_spec_tags(file_path, config=None):
762
766
  spec = get_spec(attributes, preset, fork, version)
763
767
  hash_value = hashlib.sha256(spec.encode('utf-8')).hexdigest()[:8]
764
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
+
765
780
  if style == "hash":
766
781
  # Rebuild a fresh self-closing tag.
767
782
  updated_tag = rebuild_self_closing_tag(attributes, hash_value)
@@ -786,6 +801,151 @@ def replace_spec_tags(file_path, config=None):
786
801
  with open(file_path, 'w') as file:
787
802
  file.write(updated_content)
788
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
+
789
949
 
790
950
  def check_source_files(yaml_file, project_root, exceptions=None):
791
951
  """
@@ -1520,6 +1680,209 @@ def run_checks(project_dir, config):
1520
1680
  return overall_success, results
1521
1681
 
1522
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
+
1523
1886
  def generate_specref_files(output_dir, version="nightly", preset="mainnet"):
1524
1887
  """
1525
1888
  Generate specref YAML files without sources for manual mapping.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ethspecify
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: A utility for processing Ethereum specification tags.
5
5
  Home-page: https://github.com/jtraglia/ethspecify
6
6
  Author: Justin Traglia
@@ -8,7 +8,7 @@ long_description = (this_directory / "README.md").read_text(encoding="utf-8")
8
8
 
9
9
  setup(
10
10
  name="ethspecify",
11
- version="0.3.1",
11
+ version="0.3.2",
12
12
  description="A utility for processing Ethereum specification tags.",
13
13
  long_description=long_description,
14
14
  long_description_content_type="text/markdown",
File without changes
File without changes
File without changes