learningcircuit--local-deep-research
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
245 行
7.8 KiB
Python
可执行文件
245 行
7.8 KiB
Python
可执行文件
#!/usr/bin/env python3
|
|
"""
|
|
Pre-commit hook to flag unsafe cleanup patterns.
|
|
|
|
Check 1 — Silent close() methods:
|
|
Detects `def close(self)` methods that contain bare `except Exception: pass`
|
|
or `except: pass` — cleanup failures should be logged via safe_close() from
|
|
utilities.resource_utils, or with explicit logger calls.
|
|
|
|
Check 2 — Bare .close() in finally/except blocks:
|
|
Detects `obj.close()` calls inside finally/except blocks that are not wrapped
|
|
in safe_close(). If .close() raises, it masks the original exception.
|
|
"""
|
|
|
|
import ast
|
|
import sys
|
|
|
|
# Files/patterns that are excluded from this check
|
|
ALLOWED_PATTERNS = {
|
|
"tests/",
|
|
"test_",
|
|
"_test.py",
|
|
# LLM wrapper close() methods delegate to _close_base_llm which has
|
|
# selective httpx-client introspection logic — not a simple .close() call.
|
|
"config/llm_config.py",
|
|
"rate_limiting/llm/wrapper.py",
|
|
}
|
|
|
|
|
|
class SilentCleanupChecker(ast.NodeVisitor):
|
|
"""AST visitor to detect silent exceptions in close() methods."""
|
|
|
|
def __init__(self, filename: str):
|
|
self.filename = filename
|
|
self.errors = []
|
|
|
|
def visit_FunctionDef(self, node):
|
|
# Only inspect `def close(self, ...):` methods
|
|
if node.name != "close":
|
|
self.generic_visit(node)
|
|
return
|
|
|
|
# Walk the close() body looking for try/except
|
|
for child in ast.walk(node):
|
|
if not isinstance(child, ast.ExceptHandler):
|
|
continue
|
|
|
|
# Check if the handler is "except Exception:" or bare "except:"
|
|
if child.type is not None and not (
|
|
isinstance(child.type, ast.Name)
|
|
and child.type.id == "Exception"
|
|
):
|
|
continue
|
|
|
|
# Check if handler body is *only* `pass` (no logging, no raise)
|
|
if self._is_silent_handler(child):
|
|
self.errors.append(
|
|
(
|
|
child.lineno,
|
|
"Silent exception in close() method — use "
|
|
"safe_close() from utilities.resource_utils "
|
|
"or add explicit logging so cleanup failures "
|
|
"are visible.",
|
|
)
|
|
)
|
|
|
|
self.generic_visit(node)
|
|
|
|
@staticmethod
|
|
def _is_silent_handler(handler: ast.ExceptHandler) -> bool:
|
|
"""Return True if handler body is only `pass` with no logging/raise."""
|
|
for stmt in handler.body:
|
|
# pass statement — continue checking
|
|
if isinstance(stmt, ast.Pass):
|
|
continue
|
|
|
|
# raise — not silent
|
|
if isinstance(stmt, ast.Raise):
|
|
return False
|
|
|
|
# Any expression that looks like logger.something(...)
|
|
if isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Call):
|
|
func = stmt.value.func
|
|
if isinstance(func, ast.Attribute) and isinstance(
|
|
func.value, ast.Name
|
|
):
|
|
if func.value.id == "logger":
|
|
return False
|
|
|
|
# Any other statement means it's not just `pass`
|
|
return False
|
|
|
|
# If we got here, every statement was `pass`
|
|
return True
|
|
|
|
def _is_file_allowed(self) -> bool:
|
|
for pattern in ALLOWED_PATTERNS:
|
|
if pattern in self.filename:
|
|
return True
|
|
return False
|
|
|
|
|
|
class BareCloseInFinallyChecker(ast.NodeVisitor):
|
|
"""AST visitor to detect bare .close() calls in finally/except blocks.
|
|
|
|
A bare `obj.close()` in a finally or except block can mask the original
|
|
exception if .close() itself raises. Use safe_close() instead.
|
|
"""
|
|
|
|
def __init__(self, filename: str):
|
|
self.filename = filename
|
|
self.errors = []
|
|
|
|
def visit_Try(self, node):
|
|
# Check finally body
|
|
for stmt in node.finalbody:
|
|
self._check_block(stmt)
|
|
# Check except handlers
|
|
for handler in node.handlers:
|
|
for stmt in handler.body:
|
|
self._check_block(stmt)
|
|
self.generic_visit(node)
|
|
|
|
def _check_block(self, node):
|
|
"""Check a single statement for bare .close() calls.
|
|
|
|
Recurses into if/elif/else bodies but stops at nested try blocks
|
|
(which would provide their own exception protection).
|
|
"""
|
|
# Direct .close() call as a statement
|
|
if self._is_bare_close(node):
|
|
self.errors.append(
|
|
(
|
|
node.lineno,
|
|
"Bare .close() in finally/except block — if "
|
|
".close() raises, it masks the original exception. "
|
|
"Use safe_close() from utilities.resource_utils.",
|
|
)
|
|
)
|
|
return
|
|
|
|
# Recurse into if/elif/else bodies (common pattern: if x is not None: x.close())
|
|
if isinstance(node, ast.If):
|
|
for stmt in node.body:
|
|
self._check_block(stmt)
|
|
for stmt in node.orelse:
|
|
self._check_block(stmt)
|
|
|
|
# Names that are not resource handles — .close() on these is safe
|
|
_SAFE_CLOSE_NAMES = {"plt", "figure", "fig", "ax"}
|
|
|
|
@classmethod
|
|
def _is_bare_close(cls, node) -> bool:
|
|
"""Return True if node is an expression statement calling .close()."""
|
|
if not isinstance(node, ast.Expr):
|
|
return False
|
|
if not isinstance(node.value, ast.Call):
|
|
return False
|
|
call = node.value
|
|
if not (
|
|
isinstance(call.func, ast.Attribute)
|
|
and call.func.attr == "close"
|
|
and not call.args
|
|
and not call.keywords
|
|
):
|
|
return False
|
|
# Skip known-safe names (e.g. plt.close(), cursor.close())
|
|
if isinstance(call.func.value, ast.Name):
|
|
if call.func.value.id in cls._SAFE_CLOSE_NAMES:
|
|
return False
|
|
return True
|
|
|
|
|
|
def check_file(filename: str) -> bool:
|
|
"""Check a single Python file for silent cleanup patterns."""
|
|
if not filename.endswith(".py"):
|
|
return True
|
|
|
|
# Skip allowed files
|
|
for pattern in ALLOWED_PATTERNS:
|
|
if pattern in filename:
|
|
return True
|
|
|
|
try:
|
|
with open(filename, "r", encoding="utf-8") as f:
|
|
content = f.read()
|
|
except Exception as e:
|
|
print(f"Error reading {filename}: {e}")
|
|
return False
|
|
|
|
try:
|
|
tree = ast.parse(content, filename=filename)
|
|
|
|
checker = SilentCleanupChecker(filename)
|
|
checker.visit(tree)
|
|
|
|
bare_checker = BareCloseInFinallyChecker(filename)
|
|
bare_checker.visit(tree)
|
|
|
|
all_errors = checker.errors + bare_checker.errors
|
|
if all_errors:
|
|
print(f"\n{filename}:")
|
|
for line_num, error in sorted(all_errors):
|
|
print(f" Line {line_num}: {error}")
|
|
return False
|
|
|
|
except SyntaxError:
|
|
pass
|
|
except Exception as e:
|
|
print(f"Error parsing {filename}: {e}")
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def main():
|
|
"""Main function to check all provided Python files."""
|
|
if len(sys.argv) < 2:
|
|
print("Usage: check-silent-cleanup.py <file1> <file2> ...")
|
|
sys.exit(1)
|
|
|
|
files_to_check = sys.argv[1:]
|
|
has_errors = False
|
|
|
|
for filename in files_to_check:
|
|
if not check_file(filename):
|
|
has_errors = True
|
|
|
|
if has_errors:
|
|
print("\n" + "=" * 70)
|
|
print("Silent Cleanup: Unsafe close() patterns detected!")
|
|
print("=" * 70)
|
|
print("\nTo fix, replace silent try/except blocks with safe_close():")
|
|
print(" from ...utilities.resource_utils import safe_close")
|
|
print(' safe_close(self.client, "client name")')
|
|
print("\nOr add explicit logging:")
|
|
print(' logger.warning("Failed to close resource", exc_info=True)')
|
|
sys.exit(1)
|
|
else:
|
|
sys.exit(0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|