add toml, yaml, json validation
This commit is contained in:
@@ -2,19 +2,27 @@
|
|||||||
"""
|
"""
|
||||||
Development workflow runner for the Skelly Godot project.
|
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.
|
Provides colored output and error reporting.
|
||||||
|
|
||||||
Usage examples:
|
Usage examples:
|
||||||
python tools/run_development.py # Run all steps
|
python tools/run_development.py # Run all steps
|
||||||
python tools/run_development.py --lint # Only linting
|
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
|
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
|
NOTE: Handles "successful but noisy" linter output such as
|
||||||
"Success: no problems found" - treats these as clean instead of warnings.
|
"Success: no problems found" - treats these as clean instead of warnings.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -24,6 +32,19 @@ import warnings
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Tuple
|
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
|
# Suppress pkg_resources deprecation warning from gdtoolkit
|
||||||
warnings.filterwarnings("ignore", message="pkg_resources is deprecated", category=UserWarning)
|
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"))
|
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:
|
def format_test_name(filename: str) -> str:
|
||||||
"""Convert test_filename to readable test name."""
|
"""Convert test_filename to readable test name."""
|
||||||
return filename.replace("test_", "").replace("_", " ")
|
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}
|
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]:
|
def run_format(project_root: Path) -> Tuple[bool, Dict]:
|
||||||
"""Run gdformat on all GDScript files."""
|
"""Run gdformat on all GDScript files."""
|
||||||
@@ -498,7 +705,8 @@ def run_workflow(project_root: Path, steps: List[str]) -> bool:
|
|||||||
workflow_steps = {
|
workflow_steps = {
|
||||||
"lint": ("🔍 Code linting (gdlint)", run_lint),
|
"lint": ("🔍 Code linting (gdlint)", run_lint),
|
||||||
"format": ("🎨 Code formatting (gdformat)", run_format),
|
"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:"
|
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_text = "PASSED" if step_success else "FAILED"
|
||||||
status_color = Colors.GREEN if step_success else Colors.RED
|
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)
|
colored_status = Colors.colorize(f"{status_text}", status_color + Colors.BOLD)
|
||||||
print(f"{step_emoji} {step.capitalize()}: {status_emoji} {colored_status}")
|
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():
|
def main():
|
||||||
"""Main entry point."""
|
"""Main entry point."""
|
||||||
parser = argparse.ArgumentParser(description="Run development workflow for Skelly Godot project")
|
parser = argparse.ArgumentParser(description="Run development workflow for Skelly Godot project")
|
||||||
parser.add_argument("--steps", nargs="+", choices=["lint", "format", "test"],
|
parser.add_argument("--steps", nargs="+", choices=["lint", "format", "test", "validate"],
|
||||||
default=["format", "lint", "test"], help="Workflow steps to run")
|
default=["format", "lint", "test", "validate"], help="Workflow steps to run")
|
||||||
parser.add_argument("--lint", action="store_true", help="Run linting")
|
parser.add_argument("--lint", action="store_true", help="Run linting")
|
||||||
parser.add_argument("--format", action="store_true", help="Run formatting")
|
parser.add_argument("--format", action="store_true", help="Run formatting")
|
||||||
parser.add_argument("--test", action="store_true", help="Run tests")
|
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()
|
args = parser.parse_args()
|
||||||
project_root = Path(__file__).parent.parent
|
project_root = Path(__file__).parent.parent
|
||||||
@@ -605,12 +814,14 @@ def main():
|
|||||||
steps = ["format"]
|
steps = ["format"]
|
||||||
elif args.test:
|
elif args.test:
|
||||||
steps = ["test"]
|
steps = ["test"]
|
||||||
|
elif args.validate:
|
||||||
|
steps = ["validate"]
|
||||||
else:
|
else:
|
||||||
steps = args.steps
|
steps = args.steps
|
||||||
|
|
||||||
# Run workflow or individual step
|
# Run workflow or individual step
|
||||||
if len(steps) == 1:
|
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)
|
success, _ = step_funcs[steps[0]](project_root)
|
||||||
else:
|
else:
|
||||||
success = run_workflow(project_root, steps)
|
success = run_workflow(project_root, steps)
|
||||||
|
|||||||
Reference in New Issue
Block a user