add toml, yaml, json validation
This commit is contained in:
@@ -2,19 +2,27 @@
|
||||
"""
|
||||
Development workflow runner for the Skelly Godot project.
|
||||
|
||||
Runs code quality checks (linting, formatting, testing) individually or together.
|
||||
Runs code quality checks (linting, formatting, testing, validation) individually or together.
|
||||
Provides colored output and error reporting.
|
||||
|
||||
Usage examples:
|
||||
python tools/run_development.py # Run all steps
|
||||
python tools/run_development.py --lint # Only linting
|
||||
python tools/run_development.py --steps lint test # Custom workflow
|
||||
python tools/run_development.py # Run all steps
|
||||
python tools/run_development.py --lint # Only linting
|
||||
python tools/run_development.py --validate # Only file validation
|
||||
python tools/run_development.py --steps lint test # Custom workflow
|
||||
|
||||
Features:
|
||||
- GDScript formatting and linting
|
||||
- Test execution
|
||||
- YAML, TOML, and JSON file validation (respects .gitignore)
|
||||
- Colored output and comprehensive error reporting
|
||||
|
||||
NOTE: Handles "successful but noisy" linter output such as
|
||||
"Success: no problems found" - treats these as clean instead of warnings.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
@@ -24,6 +32,19 @@ import warnings
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
yaml = None
|
||||
|
||||
try:
|
||||
import tomllib
|
||||
except ImportError:
|
||||
try:
|
||||
import tomli as tomllib
|
||||
except ImportError:
|
||||
tomllib = None
|
||||
|
||||
# Suppress pkg_resources deprecation warning from gdtoolkit
|
||||
warnings.filterwarnings("ignore", message="pkg_resources is deprecated", category=UserWarning)
|
||||
|
||||
@@ -169,6 +190,67 @@ def get_gd_files(project_root: Path) -> List[Path]:
|
||||
return list(project_root.rglob("*.gd"))
|
||||
|
||||
|
||||
def read_gitignore(project_root: Path) -> List[str]:
|
||||
"""Read gitignore patterns from .gitignore file."""
|
||||
gitignore_path = project_root / ".gitignore"
|
||||
patterns = []
|
||||
|
||||
if gitignore_path.exists():
|
||||
try:
|
||||
with open(gitignore_path, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and not line.startswith('#'):
|
||||
patterns.append(line)
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not read .gitignore: {e}")
|
||||
|
||||
return patterns
|
||||
|
||||
|
||||
def is_ignored_by_gitignore(file_path: Path, project_root: Path, patterns: List[str]) -> bool:
|
||||
"""Check if a file should be ignored based on gitignore patterns."""
|
||||
relative_path = file_path.relative_to(project_root)
|
||||
path_str = str(relative_path).replace('\\', '/')
|
||||
|
||||
for pattern in patterns:
|
||||
# Handle directory patterns
|
||||
if pattern.endswith('/'):
|
||||
if any(part == pattern[:-1] for part in relative_path.parts):
|
||||
return True
|
||||
# Handle file patterns
|
||||
elif pattern in path_str or relative_path.name == pattern:
|
||||
return True
|
||||
# Handle glob patterns (simple implementation)
|
||||
elif '*' in pattern:
|
||||
import fnmatch
|
||||
if fnmatch.fnmatch(path_str, pattern) or fnmatch.fnmatch(relative_path.name, pattern):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def get_validation_files(project_root: Path) -> Dict[str, List[Path]]:
|
||||
"""Get all YAML, TOML, and JSON files in the project, respecting gitignore."""
|
||||
gitignore_patterns = read_gitignore(project_root)
|
||||
|
||||
file_types = {
|
||||
'yaml': ['*.yaml', '*.yml'],
|
||||
'toml': ['*.toml'],
|
||||
'json': ['*.json']
|
||||
}
|
||||
|
||||
files = {file_type: [] for file_type in file_types}
|
||||
|
||||
for file_type, patterns in file_types.items():
|
||||
for pattern in patterns:
|
||||
for file_path in project_root.rglob(pattern):
|
||||
if file_path.is_file() and not is_ignored_by_gitignore(file_path, project_root, gitignore_patterns):
|
||||
files[file_type].append(file_path)
|
||||
|
||||
return files
|
||||
|
||||
|
||||
def format_test_name(filename: str) -> str:
|
||||
"""Convert test_filename to readable test name."""
|
||||
return filename.replace("test_", "").replace("_", " ")
|
||||
@@ -279,7 +361,132 @@ def run_lint(project_root: Path) -> Tuple[bool, Dict]:
|
||||
return success, {**stats, "failed_paths": failed_paths}
|
||||
|
||||
|
||||
# rest of file unchanged (format, tests, workflow runner) -- copied verbatim
|
||||
def validate_yaml_file(file_path: Path) -> Tuple[bool, str]:
|
||||
"""Validate a YAML file."""
|
||||
if yaml is None:
|
||||
return False, "PyYAML not installed. Install with: pip install PyYAML"
|
||||
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
yaml.safe_load(f)
|
||||
return True, ""
|
||||
except yaml.YAMLError as e:
|
||||
return False, f"YAML syntax error: {e}"
|
||||
except Exception as e:
|
||||
return False, f"Error reading file: {e}"
|
||||
|
||||
|
||||
def validate_toml_file(file_path: Path) -> Tuple[bool, str]:
|
||||
"""Validate a TOML file."""
|
||||
if tomllib is None:
|
||||
return False, "tomllib/tomli not available. For Python 3.11+, it's built-in. For older versions: pip install tomli"
|
||||
|
||||
try:
|
||||
with open(file_path, 'rb') as f:
|
||||
tomllib.load(f)
|
||||
return True, ""
|
||||
except tomllib.TOMLDecodeError as e:
|
||||
return False, f"TOML syntax error: {e}"
|
||||
except Exception as e:
|
||||
return False, f"Error reading file: {e}"
|
||||
|
||||
|
||||
def validate_json_file(file_path: Path) -> Tuple[bool, str]:
|
||||
"""Validate a JSON file."""
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
json.load(f)
|
||||
return True, ""
|
||||
except json.JSONDecodeError as e:
|
||||
return False, f"JSON syntax error: {e}"
|
||||
except Exception as e:
|
||||
return False, f"Error reading file: {e}"
|
||||
|
||||
|
||||
def run_validate(project_root: Path) -> Tuple[bool, Dict]:
|
||||
"""Run validation on YAML, TOML, and JSON files."""
|
||||
print_header("📋 File Format Validation")
|
||||
|
||||
# Get all validation files
|
||||
validation_files = get_validation_files(project_root)
|
||||
total_files = sum(len(files) for files in validation_files.values())
|
||||
|
||||
if total_files == 0:
|
||||
msg = "No YAML, TOML, or JSON files found to validate."
|
||||
colored_msg = Colors.colorize(msg, Colors.YELLOW)
|
||||
print(colored_msg)
|
||||
return True, {"Total files": 0, "Valid files": 0, "Invalid files": 0}
|
||||
|
||||
count_msg = f"Found {total_files} files to validate:"
|
||||
colored_count = Colors.colorize(count_msg, Colors.BLUE)
|
||||
print(colored_count)
|
||||
|
||||
for file_type, files in validation_files.items():
|
||||
if files:
|
||||
type_msg = f" {file_type.upper()}: {len(files)} files"
|
||||
colored_type = Colors.colorize(type_msg, Colors.CYAN)
|
||||
print(colored_type)
|
||||
|
||||
print()
|
||||
|
||||
valid_files = invalid_files = 0
|
||||
failed_paths = []
|
||||
|
||||
# Validation functions mapping
|
||||
validators = {
|
||||
'yaml': validate_yaml_file,
|
||||
'toml': validate_toml_file,
|
||||
'json': validate_json_file
|
||||
}
|
||||
|
||||
# Validate each file type
|
||||
for file_type, files in validation_files.items():
|
||||
if not files:
|
||||
continue
|
||||
|
||||
validator = validators[file_type]
|
||||
type_header = f"🔍 Validating {file_type.upper()} files"
|
||||
colored_header = Colors.colorize(type_header, Colors.MAGENTA + Colors.BOLD)
|
||||
print(colored_header)
|
||||
|
||||
for file_path in files:
|
||||
relative_path = file_path.relative_to(project_root)
|
||||
file_msg = f"📄 Validating: {relative_path}"
|
||||
colored_file = Colors.colorize(file_msg, Colors.CYAN)
|
||||
print(colored_file)
|
||||
|
||||
is_valid, error_msg = validator(file_path)
|
||||
|
||||
if is_valid:
|
||||
valid_files += 1
|
||||
print_result(True, "")
|
||||
else:
|
||||
invalid_files += 1
|
||||
failed_paths.append(str(relative_path))
|
||||
print_result(False, error_msg)
|
||||
|
||||
print()
|
||||
|
||||
# Summary
|
||||
stats = {
|
||||
"Total files": total_files,
|
||||
"Valid files": valid_files,
|
||||
"Invalid files": invalid_files
|
||||
}
|
||||
print_summary("Validation Summary", stats)
|
||||
|
||||
success = invalid_files == 0
|
||||
print()
|
||||
if not success:
|
||||
msg = "❌ File validation FAILED - Please fix the syntax errors above"
|
||||
colored_msg = Colors.colorize(msg, Colors.RED + Colors.BOLD)
|
||||
print(colored_msg)
|
||||
else:
|
||||
msg = "✅ All files passed validation!"
|
||||
colored_msg = Colors.colorize(msg, Colors.GREEN + Colors.BOLD)
|
||||
print(colored_msg)
|
||||
|
||||
return success, {**stats, "failed_paths": failed_paths}
|
||||
|
||||
def run_format(project_root: Path) -> Tuple[bool, Dict]:
|
||||
"""Run gdformat on all GDScript files."""
|
||||
@@ -498,7 +705,8 @@ def run_workflow(project_root: Path, steps: List[str]) -> bool:
|
||||
workflow_steps = {
|
||||
"lint": ("🔍 Code linting (gdlint)", run_lint),
|
||||
"format": ("🎨 Code formatting (gdformat)", run_format),
|
||||
"test": ("🧪 Test execution (godot tests)", run_tests)
|
||||
"test": ("🧪 Test execution (godot tests)", run_tests),
|
||||
"validate": ("📋 File format validation (yaml/toml/json)", run_validate)
|
||||
}
|
||||
|
||||
intro_msg = "🚀 This script will run the development workflow:"
|
||||
@@ -554,7 +762,7 @@ def run_workflow(project_root: Path, steps: List[str]) -> bool:
|
||||
status_text = "PASSED" if step_success else "FAILED"
|
||||
status_color = Colors.GREEN if step_success else Colors.RED
|
||||
|
||||
step_emoji = {"lint": "🔍", "format": "🎨", "test": "🧪"}.get(step, "📋")
|
||||
step_emoji = {"lint": "🔍", "format": "🎨", "test": "🧪", "validate": "📋"}.get(step, "📋")
|
||||
colored_status = Colors.colorize(f"{status_text}", status_color + Colors.BOLD)
|
||||
print(f"{step_emoji} {step.capitalize()}: {status_emoji} {colored_status}")
|
||||
|
||||
@@ -589,11 +797,12 @@ def run_workflow(project_root: Path, steps: List[str]) -> bool:
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
parser = argparse.ArgumentParser(description="Run development workflow for Skelly Godot project")
|
||||
parser.add_argument("--steps", nargs="+", choices=["lint", "format", "test"],
|
||||
default=["format", "lint", "test"], help="Workflow steps to run")
|
||||
parser.add_argument("--steps", nargs="+", choices=["lint", "format", "test", "validate"],
|
||||
default=["format", "lint", "test", "validate"], help="Workflow steps to run")
|
||||
parser.add_argument("--lint", action="store_true", help="Run linting")
|
||||
parser.add_argument("--format", action="store_true", help="Run formatting")
|
||||
parser.add_argument("--test", action="store_true", help="Run tests")
|
||||
parser.add_argument("--validate", action="store_true", help="Run file format validation")
|
||||
|
||||
args = parser.parse_args()
|
||||
project_root = Path(__file__).parent.parent
|
||||
@@ -605,12 +814,14 @@ def main():
|
||||
steps = ["format"]
|
||||
elif args.test:
|
||||
steps = ["test"]
|
||||
elif args.validate:
|
||||
steps = ["validate"]
|
||||
else:
|
||||
steps = args.steps
|
||||
|
||||
# Run workflow or individual step
|
||||
if len(steps) == 1:
|
||||
step_funcs = {"lint": run_lint, "format": run_format, "test": run_tests}
|
||||
step_funcs = {"lint": run_lint, "format": run_format, "test": run_tests, "validate": run_validate}
|
||||
success, _ = step_funcs[steps[0]](project_root)
|
||||
else:
|
||||
success = run_workflow(project_root, steps)
|
||||
|
||||
Reference in New Issue
Block a user