add toml, yaml, json validation

This commit is contained in:
2025-09-29 11:28:36 +04:00
parent 8ded8c81ee
commit 26991ce61a

View File

@@ -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)