124 lines
4.3 KiB
Python
124 lines
4.3 KiB
Python
#!/usr/bin/env python3
|
|
import ast
|
|
import os
|
|
import sys
|
|
|
|
# Target file to check: py/image_manager/api.py
|
|
TARGET_FILE = os.path.join(os.path.dirname(__file__), "..", "py/image_manager/api.py")
|
|
|
|
|
|
class DataclassUsageChecker(ast.NodeVisitor):
|
|
def __init__(self, filename: str) -> None:
|
|
self.filename = filename
|
|
self.errors: list[tuple[int, str]] = []
|
|
|
|
def _get_name(self, node: ast.AST) -> str | None:
|
|
"""Helper to get the full name of an attribute or name node."""
|
|
if isinstance(node, ast.Name):
|
|
return node.id
|
|
elif isinstance(node, ast.Attribute):
|
|
base = self._get_name(node.value)
|
|
if base:
|
|
return f"{base}.{node.attr}"
|
|
return None
|
|
|
|
def visit_Call(self, node: ast.Call) -> None:
|
|
# Detect .get("...")
|
|
if isinstance(node.func, ast.Attribute) and node.func.attr == "get" and len(node.args) >= 1:
|
|
# Filter out known false positives
|
|
receiver = self._get_name(node.func.value)
|
|
|
|
# 1. routes.get(...) used for ComfyUI routing
|
|
if receiver and "routes" in receiver.split("."):
|
|
self.generic_visit(node)
|
|
return
|
|
|
|
# 2. os.environ.get(...) or environ.get(...)
|
|
if receiver in ("os.environ", "environ"):
|
|
self.generic_visit(node)
|
|
return
|
|
|
|
# 3. request.query.get(...), request.app.get(...), etc. - Standard aiohttp access
|
|
if receiver and receiver.startswith("request."):
|
|
self.generic_visit(node)
|
|
return
|
|
|
|
# 4. db_settings.get(...) - Global settings dict
|
|
if receiver == "db_settings":
|
|
self.generic_visit(node)
|
|
return
|
|
|
|
arg0 = node.args[0]
|
|
# Handle ast.Constant (Py 3.8+)
|
|
value = None
|
|
if isinstance(arg0, ast.Constant) and isinstance(arg0.value, str):
|
|
value = arg0.value
|
|
|
|
if value is not None:
|
|
self.errors.append((node.lineno, f"Forbidden .get() with string literal: {value!r}"))
|
|
|
|
self.generic_visit(node)
|
|
|
|
def visit_Subscript(self, node: ast.Subscript) -> None:
|
|
# Detect ["..."]
|
|
|
|
# Filter out known false positives
|
|
receiver = self._get_name(node.value)
|
|
|
|
# 1. request.match_info[...], request.query[...], request.app[...] - Standard aiohttp access
|
|
if receiver and receiver.startswith("request."):
|
|
self.generic_visit(node)
|
|
return
|
|
|
|
index_node = node.slice
|
|
|
|
# Handle Python versions < 3.9 where node.slice is ast.Index(value=...)
|
|
# Use string-based type check to avoid deprecation warnings for ast.Index
|
|
if type(index_node).__name__ == "Index":
|
|
index_node = getattr(index_node, "value") # noqa: B009
|
|
|
|
value = None
|
|
if isinstance(index_node, ast.Constant) and isinstance(index_node.value, str):
|
|
value = index_node.value
|
|
|
|
if value is not None:
|
|
self.errors.append((node.lineno, f"Forbidden subscript access with string literal: {value!r}"))
|
|
|
|
self.generic_visit(node)
|
|
|
|
|
|
def main() -> int:
|
|
# Ensure we use absolute path or relative to workspace root
|
|
abs_target = os.path.abspath(TARGET_FILE)
|
|
if not os.path.exists(abs_target):
|
|
# Fallback for different execution contexts
|
|
abs_target = os.path.abspath("py/image_manager/api.py")
|
|
if not os.path.exists(abs_target):
|
|
print(f"Target file {TARGET_FILE} not found.")
|
|
return 0
|
|
|
|
with open(abs_target, encoding="utf-8") as f:
|
|
try:
|
|
tree = ast.parse(f.read())
|
|
except SyntaxError as e:
|
|
print(f"Syntax error in {abs_target}: {e}")
|
|
return 1
|
|
|
|
checker = DataclassUsageChecker(abs_target)
|
|
checker.visit(tree)
|
|
|
|
if checker.errors:
|
|
print(f"Error: Found dict-style access in {os.path.relpath(abs_target)}:")
|
|
# Sort by line number
|
|
for lineno, msg in sorted(checker.errors):
|
|
print(f" Line {lineno}: {msg}")
|
|
print(f"\nFound {len(checker.errors)} violations. Please use Dataclass attributes instead of dict keys.")
|
|
return 1
|
|
|
|
print(f"No dict-style access found in {os.path.relpath(abs_target)}.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|