fashionistar-celery-queues / scripts /validate_utf8.py
FASHIONISTAR CI/CD
🔄 Celery Queues Deploy: 4aae6106b1530c3cc7d7d64f5e3c3e1d5015691d [GitHub Actions]
27c799c
Raw
History Blame
3.13 kB
#!/usr/bin/env python3
"""Validate that every .py file is plain UTF-8 and has no BOM/UTF-16 encoding.
This catches files like the old `scratch/old_product_views.py` which was saved
as UTF-16 LE with BOM and broke the Codacy security scan.
Usage:
python scripts/validate_utf8.py
python scripts/validate_utf8.py --path /path/to/repo
Exit codes:
0 - all Python files are valid UTF-8 without BOM
1 - one or more files are malformed
"""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
# Directories that should not be scanned
SKIP_DIRS = {
".git",
".venv",
".env",
".uv-cache",
".cache",
"__pycache__",
".mypy_cache",
".pytest_cache",
".ruff_cache",
".tox",
".egg-info",
"build",
"dist",
"node_modules",
}
# Byte markers that indicate BOMs
UTF8_BOM = b"\xef\xbb\xbf"
UTF16_LE_BOM = b"\xff\xfe"
UTF16_BE_BOM = b"\xfe\xff"
def should_skip(path: Path) -> bool:
"""Return True if the directory should be skipped."""
return any(part in SKIP_DIRS for part in path.parts)
def validate_file(file_path: Path) -> list[str]:
"""Return a list of error messages for the file, or empty if valid."""
errors: list[str] = []
raw = file_path.read_bytes()
# 1. Reject UTF-16 BOMs
if raw.startswith(UTF16_LE_BOM):
errors.append(f"{file_path}: UTF-16 LE BOM detected")
elif raw.startswith(UTF16_BE_BOM):
errors.append(f"{file_path}: UTF-16 BE BOM detected")
elif raw.startswith(UTF8_BOM):
errors.append(f"{file_path}: UTF-8 BOM detected")
# 2. Reject null bytes
if b"\x00" in raw:
errors.append(f"{file_path}: contains null bytes")
# 3. Validate strict UTF-8 decode
if not errors:
try:
raw.decode("utf-8", errors="strict")
except UnicodeDecodeError as exc:
errors.append(f"{file_path}: {exc}")
return errors
def main() -> int:
parser = argparse.ArgumentParser(
description="Validate UTF-8 encoding for all .py files in a tree."
)
parser.add_argument(
"--path",
type=Path,
default=Path.cwd(),
help="Root directory to scan (default: current directory)",
)
parser.add_argument(
"--skip",
type=str,
action="append",
default=[],
help="Additional directory names to skip (can be given multiple times)",
)
args = parser.parse_args()
skip_dirs = SKIP_DIRS | set(args.skip)
root: Path = args.path.resolve()
all_errors: list[str] = []
for py_path in root.rglob("*.py"):
if should_skip(py_path):
continue
all_errors.extend(validate_file(py_path))
if all_errors:
print("Invalid encoding detected in the following files:", file=sys.stderr)
for error in all_errors:
print(f" - {error}", file=sys.stderr)
return 1
print(f"Validated {len(list(root.rglob('*.py')))} Python files in {root}")
print("All .py files are valid UTF-8 without BOM.")
return 0
if __name__ == "__main__":
raise SystemExit(main())