7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
189 lines
6.6 KiB
Python
Executable File
189 lines
6.6 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Pre-commit hook to detect try/finally session patterns and suggest context managers.
|
|
|
|
This hook checks for SQLAlchemy session management patterns that use try/finally
|
|
blocks and suggests replacing them with context managers for better resource
|
|
management and cleaner code.
|
|
"""
|
|
|
|
import ast
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import List, Tuple
|
|
|
|
|
|
class SessionPatternChecker(ast.NodeVisitor):
|
|
"""AST visitor to detect try/finally session patterns."""
|
|
|
|
def __init__(self, filename: str):
|
|
self.filename = filename
|
|
self.issues: List[Tuple[int, str]] = []
|
|
self.functions_and_methods = []
|
|
|
|
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
|
|
"""Visit function definitions to check for session patterns."""
|
|
self._check_function_for_pattern(node)
|
|
self.generic_visit(node)
|
|
|
|
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
|
|
"""Visit async function definitions to check for session patterns."""
|
|
self._check_function_for_pattern(node)
|
|
self.generic_visit(node)
|
|
|
|
def _check_function_for_pattern(self, func_node) -> None:
|
|
"""Check a function body for try/finally session patterns."""
|
|
# Look for session = Session() followed by try/finally
|
|
for i, stmt in enumerate(func_node.body):
|
|
# Check if this is a session assignment
|
|
if isinstance(stmt, ast.Assign):
|
|
session_var = self._get_session_var_from_assign(stmt)
|
|
if session_var:
|
|
# Look for a try/finally block that follows
|
|
for next_stmt in func_node.body[
|
|
i + 1 : i + 3
|
|
]: # Check next 2 statements
|
|
if (
|
|
isinstance(next_stmt, ast.Try)
|
|
and next_stmt.finalbody
|
|
):
|
|
# Check if finally has session.close()
|
|
if self._has_session_close_in_finally(
|
|
next_stmt.finalbody, session_var
|
|
):
|
|
self.issues.append(
|
|
(
|
|
stmt.lineno,
|
|
f"Found try/finally session pattern. Consider using 'with self.Session() as {session_var}:' instead",
|
|
)
|
|
)
|
|
break
|
|
|
|
def _get_session_var_from_assign(self, assign_node: ast.Assign) -> str:
|
|
"""Check if an assignment is creating a session and return the variable name."""
|
|
if isinstance(assign_node.value, ast.Call) and self._is_session_call(
|
|
assign_node.value
|
|
):
|
|
if assign_node.targets and isinstance(
|
|
assign_node.targets[0], ast.Name
|
|
):
|
|
return assign_node.targets[0].id
|
|
return None
|
|
|
|
def _is_session_call(self, call_node: ast.Call) -> bool:
|
|
"""Check if a call node is creating a SQLAlchemy session."""
|
|
# Check for self.Session() pattern
|
|
if isinstance(call_node.func, ast.Attribute):
|
|
if call_node.func.attr in (
|
|
"Session",
|
|
"get_session",
|
|
"create_session",
|
|
):
|
|
return True
|
|
# Check for Session() pattern
|
|
elif isinstance(call_node.func, ast.Name):
|
|
if call_node.func.id in (
|
|
"Session",
|
|
"get_session",
|
|
"create_session",
|
|
):
|
|
return True
|
|
return False
|
|
|
|
def _has_session_close_in_finally(
|
|
self, finalbody: List[ast.stmt], session_var: str
|
|
) -> bool:
|
|
"""Check if finally block contains session.close()."""
|
|
for stmt in finalbody:
|
|
if isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Call):
|
|
# Check for session.close() pattern
|
|
if (
|
|
isinstance(stmt.value.func, ast.Attribute)
|
|
and stmt.value.func.attr == "close"
|
|
):
|
|
# Check if it's our session variable
|
|
if (
|
|
isinstance(stmt.value.func.value, ast.Name)
|
|
and stmt.value.func.value.id == session_var
|
|
):
|
|
return True
|
|
return False
|
|
|
|
|
|
def check_file(filepath: Path) -> List[Tuple[str, int, str]]:
|
|
"""Check a single Python file for try/finally session patterns."""
|
|
issues = []
|
|
|
|
try:
|
|
content = filepath.read_text(encoding="utf-8")
|
|
tree = ast.parse(content, filename=str(filepath))
|
|
|
|
checker = SessionPatternChecker(str(filepath))
|
|
checker.visit(tree)
|
|
|
|
for line_no, message in checker.issues:
|
|
issues.append((str(filepath), line_no, message))
|
|
|
|
except SyntaxError as e:
|
|
# Skip files with syntax errors
|
|
print(f"Syntax error in {filepath}: {e}", file=sys.stderr)
|
|
except Exception as e:
|
|
print(f"Error checking {filepath}: {e}", file=sys.stderr)
|
|
|
|
return issues
|
|
|
|
|
|
def main():
|
|
"""Main entry point for the pre-commit hook."""
|
|
# Get list of files to check from command line arguments
|
|
files_to_check = sys.argv[1:] if len(sys.argv) > 1 else []
|
|
|
|
if not files_to_check:
|
|
print("No files to check")
|
|
return 0
|
|
|
|
all_issues = []
|
|
|
|
for filepath_str in files_to_check:
|
|
filepath = Path(filepath_str)
|
|
|
|
# Skip non-Python files
|
|
if not filepath.suffix == ".py":
|
|
continue
|
|
|
|
# Skip test files and migration files
|
|
if "test" in filepath.parts or "migration" in filepath.parts:
|
|
continue
|
|
|
|
issues = check_file(filepath)
|
|
all_issues.extend(issues)
|
|
|
|
# Report issues
|
|
if all_issues:
|
|
print(
|
|
"\n❌ Found try/finally session patterns that should use context managers:\n"
|
|
)
|
|
for filepath, line_no, message in all_issues:
|
|
print(f" {filepath}:{line_no}: {message}")
|
|
|
|
print("\n💡 Tip: Replace try/finally blocks with context managers:")
|
|
print(" Before:")
|
|
print(" session = self.Session()")
|
|
print(" try:")
|
|
print(" # operations")
|
|
print(" session.commit()")
|
|
print(" finally:")
|
|
print(" session.close()")
|
|
print("\n After:")
|
|
print(" with self.Session() as session:")
|
|
print(" # operations")
|
|
print(" session.commit()")
|
|
print("\n")
|
|
return 1
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|