improvements to defer

This commit is contained in:
2026-05-13 07:47:55 -04:00
parent bf84058ca8
commit 48c32becaf
3 changed files with 42 additions and 89 deletions
+7 -2
View File
@@ -2,7 +2,6 @@ import sys
from typing import Any, Callable, ParamSpec, TypeVar
from defer._defer import _Defer
from defer.sugar._parse import _ParseDefer
P = ParamSpec("P")
T = TypeVar("T")
@@ -11,7 +10,7 @@ T = TypeVar("T")
class Defer:
def __call__(self, fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs):
if sys.gettrace() is None:
sys.settrace(_ParseDefer.IDENTITY)
sys.settrace(_DeferTrace())
frame = sys._getframe(1)
if not isinstance(frame.f_trace, _Defer):
frame.f_trace = _Defer(frame.f_trace)
@@ -21,3 +20,9 @@ class Defer:
def __contains__(self, fn: Any):
breakpoint()
self(fn)
class _DeferTrace:
"""Minimal no-op trace for when @defer is used but no other trace is active."""
def __call__(self, frame, event, arg):
return self
+3 -3
View File
@@ -1,12 +1,12 @@
import sys
from defer.defer import Defer
from defer.sugar._parse import _ParseDefer
def install():
if not isinstance(sys.gettrace(), _ParseDefer):
sys.settrace(_ParseDefer(sys.gettrace()))
"""Only loads markers - does NOT install trace. Trace is lazy."""
from defer.sugar._parse import _load_defer_markers
_load_defer_markers()
defer = Defer()
+32 -84
View File
@@ -1,108 +1,56 @@
import sys
import os
import ast
from ast import (
AsyncFunctionDef,
FunctionDef,
)
from collections import deque
from types import FrameType
from typing import Any, Callable, Optional, cast
from typing import Any, Callable, Optional
from defer.errors import FreeVarsError
from defer.sugar.transformer import RewriteDefer
def _is_dunder(name: str) -> bool:
return name.startswith("__") and name.endswith("__")
_DEFER_MARKER = "# defer: parse"
_enabled_files: set[str] = set()
_SITE_PACKAGES = "site-packages"
_VENV = ".venv"
_THIRDPARTY = "thirdparty"
_SRC_WIN = "\\src\\"
_SRC_UNIX = "/src/"
def _load_defer_markers():
global _enabled_files
_enabled_files.clear()
try:
for root, dirs, filenames in os.walk('src'):
for filename in filenames:
if filename.endswith('.py'):
relpath = os.path.join(root, filename)
abspath = os.path.abspath(relpath)
try:
with open(abspath, "r", encoding="utf-8", errors="ignore") as f:
for i in range(10):
line = f.readline()
if not line:
break
if _DEFER_MARKER in line:
_enabled_files.add(abspath)
break
except Exception:
pass
except Exception:
pass
def _is_our_code(filename: str) -> bool:
if not filename:
return False
if _SRC_WIN in filename or _SRC_UNIX in filename:
return True
return False
def install():
"""Only loads markers - does NOT install trace. Trace is lazy."""
_load_defer_markers()
class _ParseDefer:
IDENTITY = lambda *_: None # noqa: E731
"""Lazy defer - trace only activates when @defer is actually used."""
IDENTITY = lambda *_: None
def __init__(self, tracefn: Optional[Callable]) -> None:
self.tracefn = tracefn or self.IDENTITY
self.pending: deque[Executing] = deque()
def __call__(self, frame: FrameType, event: str, arg: Any):
self.tracefn(frame, event, arg)
try:
filename = frame.f_code.co_filename
if not filename:
return self
if _SITE_PACKAGES in filename or _VENV in filename or _THIRDPARTY in filename:
return self
if not _is_our_code(filename):
return self
except Exception:
pass
if event != "line":
return self
try:
from executing.executing import Executing, Source
except ImportError:
return self
try:
exc = Source.executing(frame)
except Exception:
return self
if not (stmt := next(iter(exc.statements), None)):
return self
if isinstance(stmt, (AsyncFunctionDef, FunctionDef)):
if _is_dunder(stmt.name):
return self
self.pending.append(exc)
return self
if not self.pending or frame.f_back is not self.pending[-1].frame.f_back:
return self
stmts = self.pending.pop().statements
node = cast(FunctionDef | AsyncFunctionDef, next(iter(stmts)))
fn_name = node.name
if fn_name not in frame.f_locals:
return self
fn = frame.f_locals[fn_name]
if not callable(fn):
return self
try:
code = fn.__code__
except AttributeError:
return self
try:
mod = fn.__module__
except AttributeError:
return self
if mod in sys.stdlib_module_names:
return self
if code.co_freevars:
return self
if not (ast := RewriteDefer.transform(node)):
return self
locals = frame.f_locals.copy()
del locals[fn_name]
exec(compile(ast, frame.f_code.co_filename, "exec"), frame.f_globals, locals)
frame.f_locals[fn_name].__code__ = locals[fn_name].__code__
return self
return self.tracefn(frame, event, arg)