164 lines
6.6 KiB
Python
164 lines
6.6 KiB
Python
"""Seam for the server.py handler/helper split.
|
|
|
|
server.py's JSON-RPC handlers and many helpers close over its module globals
|
|
(``_sessions``, ``_ok``, ``_err``, config helpers, ...). To move them out
|
|
without rewriting a single body, each split module defines its code normally
|
|
and server.py calls :func:`bind_module` at the end of its own import, once
|
|
every global the code closes over exists. Function bodies are re-created with
|
|
``types.FunctionType`` against server.py's namespace, so they stay
|
|
byte-identical and ``global X`` statements keep mutating server.py state.
|
|
|
|
No import cycle: split modules never import server at module level — server
|
|
imports them and passes itself in.
|
|
"""
|
|
|
|
import contextlib
|
|
import types
|
|
|
|
# contextlib.contextmanager wraps the generator; rebind the generator (found via
|
|
# __wrapped__) and re-wrap, otherwise only the wrapper would see server globals.
|
|
_CM_HELPER_CODE = contextlib.contextmanager(lambda: (yield)).__code__
|
|
|
|
|
|
def rebind(fn, g: dict, _seen=None):
|
|
"""Copy ``fn`` with globals ``g``.
|
|
|
|
Closure cells holding functions from the same module are rebound too, so
|
|
handlers produced by import-time decorator factories keep working.
|
|
"""
|
|
_seen = {} if _seen is None else _seen
|
|
if id(fn) in _seen:
|
|
return _seen[id(fn)]
|
|
wrapped = getattr(fn, "__wrapped__", None)
|
|
if wrapped is not None and fn.__code__ is _CM_HELPER_CODE:
|
|
return contextlib.contextmanager(rebind(wrapped, g, _seen))
|
|
closure = fn.__closure__
|
|
if closure:
|
|
cells = []
|
|
for cell in closure:
|
|
try:
|
|
val = cell.cell_contents
|
|
except ValueError: # empty cell
|
|
cells.append(cell)
|
|
continue
|
|
if isinstance(val, types.FunctionType) and val.__module__ == fn.__module__:
|
|
cells.append(types.CellType(rebind(val, g, _seen)))
|
|
else:
|
|
cells.append(cell)
|
|
closure = tuple(cells)
|
|
real = types.FunctionType(fn.__code__, g, fn.__name__, fn.__defaults__, closure)
|
|
real.__kwdefaults__ = fn.__kwdefaults__
|
|
real.__doc__ = fn.__doc__
|
|
real.__dict__.update(fn.__dict__)
|
|
_seen[id(fn)] = real
|
|
return real
|
|
|
|
|
|
class HandlerRegistry:
|
|
"""Deferred @method registrar used by the split modules."""
|
|
|
|
def __init__(self) -> None:
|
|
self._pending: list[tuple[str, types.FunctionType]] = []
|
|
|
|
def method(self, name: str):
|
|
"""Drop-in for server.py's ``@method`` decorator (defers registration)."""
|
|
|
|
def dec(fn):
|
|
self._pending.append((name, fn))
|
|
return fn
|
|
|
|
return dec
|
|
|
|
def profile_scoped(self, fn):
|
|
"""Drop-in for server.py's ``@_profile_scoped`` (applied at install)."""
|
|
fn._hermes_profile_scoped = True
|
|
return fn
|
|
|
|
def install(self, server) -> None:
|
|
"""Rebind pending handlers onto ``server``'s globals and register them."""
|
|
g = vars(server)
|
|
for name, fn in self._pending:
|
|
real = rebind(fn, g)
|
|
if getattr(fn, "_hermes_profile_scoped", False):
|
|
real = server._profile_scoped(real)
|
|
server._methods[name] = real
|
|
|
|
|
|
_PLUMBING = {"HandlerRegistry", "method", "_profile_scoped", "register", "rebind", "logger"}
|
|
|
|
|
|
def bind_module(module_globals: dict, server, *, skip=()) -> None:
|
|
"""Publish everything a split module defines onto ``server``, rebound to its globals.
|
|
|
|
``module_globals`` is the caller's ``globals()`` (not ``sys.modules[__name__]``:
|
|
tests that ``patch.dict(sys.modules)`` around the server import drop the
|
|
submodule entries while the package attribute survives, so a re-import would
|
|
KeyError). Functions are rebound; classes get their methods rebound in place;
|
|
other values (constants, ``global``-mutated state seeds) are copied as-is.
|
|
Imported modules/functions, dunders and registry plumbing are skipped, so a
|
|
split module needs no hand-maintained export list. Dispatch tables (dicts
|
|
whose values are this module's functions) get their values rebound too.
|
|
Finally the module's ``_registry`` (if any) installs its @method handlers.
|
|
"""
|
|
g = vars(server)
|
|
mod_name = module_globals["__name__"]
|
|
seen: dict = {}
|
|
|
|
def _own_fn(v):
|
|
return isinstance(v, types.FunctionType) and v.__module__ == mod_name
|
|
|
|
def _rebind_in(v):
|
|
"""Rebind own functions nested in dict/tuple/list constants (dispatch tables)."""
|
|
if _own_fn(v):
|
|
return rebind(v, g, seen)
|
|
if isinstance(v, dict):
|
|
return {k: _rebind_in(x) for k, x in v.items()}
|
|
if isinstance(v, (tuple, list)):
|
|
return type(v)(_rebind_in(x) for x in v)
|
|
return v
|
|
|
|
def _has_own_fn(v):
|
|
if _own_fn(v):
|
|
return True
|
|
if isinstance(v, dict):
|
|
return any(_has_own_fn(x) for x in v.values())
|
|
if isinstance(v, (tuple, list)):
|
|
return any(_has_own_fn(x) for x in v)
|
|
return False
|
|
|
|
for name, obj in list(module_globals.items()):
|
|
if name.startswith("__") or name in _PLUMBING or name in skip:
|
|
continue
|
|
if isinstance(obj, (types.ModuleType, HandlerRegistry)):
|
|
continue
|
|
if isinstance(obj, types.FunctionType):
|
|
if obj.__module__ != mod_name:
|
|
if name == obj.__name__:
|
|
continue # plain import; server already has its own
|
|
# ``_alias = other_module.fn`` — publish as-is, no rebind
|
|
else:
|
|
obj = rebind(obj, g, seen)
|
|
elif isinstance(obj, (dict, tuple, list)) and _has_own_fn(obj):
|
|
obj = _rebind_in(obj)
|
|
module_globals[name] = obj # keep the split module's own view consistent
|
|
elif isinstance(obj, type):
|
|
if obj.__module__ != mod_name:
|
|
continue
|
|
for attr, val in list(vars(obj).items()):
|
|
if isinstance(val, types.FunctionType):
|
|
setattr(obj, attr, rebind(val, g))
|
|
elif isinstance(val, (staticmethod, classmethod)):
|
|
setattr(obj, attr, type(val)(rebind(val.__func__, g)))
|
|
prev = g.get(name)
|
|
if isinstance(prev, types.FunctionType) and isinstance(obj, types.FunctionType):
|
|
owner = getattr(prev, "_hermes_split_module", None)
|
|
if owner and owner != mod_name:
|
|
raise RuntimeError(
|
|
f"split-module name collision: {mod_name}.{name} would overwrite {owner}.{name}"
|
|
)
|
|
obj._hermes_split_module = mod_name
|
|
setattr(server, name, obj)
|
|
registry = module_globals.get("_registry")
|
|
if isinstance(registry, HandlerRegistry):
|
|
registry.install(server)
|