App registry (#1979)

* Add app level registry

* Add documentation for app registry

* Remove unused import

* Add force_create keyword to Sanic.get_app

* Add force_commit to docs
This commit is contained in:
Adam Hopkins 2020-12-28 22:47:31 +02:00 committed by GitHub
parent 262f89f2b6
commit 449bc417a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 90 additions and 20 deletions

View File

@ -60,3 +60,26 @@ Open the address `http://0.0.0.0:8000 <http://0.0.0.0:8000>`_ in your web browse
the message *Hello world!*. the message *Hello world!*.
You now have a working Sanic server! 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)

View File

@ -2,12 +2,11 @@ import logging
import logging.config import logging.config
import os import os
import re import re
import warnings
from asyncio import CancelledError, Protocol, ensure_future, get_event_loop from asyncio import CancelledError, Protocol, ensure_future, get_event_loop
from collections import defaultdict, deque from collections import defaultdict, deque
from functools import partial from functools import partial
from inspect import getmodulename, isawaitable, signature, stack from inspect import isawaitable, signature
from socket import socket from socket import socket
from ssl import Purpose, SSLContext, create_default_context from ssl import Purpose, SSLContext, create_default_context
from traceback import format_exc from traceback import format_exc
@ -38,6 +37,9 @@ from sanic.websocket import ConnectionClosed, WebSocketProtocol
class Sanic: class Sanic:
_app_registry: Dict[str, "Sanic"] = {}
test_mode = False
def __init__( def __init__(
self, self,
name=None, name=None,
@ -52,15 +54,10 @@ class Sanic:
# Get name from previous stack frame # Get name from previous stack frame
if name is None: if name is None:
warnings.warn( raise SanicException(
"Sanic(name=None) is deprecated and None value support " "Sanic instance cannot be unnamed. "
"for `name` will be removed in the next release. "
"Please use Sanic(name='your_application_name') instead.", "Please use Sanic(name='your_application_name') instead.",
DeprecationWarning,
stacklevel=2,
) )
frame_records = stack()[1]
name = getmodulename(frame_records[1])
# logging # logging
if configure_logging: if configure_logging:
@ -90,7 +87,8 @@ class Sanic:
self.named_response_middleware = {} self.named_response_middleware = {}
# Register alternative method names # Register alternative method names
self.go_fast = self.run self.go_fast = self.run
self.test_mode = False
self.__class__.register_app(self)
@property @property
def loop(self): def loop(self):
@ -1401,9 +1399,36 @@ class Sanic:
# -------------------------------------------------------------------- # # -------------------------------------------------------------------- #
# Configuration # Configuration
# -------------------------------------------------------------------- # # -------------------------------------------------------------------- #
def update_config(self, config: Union[bytes, str, dict, Any]): def update_config(self, config: Union[bytes, str, dict, Any]):
"""Update app.config. """Update app.config.
Please refer to config.py::Config.update_config for documentation.""" Please refer to config.py::Config.update_config for documentation."""
self.config.update_config(config) 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.')

View File

@ -11,6 +11,7 @@ from sanic.router import RouteExists, Router
random.seed("Pack my box with five dozen liquor jugs.") random.seed("Pack my box with five dozen liquor jugs.")
Sanic.test_mode = True
if sys.platform in ["win32", "cygwin"]: if sys.platform in ["win32", "cygwin"]:
collect_ignore = ["test_worker.py"] collect_ignore = ["test_worker.py"]

View File

@ -258,7 +258,7 @@ def test_handle_request_with_nested_sanic_exception(app, monkeypatch, caplog):
def test_app_name_required(): def test_app_name_required():
with pytest.deprecated_call(): with pytest.raises(SanicException):
Sanic() Sanic()
@ -274,14 +274,35 @@ def test_app_has_test_mode_sync():
assert response.status == 200 assert response.status == 200
# @pytest.mark.asyncio def test_app_registry():
# async def test_app_has_test_mode_async(): instance = Sanic("test")
# app = 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("/") def test_app_registry_wrong_type():
# assert response.status == 200 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
)