diff --git a/docs/sanic/getting_started.rst b/docs/sanic/getting_started.rst index 97613360..a781e43a 100644 --- a/docs/sanic/getting_started.rst +++ b/docs/sanic/getting_started.rst @@ -60,3 +60,26 @@ Open the address `http://0.0.0.0:8000 `_ in your web browse the message *Hello world!*. You now have a working Sanic server! + +5. Application registry +----------------------- + +When you instantiate a Sanic instance, that can be retrieved at a later time from the Sanic app registry. This can be useful, for example, if you need to access your Sanic instance from a location where it is not otherwise accessible. + +.. code-block:: python + + # ./path/to/server.py + from sanic import Sanic + + app = Sanic("my_awesome_server") + + # ./path/to/somewhere_else.py + from sanic import Sanic + + app = Sanic.get_app("my_awesome_server") + +If you call ``Sanic.get_app("non-existing")`` on an app that does not exist, it will raise ``SanicException`` by default. You can, instead, force the method to return a new instance of ``Sanic`` with that name: + +.. code-block:: python + + app = Sanic.get_app("my_awesome_server", force_create=True) diff --git a/sanic/app.py b/sanic/app.py index 15a0d18e..81cd66d1 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -2,12 +2,11 @@ import logging import logging.config import os import re -import warnings from asyncio import CancelledError, Protocol, ensure_future, get_event_loop from collections import defaultdict, deque from functools import partial -from inspect import getmodulename, isawaitable, signature, stack +from inspect import isawaitable, signature from socket import socket from ssl import Purpose, SSLContext, create_default_context from traceback import format_exc @@ -38,6 +37,9 @@ from sanic.websocket import ConnectionClosed, WebSocketProtocol class Sanic: + _app_registry: Dict[str, "Sanic"] = {} + test_mode = False + def __init__( self, name=None, @@ -52,15 +54,10 @@ class Sanic: # Get name from previous stack frame if name is None: - warnings.warn( - "Sanic(name=None) is deprecated and None value support " - "for `name` will be removed in the next release. " + raise SanicException( + "Sanic instance cannot be unnamed. " "Please use Sanic(name='your_application_name') instead.", - DeprecationWarning, - stacklevel=2, ) - frame_records = stack()[1] - name = getmodulename(frame_records[1]) # logging if configure_logging: @@ -90,7 +87,8 @@ class Sanic: self.named_response_middleware = {} # Register alternative method names self.go_fast = self.run - self.test_mode = False + + self.__class__.register_app(self) @property def loop(self): @@ -1401,9 +1399,36 @@ class Sanic: # -------------------------------------------------------------------- # # Configuration # -------------------------------------------------------------------- # + def update_config(self, config: Union[bytes, str, dict, Any]): """Update app.config. Please refer to config.py::Config.update_config for documentation.""" self.config.update_config(config) + + # -------------------------------------------------------------------- # + # Class methods + # -------------------------------------------------------------------- # + + @classmethod + def register_app(cls, app: "Sanic") -> None: + """Register a Sanic instance""" + if not isinstance(app, cls): + raise SanicException("Registered app must be an instance of Sanic") + + name = app.name + if name in cls._app_registry and not cls.test_mode: + raise SanicException(f'Sanic app name "{name}" already in use.') + + cls._app_registry[name] = app + + @classmethod + def get_app(cls, name: str, *, force_create: bool = False) -> "Sanic": + """Retrieve an instantiated Sanic instance""" + try: + return cls._app_registry[name] + except KeyError: + if force_create: + return cls(name) + raise SanicException(f'Sanic app name "{name}" not found.') diff --git a/tests/conftest.py b/tests/conftest.py index 45963863..3d57ac73 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,6 +11,7 @@ from sanic.router import RouteExists, Router random.seed("Pack my box with five dozen liquor jugs.") +Sanic.test_mode = True if sys.platform in ["win32", "cygwin"]: collect_ignore = ["test_worker.py"] diff --git a/tests/test_app.py b/tests/test_app.py index ad85c3c2..7527b1c2 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -258,7 +258,7 @@ def test_handle_request_with_nested_sanic_exception(app, monkeypatch, caplog): def test_app_name_required(): - with pytest.deprecated_call(): + with pytest.raises(SanicException): Sanic() @@ -274,14 +274,35 @@ def test_app_has_test_mode_sync(): assert response.status == 200 -# @pytest.mark.asyncio -# async def test_app_has_test_mode_async(): -# app = Sanic("test") +def test_app_registry(): + instance = Sanic("test") + assert Sanic._app_registry["test"] is instance -# @app.get("/") -# async def handler(request): -# assert request.app.test_mode -# return text("test") -# _, response = await app.asgi_client.get("/") -# assert response.status == 200 +def test_app_registry_wrong_type(): + with pytest.raises(SanicException): + Sanic.register_app(1) + + +def test_app_registry_name_reuse(): + Sanic("test") + Sanic.test_mode = False + with pytest.raises(SanicException): + Sanic("test") + Sanic.test_mode = True + + +def test_app_registry_retrieval(): + instance = Sanic("test") + assert Sanic.get_app("test") is instance + + +def test_get_app_does_not_exist(): + with pytest.raises(SanicException): + Sanic.get_app("does-not-exist") + + +def test_get_app_does_not_exist_force_create(): + assert isinstance( + Sanic.get_app("does-not-exist", force_create=True), Sanic + )