Signals Integration (#2160)
* Update some tests * Resolve #2122 route decorator returning tuple * Use rc sanic-routing version * Update unit tests to <:str> * Minimal working version with some signals implemented * Add more http signals * Update ASGI and change listeners to signals * Allow for dynamic ODE signals * Allow signals to be stacked * Begin tests * Prioritize match_info on keyword argument injection * WIP on tests * Compat with signals * Work through some test coverage * Passing tests * Post linting * Setup proper resets * coverage reporting * Fixes from vltr comments * clear delayed tasks * Fix bad test * rm pycache
This commit is contained in:
8
sanic/touchup/__init__.py
Normal file
8
sanic/touchup/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from .meta import TouchUpMeta
|
||||
from .service import TouchUp
|
||||
|
||||
|
||||
__all__ = (
|
||||
"TouchUp",
|
||||
"TouchUpMeta",
|
||||
)
|
||||
22
sanic/touchup/meta.py
Normal file
22
sanic/touchup/meta.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from sanic.exceptions import SanicException
|
||||
|
||||
from .service import TouchUp
|
||||
|
||||
|
||||
class TouchUpMeta(type):
|
||||
def __new__(cls, name, bases, attrs, **kwargs):
|
||||
gen_class = super().__new__(cls, name, bases, attrs, **kwargs)
|
||||
|
||||
methods = attrs.get("__touchup__")
|
||||
attrs["__touched__"] = False
|
||||
if methods:
|
||||
|
||||
for method in methods:
|
||||
if method not in attrs:
|
||||
raise SanicException(
|
||||
"Cannot perform touchup on non-existent method: "
|
||||
f"{name}.{method}"
|
||||
)
|
||||
TouchUp.register(gen_class, method)
|
||||
|
||||
return gen_class
|
||||
5
sanic/touchup/schemes/__init__.py
Normal file
5
sanic/touchup/schemes/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from .base import BaseScheme
|
||||
from .ode import OptionalDispatchEvent # noqa
|
||||
|
||||
|
||||
__all__ = ("BaseScheme",)
|
||||
20
sanic/touchup/schemes/base.py
Normal file
20
sanic/touchup/schemes/base.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Set, Type
|
||||
|
||||
|
||||
class BaseScheme(ABC):
|
||||
ident: str
|
||||
_registry: Set[Type] = set()
|
||||
|
||||
def __init__(self, app) -> None:
|
||||
self.app = app
|
||||
|
||||
@abstractmethod
|
||||
def run(self, method, module_globals) -> None:
|
||||
...
|
||||
|
||||
def __init_subclass__(cls):
|
||||
BaseScheme._registry.add(cls)
|
||||
|
||||
def __call__(self, method, module_globals):
|
||||
return self.run(method, module_globals)
|
||||
67
sanic/touchup/schemes/ode.py
Normal file
67
sanic/touchup/schemes/ode.py
Normal file
@@ -0,0 +1,67 @@
|
||||
from ast import Attribute, Await, Dict, Expr, NodeTransformer, parse
|
||||
from inspect import getsource
|
||||
from textwrap import dedent
|
||||
from typing import Any
|
||||
|
||||
from sanic.log import logger
|
||||
|
||||
from .base import BaseScheme
|
||||
|
||||
|
||||
class OptionalDispatchEvent(BaseScheme):
|
||||
ident = "ODE"
|
||||
|
||||
def __init__(self, app) -> None:
|
||||
super().__init__(app)
|
||||
|
||||
self._registered_events = [
|
||||
signal.path for signal in app.signal_router.routes
|
||||
]
|
||||
|
||||
def run(self, method, module_globals):
|
||||
raw_source = getsource(method)
|
||||
src = dedent(raw_source)
|
||||
tree = parse(src)
|
||||
node = RemoveDispatch(self._registered_events).visit(tree)
|
||||
compiled_src = compile(node, method.__name__, "exec")
|
||||
exec_locals: Dict[str, Any] = {}
|
||||
exec(compiled_src, module_globals, exec_locals) # nosec
|
||||
|
||||
return exec_locals[method.__name__]
|
||||
|
||||
|
||||
class RemoveDispatch(NodeTransformer):
|
||||
def __init__(self, registered_events) -> None:
|
||||
self._registered_events = registered_events
|
||||
|
||||
def visit_Expr(self, node: Expr) -> Any:
|
||||
call = node.value
|
||||
if isinstance(call, Await):
|
||||
call = call.value
|
||||
|
||||
func = getattr(call, "func", None)
|
||||
args = getattr(call, "args", None)
|
||||
if not func or not args:
|
||||
return node
|
||||
|
||||
if isinstance(func, Attribute) and func.attr == "dispatch":
|
||||
event = args[0]
|
||||
if hasattr(event, "s"):
|
||||
event_name = getattr(event, "value", event.s)
|
||||
if self._not_registered(event_name):
|
||||
logger.debug(f"Disabling event: {event_name}")
|
||||
return None
|
||||
return node
|
||||
|
||||
def _not_registered(self, event_name):
|
||||
dynamic = []
|
||||
for event in self._registered_events:
|
||||
if event.endswith(">"):
|
||||
namespace_concern, _ = event.rsplit(".", 1)
|
||||
dynamic.append(namespace_concern)
|
||||
|
||||
namespace_concern, _ = event_name.rsplit(".", 1)
|
||||
return (
|
||||
event_name not in self._registered_events
|
||||
and namespace_concern not in dynamic
|
||||
)
|
||||
33
sanic/touchup/service.py
Normal file
33
sanic/touchup/service.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from inspect import getmembers, getmodule
|
||||
from typing import Set, Tuple, Type
|
||||
|
||||
from .schemes import BaseScheme
|
||||
|
||||
|
||||
class TouchUp:
|
||||
_registry: Set[Tuple[Type, str]] = set()
|
||||
|
||||
@classmethod
|
||||
def run(cls, app):
|
||||
for target, method_name in cls._registry:
|
||||
method = getattr(target, method_name)
|
||||
|
||||
if app.test_mode:
|
||||
placeholder = f"_{method_name}"
|
||||
if hasattr(target, placeholder):
|
||||
method = getattr(target, placeholder)
|
||||
else:
|
||||
setattr(target, placeholder, method)
|
||||
|
||||
module = getmodule(target)
|
||||
module_globals = dict(getmembers(module))
|
||||
|
||||
for scheme in BaseScheme._registry:
|
||||
modified = scheme(app)(method, module_globals)
|
||||
setattr(target, method_name, modified)
|
||||
|
||||
target.__touched__ = True
|
||||
|
||||
@classmethod
|
||||
def register(cls, target, method_name):
|
||||
cls._registry.add((target, method_name))
|
||||
Reference in New Issue
Block a user