Skip to content

Commit

Permalink
Allow early Blueprint registrations to still apply later added objects (
Browse files Browse the repository at this point in the history
  • Loading branch information
ahopkins committed Nov 17, 2021
1 parent b731a6b commit 85e7b71
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 38 deletions.
4 changes: 4 additions & 0 deletions sanic/app.py
Expand Up @@ -72,6 +72,7 @@
FutureException,
FutureListener,
FutureMiddleware,
FutureRegistry,
FutureRoute,
FutureSignal,
FutureStatic,
Expand Down Expand Up @@ -115,6 +116,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
"_future_exceptions",
"_future_listeners",
"_future_middleware",
"_future_registry",
"_future_routes",
"_future_signals",
"_future_statics",
Expand Down Expand Up @@ -187,6 +189,7 @@ def __init__(
self._test_manager: Any = None
self._blueprint_order: List[Blueprint] = []
self._delayed_tasks: List[str] = []
self._future_registry: FutureRegistry = FutureRegistry()
self._state: ApplicationState = ApplicationState(app=self)
self.blueprints: Dict[str, Blueprint] = {}
self.config: Config = config or Config(
Expand Down Expand Up @@ -1625,6 +1628,7 @@ def signalize(self):
raise e

async def _startup(self):
self._future_registry.clear()
self.signalize()
self.finalize()
ErrorHandler.finalize(self.error_handler)
Expand Down
128 changes: 93 additions & 35 deletions sanic/blueprints.py
Expand Up @@ -4,7 +4,9 @@

from collections import defaultdict
from copy import deepcopy
from enum import Enum
from functools import wraps
from inspect import isfunction
from itertools import chain
from types import SimpleNamespace
from typing import (
TYPE_CHECKING,
Expand All @@ -13,7 +15,9 @@
Iterable,
List,
Optional,
Sequence,
Set,
Tuple,
Union,
)

Expand All @@ -36,6 +40,32 @@
from sanic import Sanic # noqa


def lazy(func, as_decorator=True):
@wraps(func)
def decorator(bp, *args, **kwargs):
nonlocal as_decorator
kwargs["apply"] = False
pass_handler = None

if args and isfunction(args[0]):
as_decorator = False

def wrapper(handler):
future = func(bp, *args, **kwargs)
if as_decorator:
future = future(handler)

if bp.registered:
for app in bp.apps:
bp.register(app, {})

return future

return wrapper if as_decorator else wrapper(pass_handler)

return decorator


class Blueprint(BaseSanic):
"""
In *Sanic* terminology, a **Blueprint** is a logical collection of
Expand Down Expand Up @@ -125,29 +155,16 @@ def apps(self):
)
return self._apps

def route(self, *args, **kwargs):
kwargs["apply"] = False
return super().route(*args, **kwargs)

def static(self, *args, **kwargs):
kwargs["apply"] = False
return super().static(*args, **kwargs)

def middleware(self, *args, **kwargs):
kwargs["apply"] = False
return super().middleware(*args, **kwargs)

def listener(self, *args, **kwargs):
kwargs["apply"] = False
return super().listener(*args, **kwargs)

def exception(self, *args, **kwargs):
kwargs["apply"] = False
return super().exception(*args, **kwargs)
@property
def registered(self) -> bool:
return bool(self._apps)

def signal(self, event: Union[str, Enum], *args, **kwargs):
kwargs["apply"] = False
return super().signal(event, *args, **kwargs)
exception = lazy(BaseSanic.exception)
listener = lazy(BaseSanic.listener)
middleware = lazy(BaseSanic.middleware)
route = lazy(BaseSanic.route)
signal = lazy(BaseSanic.signal)
static = lazy(BaseSanic.static, as_decorator=False)

def reset(self):
self._apps: Set[Sanic] = set()
Expand Down Expand Up @@ -284,6 +301,7 @@ def register(self, app, options):
middleware = []
exception_handlers = []
listeners = defaultdict(list)
registered = set()

# Routes
for future in self._future_routes:
Expand All @@ -310,12 +328,15 @@ def register(self, app, options):
)

name = app._generate_name(future.name)
host = future.host or self.host
if isinstance(host, list):
host = tuple(host)

apply_route = FutureRoute(
future.handler,
uri[1:] if uri.startswith("//") else uri,
future.methods,
future.host or self.host,
host,
strict_slashes,
future.stream,
version,
Expand All @@ -329,6 +350,10 @@ def register(self, app, options):
error_format,
)

if (self, apply_route) in app._future_registry:
continue

registered.add(apply_route)
route = app._apply_route(apply_route)
operation = (
routes.extend if isinstance(route, list) else routes.append
Expand All @@ -340,6 +365,11 @@ def register(self, app, options):
# Prepend the blueprint URI prefix if available
uri = url_prefix + future.uri if url_prefix else future.uri
apply_route = FutureStatic(uri, *future[1:])

if (self, apply_route) in app._future_registry:
continue

registered.add(apply_route)
route = app._apply_static(apply_route)
routes.append(route)

Expand All @@ -348,30 +378,51 @@ def register(self, app, options):
if route_names:
# Middleware
for future in self._future_middleware:
if (self, future) in app._future_registry:
continue
middleware.append(app._apply_middleware(future, route_names))

# Exceptions
for future in self._future_exceptions:
if (self, future) in app._future_registry:
continue
exception_handlers.append(
app._apply_exception_handler(future, route_names)
)

# Event listeners
for listener in self._future_listeners:
listeners[listener.event].append(app._apply_listener(listener))
for future in self._future_listeners:
if (self, future) in app._future_registry:
continue
listeners[future.event].append(app._apply_listener(future))

# Signals
for signal in self._future_signals:
signal.condition.update({"blueprint": self.name})
app._apply_signal(signal)

self.routes = [route for route in routes if isinstance(route, Route)]
self.websocket_routes = [
for future in self._future_signals:
if (self, future) in app._future_registry:
continue
future.condition.update({"blueprint": self.name})
app._apply_signal(future)

self.routes += [route for route in routes if isinstance(route, Route)]
self.websocket_routes += [
route for route in self.routes if route.ctx.websocket
]
self.middlewares = middleware
self.exceptions = exception_handlers
self.listeners = dict(listeners)
self.middlewares += middleware
self.exceptions += exception_handlers
self.listeners.update(dict(listeners))

if self.registered:
self.register_futures(
self.apps,
self,
chain(
registered,
self._future_middleware,
self._future_exceptions,
self._future_listeners,
self._future_signals,
),
)

async def dispatch(self, *args, **kwargs):
condition = kwargs.pop("condition", {})
Expand Down Expand Up @@ -403,3 +454,10 @@ def _extract_value(*values):
value = v
break
return value

@staticmethod
def register_futures(
apps: Set[Sanic], bp: Blueprint, futures: Sequence[Tuple[Any, ...]]
):
for app in apps:
app._future_registry.update(set((bp, item) for item in futures))
4 changes: 4 additions & 0 deletions sanic/models/futures.py
Expand Up @@ -60,3 +60,7 @@ class FutureSignal(NamedTuple):
handler: SignalHandler
event: str
condition: Optional[Dict[str, str]]


class FutureRegistry(set):
...
4 changes: 1 addition & 3 deletions tests/test_blueprint_copy.py
@@ -1,6 +1,4 @@
from copy import deepcopy

from sanic import Blueprint, Sanic, blueprints, response
from sanic import Blueprint, Sanic
from sanic.response import text


Expand Down
28 changes: 28 additions & 0 deletions tests/test_blueprints.py
Expand Up @@ -1088,3 +1088,31 @@ def test_bp_set_attribute_warning():
"and will be removed in version 21.12. You should change your "
"Blueprint instance to use instance.ctx.foo instead."
)


def test_early_registration(app):
assert len(app.router.routes) == 0

bp = Blueprint("bp")

@bp.get("/one")
async def one(_):
return text("one")

app.blueprint(bp)

assert len(app.router.routes) == 1

@bp.get("/two")
async def two(_):
return text("two")

@bp.get("/three")
async def three(_):
return text("three")

assert len(app.router.routes) == 3

for path in ("one", "two", "three"):
_, response = app.test_client.get(f"/{path}")
assert response.text == path

0 comments on commit 85e7b71

Please sign in to comment.