import ast from pathlib import Path def test_no_sync_db_calls_in_async_functions(): """Регресс п.9 ревью: в async-функциях app/ не должно быть прямых вызовов синхронных функций, обращающихся к БД (session_scope) — напрямую или через один уровень вызовов, — это блокирует event loop. Правильный способ — asyncio.to_thread(fn, ...). Ссылка на функцию, переданная в asyncio.to_thread(fn, ...), вызовом не считается; тела вложенных def/async def в область видимости внешней функции не входят. Единственное исключение — app.main.lifespan (выполняется до приёма запросов).""" app_dir = Path(__file__).resolve().parent.parent / "app" allowed = {"lifespan"} class _OwnScope(ast.NodeVisitor): """Вызовы прямо в теле функции, не заходя в тела вложенных def/async def.""" def __init__(self): self.calls: list[ast.Call] = [] def visit_FunctionDef(self, node): # не спускаемся во вложенную функцию pass def visit_AsyncFunctionDef(self, node): pass def visit_Call(self, node): self.calls.append(node) self.generic_visit(node) def own_calls(func) -> list[ast.Call]: v = _OwnScope() for stmt in func.body: v.visit(stmt) return v.calls def call_name(call: ast.Call) -> str | None: f = call.func if isinstance(f, ast.Name): return f.id if isinstance(f, ast.Attribute): return f.attr return None sync_funcs, async_funcs = [], [] for path in sorted(app_dir.rglob("*.py")): tree = ast.parse(path.read_text(), filename=str(path)) for node in ast.walk(tree): if isinstance(node, ast.AsyncFunctionDef): async_funcs.append((path, node)) elif isinstance(node, ast.FunctionDef): sync_funcs.append((path, node)) # функции, обращающиеся к БД напрямую, и функции, вызывающие их (один уровень) level0 = {f.name for _, f in sync_funcs if any(call_name(c) == "session_scope" for c in own_calls(f))} level1 = {f.name for _, f in sync_funcs if f.name not in level0 and any(call_name(c) in level0 for c in own_calls(f))} dangerous = level0 | level1 violations = [] for path, func in async_funcs: if func.name in allowed: continue for c in own_calls(func): name = call_name(c) if name in dangerous: violations.append(f"{path.relative_to(app_dir.parent)}::{func.name} вызывает {name}() напрямую, " "в обход asyncio.to_thread — обращение к БД блокирует event loop") assert not violations, "\n".join(violations)