Initial commit
This commit is contained in:
208
skills/fire-patterns/scripts/extract-commands.py
Executable file
208
skills/fire-patterns/scripts/extract-commands.py
Executable file
@@ -0,0 +1,208 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Extract command structure from Fire CLI for documentation"""
|
||||
|
||||
import ast
|
||||
import sys
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
|
||||
class CommandExtractor:
|
||||
"""Extract command structure from Fire CLI Python files"""
|
||||
|
||||
def __init__(self, filepath: Path):
|
||||
self.filepath = filepath
|
||||
self.tree = None
|
||||
self.commands = []
|
||||
|
||||
def extract(self) -> List[Dict]:
|
||||
"""Extract all commands from Fire CLI"""
|
||||
try:
|
||||
content = self.filepath.read_text()
|
||||
self.tree = ast.parse(content)
|
||||
except Exception as e:
|
||||
print(f"Error parsing file: {e}", file=sys.stderr)
|
||||
return []
|
||||
|
||||
# Find all classes
|
||||
for node in self.tree.body:
|
||||
if isinstance(node, ast.ClassDef):
|
||||
self._extract_from_class(node)
|
||||
|
||||
return self.commands
|
||||
|
||||
def _extract_from_class(self, class_node: ast.ClassDef, parent_path: str = ""):
|
||||
"""Extract commands from a class"""
|
||||
class_name = class_node.name
|
||||
class_doc = ast.get_docstring(class_node) or ""
|
||||
|
||||
current_path = f"{parent_path}.{class_name}" if parent_path else class_name
|
||||
|
||||
for item in class_node.body:
|
||||
if isinstance(item, ast.FunctionDef):
|
||||
# Skip private methods
|
||||
if item.name.startswith('_'):
|
||||
continue
|
||||
|
||||
command = self._extract_command(item, current_path)
|
||||
if command:
|
||||
self.commands.append(command)
|
||||
|
||||
elif isinstance(item, ast.ClassDef):
|
||||
# Nested class - recurse
|
||||
self._extract_from_class(item, current_path)
|
||||
|
||||
def _extract_command(self, func_node: ast.FunctionDef, class_path: str) -> Optional[Dict]:
|
||||
"""Extract command information from function"""
|
||||
func_name = func_node.name
|
||||
docstring = ast.get_docstring(func_node) or ""
|
||||
|
||||
# Parse docstring for description and args
|
||||
description = ""
|
||||
args_help = {}
|
||||
|
||||
if docstring:
|
||||
lines = docstring.split('\n')
|
||||
desc_lines = []
|
||||
in_args = False
|
||||
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if line.startswith('Args:'):
|
||||
in_args = True
|
||||
continue
|
||||
elif line.startswith('Returns:') or line.startswith('Raises:'):
|
||||
in_args = False
|
||||
continue
|
||||
|
||||
if not in_args and line:
|
||||
desc_lines.append(line)
|
||||
elif in_args and line:
|
||||
# Parse arg line: "arg_name: description"
|
||||
if ':' in line:
|
||||
arg_name, arg_desc = line.split(':', 1)
|
||||
args_help[arg_name.strip()] = arg_desc.strip()
|
||||
|
||||
description = ' '.join(desc_lines)
|
||||
|
||||
# Extract arguments
|
||||
args = []
|
||||
for arg in func_node.args.args:
|
||||
if arg.arg == 'self':
|
||||
continue
|
||||
|
||||
arg_info = {
|
||||
'name': arg.arg,
|
||||
'type': self._get_type_annotation(arg),
|
||||
'help': args_help.get(arg.arg, ''),
|
||||
}
|
||||
|
||||
# Check for default value
|
||||
defaults_offset = len(func_node.args.args) - len(func_node.args.defaults)
|
||||
arg_index = func_node.args.args.index(arg)
|
||||
if arg_index >= defaults_offset:
|
||||
default_index = arg_index - defaults_offset
|
||||
default_value = self._get_default_value(func_node.args.defaults[default_index])
|
||||
arg_info['default'] = default_value
|
||||
arg_info['required'] = False
|
||||
else:
|
||||
arg_info['required'] = True
|
||||
|
||||
args.append(arg_info)
|
||||
|
||||
return {
|
||||
'name': func_name,
|
||||
'path': f"{class_path}.{func_name}",
|
||||
'description': description,
|
||||
'arguments': args
|
||||
}
|
||||
|
||||
def _get_type_annotation(self, arg: ast.arg) -> Optional[str]:
|
||||
"""Extract type annotation from argument"""
|
||||
if arg.annotation:
|
||||
return ast.unparse(arg.annotation)
|
||||
return None
|
||||
|
||||
def _get_default_value(self, node) -> str:
|
||||
"""Extract default value from AST node"""
|
||||
try:
|
||||
return ast.unparse(node)
|
||||
except:
|
||||
return repr(node)
|
||||
|
||||
def print_tree(self):
|
||||
"""Print command tree in human-readable format"""
|
||||
print(f"\n{'='*60}")
|
||||
print(f"Fire CLI Commands: {self.filepath.name}")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
for cmd in self.commands:
|
||||
print(f"📌 {cmd['path']}")
|
||||
if cmd['description']:
|
||||
print(f" {cmd['description']}")
|
||||
if cmd['arguments']:
|
||||
print(f" Arguments:")
|
||||
for arg in cmd['arguments']:
|
||||
required = "required" if arg['required'] else "optional"
|
||||
type_str = f": {arg['type']}" if arg['type'] else ""
|
||||
default_str = f" = {arg['default']}" if 'default' in arg else ""
|
||||
help_str = f" - {arg['help']}" if arg['help'] else ""
|
||||
print(f" • {arg['name']}{type_str}{default_str} ({required}){help_str}")
|
||||
print()
|
||||
|
||||
def export_json(self) -> str:
|
||||
"""Export commands as JSON"""
|
||||
return json.dumps(self.commands, indent=2)
|
||||
|
||||
def export_markdown(self) -> str:
|
||||
"""Export commands as Markdown"""
|
||||
lines = [f"# {self.filepath.name} Commands\n"]
|
||||
|
||||
for cmd in self.commands:
|
||||
lines.append(f"## `{cmd['path']}`\n")
|
||||
|
||||
if cmd['description']:
|
||||
lines.append(f"{cmd['description']}\n")
|
||||
|
||||
if cmd['arguments']:
|
||||
lines.append("### Arguments\n")
|
||||
for arg in cmd['arguments']:
|
||||
required = "**required**" if arg['required'] else "*optional*"
|
||||
type_str = f" (`{arg['type']}`)" if arg['type'] else ""
|
||||
default_str = f" Default: `{arg['default']}`" if 'default' in arg else ""
|
||||
|
||||
lines.append(f"- `{arg['name']}`{type_str} - {required}{default_str}")
|
||||
if arg['help']:
|
||||
lines.append(f" - {arg['help']}")
|
||||
|
||||
lines.append("")
|
||||
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: extract-commands.py <fire-cli-file.py> [--json|--markdown]")
|
||||
sys.exit(1)
|
||||
|
||||
filepath = Path(sys.argv[1])
|
||||
output_format = sys.argv[2] if len(sys.argv) > 2 else '--tree'
|
||||
|
||||
if not filepath.exists():
|
||||
print(f"Error: File not found: {filepath}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
extractor = CommandExtractor(filepath)
|
||||
extractor.extract()
|
||||
|
||||
if output_format == '--json':
|
||||
print(extractor.export_json())
|
||||
elif output_format == '--markdown':
|
||||
print(extractor.export_markdown())
|
||||
else:
|
||||
extractor.print_tree()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
179
skills/fire-patterns/scripts/generate-fire-cli.sh
Executable file
179
skills/fire-patterns/scripts/generate-fire-cli.sh
Executable file
@@ -0,0 +1,179 @@
|
||||
#!/bin/bash
|
||||
# Generate Fire CLI from specification
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
TEMPLATES_DIR="$(dirname "$SCRIPT_DIR")/templates"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Function to display usage
|
||||
usage() {
|
||||
cat << EOF
|
||||
Usage: $(basename "$0") [OPTIONS]
|
||||
|
||||
Generate a Python Fire CLI application from templates.
|
||||
|
||||
OPTIONS:
|
||||
-n, --name NAME CLI name (required)
|
||||
-d, --description DESC CLI description (required)
|
||||
-t, --template TYPE Template type: basic, nested, rich, typed, config, multi (default: basic)
|
||||
-o, --output FILE Output file path (required)
|
||||
-c, --class-name NAME Main class name (default: CLI)
|
||||
-v, --version VERSION Version number (default: 1.0.0)
|
||||
-h, --help Show this help message
|
||||
|
||||
TEMPLATE TYPES:
|
||||
basic - Simple single-class Fire CLI
|
||||
nested - Multi-class CLI with command groups
|
||||
rich - Fire CLI with rich console output
|
||||
typed - Type-annotated Fire CLI with full type hints
|
||||
config - Fire CLI with comprehensive configuration management
|
||||
multi - Complex multi-command Fire CLI
|
||||
|
||||
EXAMPLES:
|
||||
$(basename "$0") -n mycli -d "My CLI tool" -o mycli.py
|
||||
$(basename "$0") -n deploy-tool -d "Deployment CLI" -t nested -o deploy.py
|
||||
$(basename "$0") -n mytool -d "Advanced tool" -t typed -c MyTool -o tool.py
|
||||
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Parse command line arguments
|
||||
CLI_NAME=""
|
||||
DESCRIPTION=""
|
||||
TEMPLATE="basic"
|
||||
OUTPUT_FILE=""
|
||||
CLASS_NAME="CLI"
|
||||
VERSION="1.0.0"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-n|--name)
|
||||
CLI_NAME="$2"
|
||||
shift 2
|
||||
;;
|
||||
-d|--description)
|
||||
DESCRIPTION="$2"
|
||||
shift 2
|
||||
;;
|
||||
-t|--template)
|
||||
TEMPLATE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-o|--output)
|
||||
OUTPUT_FILE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-c|--class-name)
|
||||
CLASS_NAME="$2"
|
||||
shift 2
|
||||
;;
|
||||
-v|--version)
|
||||
VERSION="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}Error: Unknown option $1${NC}"
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Validate required arguments
|
||||
if [[ -z "$CLI_NAME" ]]; then
|
||||
echo -e "${RED}Error: CLI name is required${NC}"
|
||||
usage
|
||||
fi
|
||||
|
||||
if [[ -z "$DESCRIPTION" ]]; then
|
||||
echo -e "${RED}Error: Description is required${NC}"
|
||||
usage
|
||||
fi
|
||||
|
||||
if [[ -z "$OUTPUT_FILE" ]]; then
|
||||
echo -e "${RED}Error: Output file is required${NC}"
|
||||
usage
|
||||
fi
|
||||
|
||||
# Validate template type
|
||||
TEMPLATE_FILE=""
|
||||
case $TEMPLATE in
|
||||
basic)
|
||||
TEMPLATE_FILE="$TEMPLATES_DIR/basic-fire-cli.py.template"
|
||||
;;
|
||||
nested)
|
||||
TEMPLATE_FILE="$TEMPLATES_DIR/nested-fire-cli.py.template"
|
||||
;;
|
||||
rich)
|
||||
TEMPLATE_FILE="$TEMPLATES_DIR/rich-fire-cli.py.template"
|
||||
;;
|
||||
typed)
|
||||
TEMPLATE_FILE="$TEMPLATES_DIR/typed-fire-cli.py.template"
|
||||
;;
|
||||
config)
|
||||
TEMPLATE_FILE="$TEMPLATES_DIR/config-fire-cli.py.template"
|
||||
;;
|
||||
multi)
|
||||
TEMPLATE_FILE="$TEMPLATES_DIR/multi-command-fire-cli.py.template"
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}Error: Invalid template type: $TEMPLATE${NC}"
|
||||
echo "Valid types: basic, nested, rich, typed, config, multi"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ ! -f "$TEMPLATE_FILE" ]]; then
|
||||
echo -e "${RED}Error: Template file not found: $TEMPLATE_FILE${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Prepare variables
|
||||
CLI_NAME_LOWER=$(echo "$CLI_NAME" | tr '[:upper:]' '[:lower:]' | tr ' ' '-')
|
||||
DEFAULT_PROJECT_NAME="my-project"
|
||||
SUBCOMMAND_GROUP_NAME="Resources"
|
||||
SUBCOMMAND_GROUP_DESCRIPTION="Resource management commands"
|
||||
SUBCOMMAND_GROUP_NAME_LOWER="resources"
|
||||
RESOURCE_NAME="resource"
|
||||
|
||||
echo -e "${BLUE}Generating Fire CLI...${NC}"
|
||||
echo -e "${BLUE} Name: ${NC}$CLI_NAME"
|
||||
echo -e "${BLUE} Description: ${NC}$DESCRIPTION"
|
||||
echo -e "${BLUE} Template: ${NC}$TEMPLATE"
|
||||
echo -e "${BLUE} Class: ${NC}$CLASS_NAME"
|
||||
echo -e "${BLUE} Version: ${NC}$VERSION"
|
||||
echo -e "${BLUE} Output: ${NC}$OUTPUT_FILE"
|
||||
|
||||
# Generate CLI by replacing template variables
|
||||
sed -e "s/{{CLI_NAME}}/$CLI_NAME/g" \
|
||||
-e "s/{{CLI_DESCRIPTION}}/$DESCRIPTION/g" \
|
||||
-e "s/{{CLASS_NAME}}/$CLASS_NAME/g" \
|
||||
-e "s/{{CLI_NAME_LOWER}}/$CLI_NAME_LOWER/g" \
|
||||
-e "s/{{VERSION}}/$VERSION/g" \
|
||||
-e "s/{{DEFAULT_PROJECT_NAME}}/$DEFAULT_PROJECT_NAME/g" \
|
||||
-e "s/{{SUBCOMMAND_GROUP_NAME}}/$SUBCOMMAND_GROUP_NAME/g" \
|
||||
-e "s/{{SUBCOMMAND_GROUP_DESCRIPTION}}/$SUBCOMMAND_GROUP_DESCRIPTION/g" \
|
||||
-e "s/{{SUBCOMMAND_GROUP_NAME_LOWER}}/$SUBCOMMAND_GROUP_NAME_LOWER/g" \
|
||||
-e "s/{{RESOURCE_NAME}}/$RESOURCE_NAME/g" \
|
||||
"$TEMPLATE_FILE" > "$OUTPUT_FILE"
|
||||
|
||||
# Make executable
|
||||
chmod +x "$OUTPUT_FILE"
|
||||
|
||||
echo -e "${GREEN}✓ Generated Fire CLI: $OUTPUT_FILE${NC}"
|
||||
echo -e "${YELLOW}Next steps:${NC}"
|
||||
echo -e " 1. Review and customize: ${BLUE}$OUTPUT_FILE${NC}"
|
||||
echo -e " 2. Install dependencies: ${BLUE}pip install fire rich${NC}"
|
||||
echo -e " 3. Test the CLI: ${BLUE}python $OUTPUT_FILE --help${NC}"
|
||||
echo -e " 4. Validate: ${BLUE}$SCRIPT_DIR/validate-fire-cli.py $OUTPUT_FILE${NC}"
|
||||
208
skills/fire-patterns/scripts/test-fire-cli.py
Executable file
208
skills/fire-patterns/scripts/test-fire-cli.py
Executable file
@@ -0,0 +1,208 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Test Fire CLI commands programmatically"""
|
||||
|
||||
import sys
|
||||
import importlib.util
|
||||
import inspect
|
||||
from pathlib import Path
|
||||
from typing import Any, List, Dict
|
||||
import json
|
||||
|
||||
|
||||
class FireCLITester:
|
||||
"""Test Fire CLI commands without running them"""
|
||||
|
||||
def __init__(self, filepath: Path):
|
||||
self.filepath = filepath
|
||||
self.module = None
|
||||
self.cli_class = None
|
||||
|
||||
def load_cli(self) -> bool:
|
||||
"""Load CLI module dynamically"""
|
||||
try:
|
||||
spec = importlib.util.spec_from_file_location("cli_module", self.filepath)
|
||||
self.module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(self.module)
|
||||
|
||||
# Find main CLI class (first class in module)
|
||||
for name, obj in inspect.getmembers(self.module):
|
||||
if inspect.isclass(obj) and obj.__module__ == self.module.__name__:
|
||||
self.cli_class = obj
|
||||
break
|
||||
|
||||
if not self.cli_class:
|
||||
print("Error: No CLI class found in module", file=sys.stderr)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error loading CLI module: {e}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
def get_commands(self) -> Dict[str, Any]:
|
||||
"""Get all available commands"""
|
||||
if not self.cli_class:
|
||||
return {}
|
||||
|
||||
commands = {}
|
||||
instance = self.cli_class()
|
||||
|
||||
# Get methods from main class
|
||||
for name, method in inspect.getmembers(instance, predicate=inspect.ismethod):
|
||||
if not name.startswith('_'):
|
||||
commands[name] = {
|
||||
'type': 'method',
|
||||
'signature': str(inspect.signature(method)),
|
||||
'doc': inspect.getdoc(method) or 'No documentation'
|
||||
}
|
||||
|
||||
# Get nested classes (command groups)
|
||||
for name, obj in inspect.getmembers(self.cli_class):
|
||||
if inspect.isclass(obj) and not name.startswith('_'):
|
||||
commands[name] = {
|
||||
'type': 'command_group',
|
||||
'doc': inspect.getdoc(obj) or 'No documentation',
|
||||
'methods': {}
|
||||
}
|
||||
|
||||
# Get methods from nested class
|
||||
for method_name, method in inspect.getmembers(obj, predicate=inspect.isfunction):
|
||||
if not method_name.startswith('_'):
|
||||
commands[name]['methods'][method_name] = {
|
||||
'signature': str(inspect.signature(method)),
|
||||
'doc': inspect.getdoc(method) or 'No documentation'
|
||||
}
|
||||
|
||||
return commands
|
||||
|
||||
def test_instantiation(self) -> bool:
|
||||
"""Test if CLI class can be instantiated"""
|
||||
try:
|
||||
instance = self.cli_class()
|
||||
print("✅ CLI class instantiation: PASSED")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ CLI class instantiation: FAILED - {e}")
|
||||
return False
|
||||
|
||||
def test_method_signatures(self) -> bool:
|
||||
"""Test if all methods have valid signatures"""
|
||||
try:
|
||||
instance = self.cli_class()
|
||||
errors = []
|
||||
|
||||
for name, method in inspect.getmembers(instance, predicate=inspect.ismethod):
|
||||
if name.startswith('_'):
|
||||
continue
|
||||
|
||||
try:
|
||||
sig = inspect.signature(method)
|
||||
# Check for invalid parameter types
|
||||
for param_name, param in sig.parameters.items():
|
||||
if param.kind == inspect.Parameter.VAR_KEYWORD:
|
||||
errors.append(f"Method '{name}' uses **kwargs (works but not recommended)")
|
||||
except Exception as e:
|
||||
errors.append(f"Method '{name}' signature error: {e}")
|
||||
|
||||
if errors:
|
||||
print("⚠️ Method signatures: WARNINGS")
|
||||
for error in errors:
|
||||
print(f" • {error}")
|
||||
return True # Warnings, not failures
|
||||
else:
|
||||
print("✅ Method signatures: PASSED")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Method signatures: FAILED - {e}")
|
||||
return False
|
||||
|
||||
def test_docstrings(self) -> bool:
|
||||
"""Test if all public methods have docstrings"""
|
||||
try:
|
||||
instance = self.cli_class()
|
||||
missing = []
|
||||
|
||||
for name, method in inspect.getmembers(instance, predicate=inspect.ismethod):
|
||||
if name.startswith('_'):
|
||||
continue
|
||||
|
||||
doc = inspect.getdoc(method)
|
||||
if not doc:
|
||||
missing.append(name)
|
||||
|
||||
if missing:
|
||||
print("⚠️ Docstrings: WARNINGS")
|
||||
print(f" Missing docstrings for: {', '.join(missing)}")
|
||||
return True # Warnings, not failures
|
||||
else:
|
||||
print("✅ Docstrings: PASSED")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Docstrings: FAILED - {e}")
|
||||
return False
|
||||
|
||||
def print_summary(self):
|
||||
"""Print CLI summary"""
|
||||
commands = self.get_commands()
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"Fire CLI Test Report: {self.filepath.name}")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
print(f"CLI Class: {self.cli_class.__name__}")
|
||||
print(f"Total Commands: {len(commands)}\n")
|
||||
|
||||
print("Available Commands:")
|
||||
for cmd_name, cmd_info in commands.items():
|
||||
if cmd_info['type'] == 'method':
|
||||
print(f" • {cmd_name}{cmd_info['signature']}")
|
||||
elif cmd_info['type'] == 'command_group':
|
||||
print(f" • {cmd_name}/ (command group)")
|
||||
for method_name, method_info in cmd_info['methods'].items():
|
||||
print(f" ◦ {method_name}{method_info['signature']}")
|
||||
|
||||
print()
|
||||
|
||||
def run_tests(self) -> bool:
|
||||
"""Run all tests"""
|
||||
print(f"\nTesting Fire CLI: {self.filepath.name}\n")
|
||||
|
||||
results = []
|
||||
results.append(self.test_instantiation())
|
||||
results.append(self.test_method_signatures())
|
||||
results.append(self.test_docstrings())
|
||||
|
||||
print()
|
||||
return all(results)
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: test-fire-cli.py <fire-cli-file.py> [--summary]")
|
||||
sys.exit(1)
|
||||
|
||||
filepath = Path(sys.argv[1])
|
||||
show_summary = '--summary' in sys.argv
|
||||
|
||||
if not filepath.exists():
|
||||
print(f"Error: File not found: {filepath}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
tester = FireCLITester(filepath)
|
||||
|
||||
if not tester.load_cli():
|
||||
sys.exit(1)
|
||||
|
||||
if show_summary:
|
||||
tester.print_summary()
|
||||
else:
|
||||
passed = tester.run_tests()
|
||||
tester.print_summary()
|
||||
sys.exit(0 if passed else 1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
194
skills/fire-patterns/scripts/validate-fire-cli.py
Executable file
194
skills/fire-patterns/scripts/validate-fire-cli.py
Executable file
@@ -0,0 +1,194 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate Fire CLI structure and docstrings"""
|
||||
|
||||
import ast
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Tuple
|
||||
|
||||
|
||||
class FireCLIValidator:
|
||||
"""Validates Fire CLI Python files for proper structure"""
|
||||
|
||||
def __init__(self, filepath: Path):
|
||||
self.filepath = filepath
|
||||
self.errors: List[str] = []
|
||||
self.warnings: List[str] = []
|
||||
self.tree = None
|
||||
|
||||
def validate(self) -> bool:
|
||||
"""Run all validation checks"""
|
||||
if not self._parse_file():
|
||||
return False
|
||||
|
||||
self._check_fire_import()
|
||||
self._check_main_class()
|
||||
self._check_docstrings()
|
||||
self._check_fire_call()
|
||||
self._check_method_signatures()
|
||||
|
||||
return len(self.errors) == 0
|
||||
|
||||
def _parse_file(self) -> bool:
|
||||
"""Parse Python file into AST"""
|
||||
try:
|
||||
content = self.filepath.read_text()
|
||||
self.tree = ast.parse(content)
|
||||
return True
|
||||
except SyntaxError as e:
|
||||
self.errors.append(f"Syntax error: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.errors.append(f"Failed to parse file: {e}")
|
||||
return False
|
||||
|
||||
def _check_fire_import(self):
|
||||
"""Check for fire import"""
|
||||
has_fire_import = False
|
||||
for node in ast.walk(self.tree):
|
||||
if isinstance(node, ast.Import):
|
||||
for alias in node.names:
|
||||
if alias.name == 'fire':
|
||||
has_fire_import = True
|
||||
break
|
||||
elif isinstance(node, ast.ImportFrom):
|
||||
if node.module == 'fire':
|
||||
has_fire_import = True
|
||||
break
|
||||
|
||||
if not has_fire_import:
|
||||
self.errors.append("Missing 'import fire' statement")
|
||||
|
||||
def _check_main_class(self):
|
||||
"""Check for main CLI class"""
|
||||
classes = [node for node in self.tree.body if isinstance(node, ast.ClassDef)]
|
||||
|
||||
if not classes:
|
||||
self.errors.append("No classes found - Fire CLI requires at least one class")
|
||||
return
|
||||
|
||||
main_class = classes[0] # Assume first class is main
|
||||
|
||||
# Check class docstring
|
||||
docstring = ast.get_docstring(main_class)
|
||||
if not docstring:
|
||||
self.warnings.append(f"Class '{main_class.name}' missing docstring")
|
||||
|
||||
# Check for __init__ method
|
||||
has_init = any(
|
||||
isinstance(node, ast.FunctionDef) and node.name == '__init__'
|
||||
for node in main_class.body
|
||||
)
|
||||
|
||||
if not has_init:
|
||||
self.warnings.append(f"Class '{main_class.name}' missing __init__ method")
|
||||
|
||||
def _check_docstrings(self):
|
||||
"""Check method docstrings"""
|
||||
for node in ast.walk(self.tree):
|
||||
if isinstance(node, ast.ClassDef):
|
||||
for item in node.body:
|
||||
if isinstance(item, ast.FunctionDef):
|
||||
# Skip private methods
|
||||
if item.name.startswith('_'):
|
||||
continue
|
||||
|
||||
docstring = ast.get_docstring(item)
|
||||
if not docstring:
|
||||
self.warnings.append(
|
||||
f"Method '{item.name}' missing docstring "
|
||||
"(used for Fire help text)"
|
||||
)
|
||||
else:
|
||||
# Check for Args section in docstring
|
||||
if item.args.args and len(item.args.args) > 1: # Skip 'self'
|
||||
if 'Args:' not in docstring:
|
||||
self.warnings.append(
|
||||
f"Method '{item.name}' docstring missing 'Args:' section"
|
||||
)
|
||||
|
||||
def _check_fire_call(self):
|
||||
"""Check for fire.Fire() call"""
|
||||
has_fire_call = False
|
||||
|
||||
for node in ast.walk(self.tree):
|
||||
if isinstance(node, ast.Call):
|
||||
if isinstance(node.func, ast.Attribute):
|
||||
if (isinstance(node.func.value, ast.Name) and
|
||||
node.func.value.id == 'fire' and
|
||||
node.func.attr == 'Fire'):
|
||||
has_fire_call = True
|
||||
break
|
||||
|
||||
if not has_fire_call:
|
||||
self.errors.append("Missing 'fire.Fire()' call (required to run CLI)")
|
||||
|
||||
def _check_method_signatures(self):
|
||||
"""Check method signatures for Fire compatibility"""
|
||||
for node in ast.walk(self.tree):
|
||||
if isinstance(node, ast.ClassDef):
|
||||
for item in node.body:
|
||||
if isinstance(item, ast.FunctionDef):
|
||||
# Skip private and special methods
|
||||
if item.name.startswith('_'):
|
||||
continue
|
||||
|
||||
# Check for *args or **kwargs (Fire handles these but warn)
|
||||
if item.args.vararg or item.args.kwarg:
|
||||
self.warnings.append(
|
||||
f"Method '{item.name}' uses *args or **kwargs - "
|
||||
"Fire will handle these, but explicit params are clearer"
|
||||
)
|
||||
|
||||
def print_results(self):
|
||||
"""Print validation results"""
|
||||
print(f"\n{'='*60}")
|
||||
print(f"Fire CLI Validation: {self.filepath.name}")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
if self.errors:
|
||||
print("❌ ERRORS:")
|
||||
for error in self.errors:
|
||||
print(f" • {error}")
|
||||
print()
|
||||
|
||||
if self.warnings:
|
||||
print("⚠️ WARNINGS:")
|
||||
for warning in self.warnings:
|
||||
print(f" • {warning}")
|
||||
print()
|
||||
|
||||
if not self.errors and not self.warnings:
|
||||
print("✅ All checks passed!")
|
||||
elif not self.errors:
|
||||
print(f"✅ Validation passed with {len(self.warnings)} warning(s)")
|
||||
else:
|
||||
print(f"❌ Validation failed with {len(self.errors)} error(s)")
|
||||
|
||||
print()
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
print("Usage: validate-fire-cli.py <fire-cli-file.py>")
|
||||
sys.exit(1)
|
||||
|
||||
filepath = Path(sys.argv[1])
|
||||
|
||||
if not filepath.exists():
|
||||
print(f"Error: File not found: {filepath}")
|
||||
sys.exit(1)
|
||||
|
||||
if not filepath.suffix == '.py':
|
||||
print(f"Error: File must be a Python file (.py): {filepath}")
|
||||
sys.exit(1)
|
||||
|
||||
validator = FireCLIValidator(filepath)
|
||||
is_valid = validator.validate()
|
||||
validator.print_results()
|
||||
|
||||
sys.exit(0 if is_valid else 1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user