Repository: sanic-org/sanic

Issue:
log a warning or error when adding something to blueprint after adding it to application
**Is your feature request related to a problem? Please describe.**
when i add a blueprint to the sanic app, and then add routes to it, those routes dont work. i have to add the blueprint to the app after adding routes. but there is no indication that im doing something wrong if i do it the wrong way


**Describe the solution you'd like**
log a warning or error message telling the programmer that they added routes to a blueprint after adding it to the app, and that those routes will not work. or straight up throw an error



Discussion:
:100: 

This should be the case for any Blueprint addition (routes, listeners, middleware, exception handlers, signals).
I might give this one a try tomorrow or on Monday 
Or better yet, make the blueprint changes work even after added to an app, if doing so is feasible.
> Or better yet, make the blueprint changes work even after added to an app, if doing so is feasible.

This could be a bit messy, and I believe would require a bit of a refactor from how the decorators work. I think it also breaks the principle of what the Blueprint is, but perhaps could be doable with something like this:

```python
    def route(self, *args, **kwargs):
        kwargs["apply"] = False
        sup = super().route

        def wrapper(handler):
            retval = sup(*args, **kwargs)(handler)
            for app in self.apps:
                self.register(app, {})
            return retval

        return wrapper
```

If we are going this direction, then we need to definitely beef up the unit testing around this, and probably make all of these methods (route, static, middleware, exception, signal) perhaps a little more DRY.
@prryplatypus I am going to take this one myself. I hope you do not mind.

Resolving patch:
diff --git a/sanic/app.py b/sanic/app.py
--- a/sanic/app.py
+++ b/sanic/app.py
@@ -72,6 +72,7 @@
     FutureException,
     FutureListener,
     FutureMiddleware,
+    FutureRegistry,
     FutureRoute,
     FutureSignal,
     FutureStatic,
@@ -115,6 +116,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
         "_future_exceptions",
         "_future_listeners",
         "_future_middleware",
+        "_future_registry",
         "_future_routes",
         "_future_signals",
         "_future_statics",
@@ -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(
@@ -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)
diff --git a/sanic/blueprints.py b/sanic/blueprints.py
--- a/sanic/blueprints.py
+++ b/sanic/blueprints.py
@@ -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,
@@ -13,7 +15,9 @@
     Iterable,
     List,
     Optional,
+    Sequence,
     Set,
+    Tuple,
     Union,
 )
 
@@ -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
@@ -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()
@@ -284,6 +301,7 @@ def register(self, app, options):
         middleware = []
         exception_handlers = []
         listeners = defaultdict(list)
+        registered = set()
 
         # Routes
         for future in self._future_routes:
@@ -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,
@@ -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
@@ -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)
 
@@ -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", {})
@@ -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))
diff --git a/sanic/models/futures.py b/sanic/models/futures.py
--- a/sanic/models/futures.py
+++ b/sanic/models/futures.py
@@ -60,3 +60,7 @@ class FutureSignal(NamedTuple):
     handler: SignalHandler
     event: str
     condition: Optional[Dict[str, str]]
+
+
+class FutureRegistry(set):
+    ...


Test patch:
diff --git a/tests/test_blueprint_copy.py b/tests/test_blueprint_copy.py
--- a/tests/test_blueprint_copy.py
+++ b/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
 
 
diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py
--- a/tests/test_blueprints.py
+++ b/tests/test_blueprints.py
@@ -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