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 typing import Any, Callable, ParamSpec, TypeVar
from defer._defer import _Defer from defer._defer import _Defer
from defer.sugar._parse import _ParseDefer
P = ParamSpec("P") P = ParamSpec("P")
T = TypeVar("T") T = TypeVar("T")
@@ -11,7 +10,7 @@ T = TypeVar("T")
class Defer: class Defer:
def __call__(self, fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs): def __call__(self, fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs):
if sys.gettrace() is None: if sys.gettrace() is None:
sys.settrace(_ParseDefer.IDENTITY) sys.settrace(_DeferTrace())
frame = sys._getframe(1) frame = sys._getframe(1)
if not isinstance(frame.f_trace, _Defer): if not isinstance(frame.f_trace, _Defer):
frame.f_trace = _Defer(frame.f_trace) frame.f_trace = _Defer(frame.f_trace)
@@ -21,3 +20,9 @@ class Defer:
def __contains__(self, fn: Any): def __contains__(self, fn: Any):
breakpoint() breakpoint()
self(fn) 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 import sys
from defer.defer import Defer from defer.defer import Defer
from defer.sugar._parse import _ParseDefer
def install(): def install():
if not isinstance(sys.gettrace(), _ParseDefer): """Only loads markers - does NOT install trace. Trace is lazy."""
sys.settrace(_ParseDefer(sys.gettrace())) from defer.sugar._parse import _load_defer_markers
_load_defer_markers()
defer = Defer() defer = Defer()
+32 -84
View File
@@ -1,108 +1,56 @@
import sys import sys
import os import os
import ast
from ast import ( from ast import (
AsyncFunctionDef, AsyncFunctionDef,
FunctionDef, FunctionDef,
) )
from collections import deque
from types import FrameType from types import FrameType
from typing import Any, Callable, Optional, cast from typing import Any, Callable, Optional
from defer.errors import FreeVarsError from defer.errors import FreeVarsError
from defer.sugar.transformer import RewriteDefer from defer.sugar.transformer import RewriteDefer
def _is_dunder(name: str) -> bool: _DEFER_MARKER = "# defer: parse"
return name.startswith("__") and name.endswith("__") _enabled_files: set[str] = set()
_SITE_PACKAGES = "site-packages" def _load_defer_markers():
_VENV = ".venv" global _enabled_files
_THIRDPARTY = "thirdparty" _enabled_files.clear()
_SRC_WIN = "\\src\\" try:
_SRC_UNIX = "/src/" 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: def install():
if not filename: """Only loads markers - does NOT install trace. Trace is lazy."""
return False _load_defer_markers()
if _SRC_WIN in filename or _SRC_UNIX in filename:
return True
return False
class _ParseDefer: 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: def __init__(self, tracefn: Optional[Callable]) -> None:
self.tracefn = tracefn or self.IDENTITY self.tracefn = tracefn or self.IDENTITY
self.pending: deque[Executing] = deque()
def __call__(self, frame: FrameType, event: str, arg: Any): def __call__(self, frame: FrameType, event: str, arg: Any):
self.tracefn(frame, event, arg) return 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