ethspecify 0.2.9__tar.gz → 0.3.0__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.2.9
3
+ Version: 0.3.0
4
4
  Summary: A utility for processing Ethereum specification tags.
5
5
  Home-page: https://github.com/jtraglia/ethspecify
6
6
  Author: Justin Traglia
@@ -38,7 +38,7 @@ When that happens, they can update the implementations appropriately.
38
38
  ### Installation
39
39
 
40
40
  ```
41
- python3 -mpip install ethspecify
41
+ pipx install ethspecify
42
42
  ```
43
43
 
44
44
  ### Adding Spec Tags
@@ -12,7 +12,7 @@ When that happens, they can update the implementations appropriately.
12
12
  ### Installation
13
13
 
14
14
  ```
15
- python3 -mpip install ethspecify
15
+ pipx install ethspecify
16
16
  ```
17
17
 
18
18
  ### Adding Spec Tags
@@ -322,4 +322,4 @@ Or, to display just a single line, only specify a single number. For example:
322
322
  * has_execution_withdrawal_credential(validator) # [Modified in Electra:EIP7251]
323
323
  * </spec>
324
324
  */
325
- ```
325
+ ```
@@ -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
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
 
@@ -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()
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ethspecify
3
- Version: 0.2.9
3
+ Version: 0.3.0
4
4
  Summary: A utility for processing Ethereum specification tags.
5
5
  Home-page: https://github.com/jtraglia/ethspecify
6
6
  Author: Justin Traglia
@@ -38,7 +38,7 @@ When that happens, they can update the implementations appropriately.
38
38
  ### Installation
39
39
 
40
40
  ```
41
- python3 -mpip install ethspecify
41
+ pipx install ethspecify
42
42
  ```
43
43
 
44
44
  ### Adding Spec Tags
@@ -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.2.9",
11
+ version="0.3.0",
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