Repository: sanic-org/sanic

Issue:
Allow an alternate configuration class or object to be passed to application objects
It is currently difficult to extend the `Config` class, and have an `Sanic` instance actually use that configuration class throughout its entire lifecycle. This is because the `Sanic` class's `__init__` method is hard-coded to use `sanic.config.Config`. Anyone wishing to use a different class must do one of:
  - Patch and replace `sanic.config.Config`.
  - Re-implement `Sanic.__init__` in a sub-class, duplicating most of the base implementation.
  - Assign a different value to `Sanic.config`.

The first solution is inelegant, and hard to do correctly, the second involves redundant code duplication, and the third means that users are only able to introduce custom behavior post-init.

A simple fix for this is to modify `Sanic.__init__` to allow passing in a custom configuration class. This is already done for the class used to represent requests, and users can also pass in custom router and error handler instances.


Discussion:
This was on my list of simple changes to add. Same thing for the context object. 

Resolving patch:
diff --git a/sanic/app.py b/sanic/app.py
--- a/sanic/app.py
+++ b/sanic/app.py
@@ -123,6 +123,8 @@ class Sanic(BaseSanic):
     def __init__(
         self,
         name: str = None,
+        config: Optional[Config] = None,
+        ctx: Optional[Any] = None,
         router: Optional[Router] = None,
         signal_router: Optional[SignalRouter] = None,
         error_handler: Optional[ErrorHandler] = None,
@@ -141,6 +143,12 @@ def __init__(
         if configure_logging:
             logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS)
 
+        if config and (load_env is not True or env_prefix != SANIC_PREFIX):
+            raise SanicException(
+                "When instantiating Sanic with config, you cannot also pass "
+                "load_env or env_prefix"
+            )
+
         self._asgi_client = None
         self._blueprint_order: List[Blueprint] = []
         self._test_client = None
@@ -148,9 +156,11 @@ def __init__(
         self.asgi = False
         self.auto_reload = False
         self.blueprints: Dict[str, Blueprint] = {}
-        self.config = Config(load_env=load_env, env_prefix=env_prefix)
+        self.config = config or Config(
+            load_env=load_env, env_prefix=env_prefix
+        )
         self.configure_logging = configure_logging
-        self.ctx = SimpleNamespace()
+        self.ctx = ctx or SimpleNamespace()
         self.debug = None
         self.error_handler = error_handler or ErrorHandler()
         self.is_running = False


Test patch:
diff --git a/tests/test_app.py b/tests/test_app.py
--- a/tests/test_app.py
+++ b/tests/test_app.py
@@ -9,6 +9,7 @@
 import pytest
 
 from sanic import Sanic
+from sanic.config import Config
 from sanic.exceptions import SanicException
 from sanic.response import text
 
@@ -412,3 +413,42 @@ class CustomSanic(Sanic):
         pass
 
     CustomSanic("test_subclass_initialisation")
+
+
+def test_bad_custom_config():
+    with pytest.raises(
+        SanicException,
+        match=(
+            "When instantiating Sanic with config, you cannot also pass "
+            "load_env or env_prefix"
+        ),
+    ):
+        Sanic("test", config=1, load_env=1)
+    with pytest.raises(
+        SanicException,
+        match=(
+            "When instantiating Sanic with config, you cannot also pass "
+            "load_env or env_prefix"
+        ),
+    ):
+        Sanic("test", config=1, env_prefix=1)
+
+
+def test_custom_config():
+    class CustomConfig(Config):
+        ...
+
+    config = CustomConfig()
+    app = Sanic("custom", config=config)
+
+    assert app.config == config
+
+
+def test_custom_context():
+    class CustomContext:
+        ...
+
+    ctx = CustomContext()
+    app = Sanic("custom", ctx=ctx)
+
+    assert app.ctx == ctx