#!/bin/bash # Fission Docstring Updater # Parses and updates embedded fission configuration in Python function docstrings set -euo pipefail usage() { echo "Usage: $0 [function-name] [--set \"\"] [--get] [--help]" echo "" echo "Arguments:" echo " file-path: Path to the Python file containing the function" echo " function-name: Optional specific function name to target (if not provided, processes all functions with fission configuration)" echo "" echo "Options:" echo " --set : Set the fission configuration to the provided JSON string" echo " --get: Get/display the current fission configuration (default action)" echo " --help: Show this help message" echo "" echo "Examples:" echo " $0 ./src/my_function.py main --get" echo " $0 ./src/my_function.py main --set '{\"name\": \"updated-function\"}'" echo " $0 ./src/functions.py --get" exit 1 } # Check dependencies if ! command -v python3 &> /dev/null; then echo "Error: python3 is required but not found" exit 1 fi # Parse arguments if [[ $# -lt 1 ]]; then usage fi FILE_PATH="$1" shift FUNCTION_NAME="" ACTION="get" SET_VALUE="" while [[ $# -gt 0 ]]; do case $1 in --set) SET_VALUE="$2" ACTION="set" shift 2 ;; --get) ACTION="get" shift ;; --help) usage ;; *) if [[ -z "$FUNCTION_NAME" ]]; then FUNCTION_NAME="$1" else echo "Error: Unexpected argument '$1'" usage fi shift ;; esac done # Validate file exists if [[ ! -f "$FILE_PATH" ]]; then echo "Error: File '$FILE_PATH' does not exist" exit 1 fi # Validate JSON if setting if [[ "$ACTION" == "set" && -z "$SET_VALUE" ]]; then echo "Error: --set requires a JSON value" exit 1 fi if [[ "$ACTION" == "set" ]]; then # Validate JSON format if ! echo "$SET_VALUE" | python3 -m json.tool >/dev/null 2>&1; then echo "Error: Invalid JSON provided for --set" exit 1 fi fi # Python script to handle the docstring parsing and updating PYTHON_SCRIPT=$(cat << 'EOF' import re import sys import json import os from pathlib import Path def extract_functions_with_fission(content): """Extract all functions that have fission configuration in their docstrings.""" # Pattern to match Python functions with docstrings containing fission configuration # This looks for def function_name(): followed by a docstring that contains ```fission pattern = r'(\s*def\s+(\w+)\s*\([^)]*\):\s*(?:\n\s*)?\"\"\"[\s\S]*?```fission[\s\S]*?```[\s\S]*?\"\"\"[\s\S]*?)(?=\n\s*def|\n\s*class|\Z)' matches = re.finditer(pattern, content, re.MULTILINE) functions = [] for match in matches: full_match = match.group(1) # Extract function name from the match func_name_match = re.search(r'def\s+(\w+)\s*\(', full_match) if func_name_match: func_name = func_name_match.group(1) functions.append({ 'name': func_name, 'full_text': full_match, 'start_pos': match.start(), 'end_pos': match.end() }) return functions def extract_fission_config(docstring): """Extract fission configuration from a docstring.""" # Look for ```fission ... ``` blocks pattern = r'```fission\s*([\s\S]*?)\s*```' match = re.search(pattern, docstring) if match: config_text = match.group(1).strip() try: return json.loads(config_text) except json.JSONDecodeError as e: return None return None def replace_fission_config_in_docstring(docstring, new_config): """Replace fission configuration in a docstring with new config.""" # Format the new config as JSON with indentation formatted_config = json.dumps(new_config, indent=4) # Replace the ```fission ... ``` block pattern = r'(```fission\s*)[\s\S]*?(\s*```)' replacement = r'\1' + formatted_config + r'\2' return re.sub(pattern, replacement, docstring, flags=re.DOTALL) def process_file(file_path, target_function=None, action='get', set_value=None): """Process the Python file to get or set fission configuration.""" try: with open(file_path, 'r') as f: content = f.read() except IOError as e: print(f"Error: Cannot read file '{file_path}': {e}", file=sys.stderr) sys.exit(1) functions = extract_functions_with_fission(content) if not functions: print("No functions with fission configuration found in file.", file=sys.stderr) if action == 'get': sys.exit(0) else: sys.exit(1) # Filter by function name if specified if target_function: functions = [f for f in functions if f['name'] == target_function] if not functions: print(f"Error: Function '{target_function}' with fission configuration not found.", file=sys.stderr) sys.exit(1) if action == 'get': # Display current configuration for each function for func in functions: # Extract docstring from the function text docstring_match = re.search(r'\"\"\"[\s\S]*?\"\"\"', func['full_text']) if docstring_match: docstring = docstring_match.group(0) config = extract_fission_config(docstring) if config is not None: if len(functions) == 1: print(json.dumps(config, indent=2)) else: print(f"Function '{func['name']}':") print(json.dumps(config, indent=2)) print() else: if len(functions) == 1: print("No fission configuration found in function docstring.", file=sys.stderr) else: print(f"Function '{func['name']}': No fission configuration found in docstring.", file=sys.stderr) else: if len(functions) == 1: print("Could not extract docstring from function.", file=sys.stderr) else: print(f"Function '{func['name']}': Could not extract docstring.", file=sys.stderr) elif action == 'set': if set_value is None: print("Error: No value provided for --set", file=sys.stderr) sys.exit(1) try: new_config = json.loads(set_value) except json.JSONDecodeError as e: print(f"Error: Invalid JSON provided for --set: {e}", file=sys.stderr) sys.exit(1) # Update each function updated_content = content offset = 0 # Track position changes due to replacements for func in functions: # Extract docstring from the function text docstring_match = re.search(r'\"\"\"[\s\S]*?\"\"\"', func['full_text']) if docstring_match: docstring = docstring_match.group(0) # Check if fission configuration exists if extract_fission_config(docstring) is not None: # Replace fission configuration in docstring new_docstring = replace_fission_config_in_docstring(docstring, new_config) # Replace the docstring in the function text new_func_text = func['full_text'].replace(docstring, new_docstring, 1) # Replace in the overall content (adjusting for previous changes) start_pos = func['start_pos'] + offset end_pos = func['end_pos'] + offset # Update content with the change before = updated_content[:start_pos] after = updated_content[end_pos:] updated_content = before + new_func_text + after # Update offset for next replacements offset += len(new_func_text) - len(func['full_text']) else: print(f"Warning: No fission configuration found in function '{func['name']}' to update.", file=sys.stderr) else: print(f"Warning: Could not extract docstring from function '{func['name']}'.", file=sys.stderr) # Write back to file try: with open(file_path, 'w') as f: f.write(updated_content) if len(functions) == 1: print(f"Updated fission configuration in function '{functions[0]['name']}'.") else: print(f"Updated fission configuration in {len(functions)} function(s).") except IOError as e: print(f"Error: Cannot write to file '{file_path}': {e}", file=sys.stderr) sys.exit(1) if __name__ == '__main__': if len(sys.argv) < 2: print("Usage: fission-docstring-updater [function-name] [--set \"\"] [--get]", file=sys.stderr) sys.exit(1) file_path = sys.argv[1] target_function = sys.argv[2] if len(sys.argv) > 2 and not sys.argv[2].startswith('--') else None # Parse arguments action = 'get' set_value = None i = 3 if target_function else 2 while i < len(sys.argv): if sys.argv[i] == '--set' and i + 1 < len(sys.argv): action = 'set' set_value = sys.argv[i + 1] i += 2 elif sys.argv[i] == '--get': action = 'get' i += 1 else: print(f"Error: Unknown argument '{sys.argv[i]}'", file=sys.stderr) sys.exit(1) process_file(file_path, target_function, action, set_value) EOF ) # Build arguments for Python script PYTHON_ARGS=("$FILE_PATH") if [[ -n "$FUNCTION_NAME" ]]; then PYTHON_ARGS+=("$FUNCTION_NAME") fi if [[ "$ACTION" == "set" ]]; then PYTHON_ARGS+=("--set" "$SET_VALUE") else PYTHON_ARGS+=("--get") fi # Execute the Python script python3 -c "$PYTHON_SCRIPT" "${PYTHON_ARGS[@]}"