Repository: huge-success/sanic
Pull request #1979 (2020-12-10 08:09:03)

App registry
See #1968 

The purpose of this is to add a convenience for access an application instance.

```python
from sanic import Sanic

app = Sanic.get_app("my_awesome_server")
```

codecov[bot]: # [Codecov](https://codecov.io/gh/huge-success/sanic/pull/1979?src=pr&el=h1) Report
> Merging [#1979](https://codecov.io/gh/huge-success/sanic/pull/1979?src=pr&el=desc) (1123031) into [master](https://codecov.io/gh/huge-success/sanic/commit/262f89f2b6732be77e7812192f8d17b9d8e0fe21?el=desc) (262f89f) will **decrease** coverage by `0.03%`.
> The diff coverage is `89.47%`.

[![Impacted file tree graph](https://codecov.io/gh/huge-success/sanic/pull/1979/graphs/tree.svg?width=650&height=150&src=pr&token=NqLbJHUqoR)](https://codecov.io/gh/huge-success/sanic/pull/1979?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master    #1979      +/-   ##
==========================================
- Coverage   92.78%   92.74%   -0.04%     
==========================================
  Files          29       29              
  Lines        3241     3255      +14     
  Branches      566      568       +2     
==========================================
+ Hits         3007     3019      +12     
- Misses        158      160       +2     
  Partials       76       76              
```


| [Impacted Files](https://codecov.io/gh/huge-success/sanic/pull/1979?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [sanic/app.py](https://codecov.io/gh/huge-success/sanic/pull/1979/diff?src=pr&el=tree#diff-c2FuaWMvYXBwLnB5) | `95.56% <89.47%> (-0.30%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/huge-success/sanic/pull/1979?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/huge-success/sanic/pull/1979?src=pr&el=footer). Last update [262f89f...1123031](https://codecov.io/gh/huge-success/sanic/pull/1979?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
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 <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.')


Test patch:
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
+    )