Repository: sanic-org/sanic

Issue:
Remove usage of ctx in routing
## TLDR

Remove all Sanic reliance upon `ctx` values in the `routing` module.

## Rationale

There are several locations throughout Sanic where the developer is provided with a `ctx` object to inject arbitrary values. The two most visible locations are [`Sanic.ctx`](https://sanicframework.org/en/guide/basics/app.html#application-context) and [`Request.ctx`](https://sanicframework.org/en/guide/basics/request.html#body). However, this also exists for the router, routes, blueprints, and connections.

Only in routing does Sanic actually use the object for storing internal state. It will be much more consistent if Sanic _never_ touches these objects, and therefore they can serve their original purpose: injection of arbitrary values without the worry of conflict.

---

## Implementation

The reason that Sanic has used these values is so that `sanic-routing` can exist independently of `sanic`. So, from the perspective of `sanic-routing`, these objects are fulfilling their purpose and not being overburdened with values. Ultimately, however, this is somewhat moot as far as the developer is concerned as they need to be careful not to overwrite these values.

Therefore, the simplest approach is probably to add a `state` object into `sanic-routing` that looks and functions the same was as `ctx`, but is specifically intended for arbitrary "stateful" information coming from `Sanic`.

See #2300 and #2302

Discussion:
ahopkins: > Happy with these changes if/when the tests pass!

Coming shortly. There will be some failures because we have been testing the deprecations.
codecov[bot]: # [Codecov](https://codecov.io/gh/sanic-org/sanic/pull/2555?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org) Report
Base: **87.953**% // Head: **88.018**% // Increases project coverage by **`+0.064%`** :tada:
> Coverage data is based on head [(`599c893`)](https://codecov.io/gh/sanic-org/sanic/pull/2555?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org) compared to base [(`23ce4ea`)](https://codecov.io/gh/sanic-org/sanic/commit/23ce4eaaa4ef8f05dc76766d4af7780769ca98b3?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org).
> Patch coverage: 100.000% of modified lines in pull request are covered.

<details><summary>Additional details and impacted files</summary>


```diff
@@              Coverage Diff              @@
##              main     #2555       +/-   ##
=============================================
+ Coverage   87.953%   88.018%   +0.064%     
=============================================
  Files           71        71               
  Lines         5346      5333       -13     
  Branches       992       987        -5     
=============================================
- Hits          4702      4694        -8     
+ Misses         464       461        -3     
+ Partials       180       178        -2     
```


| [Impacted Files](https://codecov.io/gh/sanic-org/sanic/pull/2555?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org) | Coverage Δ | |
|---|---|---|
| [sanic/config.py](https://codecov.io/gh/sanic-org/sanic/pull/2555/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org#diff-c2FuaWMvY29uZmlnLnB5) | `97.350% <100.000%> (-0.102%)` | :arrow_down: |
| [sanic/errorpages.py](https://codecov.io/gh/sanic-org/sanic/pull/2555/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org#diff-c2FuaWMvZXJyb3JwYWdlcy5weQ==) | `97.938% <100.000%> (ø)` | |
| [sanic/mixins/routes.py](https://codecov.io/gh/sanic-org/sanic/pull/2555/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org#diff-c2FuaWMvbWl4aW5zL3JvdXRlcy5weQ==) | `94.881% <100.000%> (+1.059%)` | :arrow_up: |
| [sanic/mixins/startup.py](https://codecov.io/gh/sanic-org/sanic/pull/2555/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org#diff-c2FuaWMvbWl4aW5zL3N0YXJ0dXAucHk=) | `91.898% <100.000%> (+0.423%)` | :arrow_up: |
| [sanic/router.py](https://codecov.io/gh/sanic-org/sanic/pull/2555/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org#diff-c2FuaWMvcm91dGVyLnB5) | `95.789% <100.000%> (ø)` | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/sanic-org/sanic/pull/2555?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org).


Patch:
diff --git a/sanic/app.py b/sanic/app.py
index 26f1b9d9..cb6f76b3 100644
--- a/sanic/app.py
+++ b/sanic/app.py
@@ -47,7 +47,7 @@ from sanic_routing.exceptions import FinalizationError, NotFound
 from sanic_routing.route import Route
 
 from sanic.application.ext import setup_ext
-from sanic.application.state import ApplicationState, Mode, ServerStage
+from sanic.application.state import ApplicationState, ServerStage
 from sanic.asgi import ASGIApp
 from sanic.base.root import BaseSanic
 from sanic.blueprint_group import BlueprintGroup
@@ -158,7 +158,6 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
     )
 
     _app_registry: Dict[str, "Sanic"] = {}
-    _uvloop_setting = None  # TODO: Remove in v22.6
     test_mode = False
 
     def __init__(
@@ -394,8 +393,8 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
             routes = [routes]
 
         for r in routes:
-            r.ctx.websocket = websocket
-            r.ctx.static = params.get("static", False)
+            r.extra.websocket = websocket
+            r.extra.static = params.get("static", False)
             r.ctx.__dict__.update(ctx)
 
         return routes
@@ -589,7 +588,7 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
 
         uri = route.path
 
-        if getattr(route.ctx, "static", None):
+        if getattr(route.extra, "static", None):
             filename = kwargs.pop("filename", "")
             # it's static folder
             if "__file_uri__" in uri:
@@ -622,18 +621,18 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
         host = kwargs.pop("_host", None)
         external = kwargs.pop("_external", False) or bool(host)
         scheme = kwargs.pop("_scheme", "")
-        if route.ctx.hosts and external:
-            if not host and len(route.ctx.hosts) > 1:
+        if route.extra.hosts and external:
+            if not host and len(route.extra.hosts) > 1:
                 raise ValueError(
-                    f"Host is ambiguous: {', '.join(route.ctx.hosts)}"
+                    f"Host is ambiguous: {', '.join(route.extra.hosts)}"
                 )
-            elif host and host not in route.ctx.hosts:
+            elif host and host not in route.extra.hosts:
                 raise ValueError(
                     f"Requested host ({host}) is not available for this "
-                    f"route: {route.ctx.hosts}"
+                    f"route: {route.extra.hosts}"
                 )
             elif not host:
-                host = list(route.ctx.hosts)[0]
+                host = list(route.extra.hosts)[0]
 
         if scheme and not external:
             raise ValueError("When specifying _scheme, _external must be True")
@@ -884,7 +883,7 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
             if (
                 request.stream
                 and request.stream.request_body
-                and not route.ctx.ignore_body
+                and not route.extra.ignore_body
             ):
 
                 if hasattr(handler, "is_stream"):
@@ -1346,18 +1345,6 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
     def debug(self):
         return self.state.is_debug
 
-    @debug.setter
-    def debug(self, value: bool):
-        deprecation(
-            "Setting the value of a Sanic application's debug value directly "
-            "is deprecated and will be removed in v22.9. Please set it using "
-            "the CLI, app.run, app.prepare, or directly set "
-            "app.state.mode to Mode.DEBUG.",
-            22.9,
-        )
-        mode = Mode.DEBUG if value else Mode.PRODUCTION
-        self.state.mode = mode
-
     @property
     def auto_reload(self):
         return self.config.AUTO_RELOAD
@@ -1374,58 +1361,6 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
         """
         return self._state
 
-    @property
-    def is_running(self):
-        deprecation(
-            "Use of the is_running property is no longer used by Sanic "
-            "internally. The property is now deprecated and will be removed "
-            "in version 22.9. You may continue to set the property for your "
-            "own needs until that time. If you would like to check whether "
-            "the application is operational, please use app.state.stage. More "
-            "information is available at ___.",
-            22.9,
-        )
-        return self.state.is_running
-
-    @is_running.setter
-    def is_running(self, value: bool):
-        deprecation(
-            "Use of the is_running property is no longer used by Sanic "
-            "internally. The property is now deprecated and will be removed "
-            "in version 22.9. You may continue to set the property for your "
-            "own needs until that time. If you would like to check whether "
-            "the application is operational, please use app.state.stage. More "
-            "information is available at ___.",
-            22.9,
-        )
-        self.state.is_running = value
-
-    @property
-    def is_stopping(self):
-        deprecation(
-            "Use of the is_stopping property is no longer used by Sanic "
-            "internally. The property is now deprecated and will be removed "
-            "in version 22.9. You may continue to set the property for your "
-            "own needs until that time. If you would like to check whether "
-            "the application is operational, please use app.state.stage. More "
-            "information is available at ___.",
-            22.9,
-        )
-        return self.state.is_stopping
-
-    @is_stopping.setter
-    def is_stopping(self, value: bool):
-        deprecation(
-            "Use of the is_stopping property is no longer used by Sanic "
-            "internally. The property is now deprecated and will be removed "
-            "in version 22.9. You may continue to set the property for your "
-            "own needs until that time. If you would like to check whether "
-            "the application is operational, please use app.state.stage. More "
-            "information is available at ___.",
-            22.9,
-        )
-        self.state.is_stopping = value
-
     @property
     def reload_dirs(self):
         return self.state.reload_dirs
@@ -1520,6 +1455,16 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
                 return cls(name)
             raise SanicException(f'Sanic app name "{name}" not found.')
 
+    @classmethod
+    def _check_uvloop_conflict(cls) -> None:
+        values = {app.config.USE_UVLOOP for app in cls._app_registry.values()}
+        if len(values) > 1:
+            error_logger.warning(
+                "It looks like you're running several apps with different "
+                "uvloop settings. This is not supported and may lead to "
+                "unintended behaviour."
+            )
+
     # -------------------------------------------------------------------- #
     # Lifecycle
     # -------------------------------------------------------------------- #
@@ -1569,17 +1514,7 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
                 23.3,
             )
 
-        # TODO: Replace in v22.6 to check against apps in app registry
-        if (
-            self.__class__._uvloop_setting is not None
-            and self.__class__._uvloop_setting != self.config.USE_UVLOOP
-        ):
-            error_logger.warning(
-                "It looks like you're running several apps with different "
-                "uvloop settings. This is not supported and may lead to "
-                "unintended behaviour."
-            )
-        self.__class__._uvloop_setting = self.config.USE_UVLOOP
+        Sanic._check_uvloop_conflict()
 
         # Startup time optimizations
         if self.state.primary:
diff --git a/sanic/blueprints.py b/sanic/blueprints.py
index 271306cb..0cca4bd4 100644
--- a/sanic/blueprints.py
+++ b/sanic/blueprints.py
@@ -406,7 +406,7 @@ class Blueprint(BaseSanic):
 
         self.routes += [route for route in routes if isinstance(route, Route)]
         self.websocket_routes += [
-            route for route in self.routes if route.ctx.websocket
+            route for route in self.routes if route.extra.websocket
         ]
         self.middlewares += middleware
         self.exceptions += exception_handlers
diff --git a/sanic/config.py b/sanic/config.py
index 5c90086d..8f0e8de4 100644
--- a/sanic/config.py
+++ b/sanic/config.py
@@ -12,7 +12,7 @@ from sanic.constants import LocalCertCreator
 from sanic.errorpages import DEFAULT_FORMAT, check_error_format
 from sanic.helpers import Default, _default
 from sanic.http import Http
-from sanic.log import deprecation, error_logger
+from sanic.log import error_logger
 from sanic.utils import load_module_from_file_location, str_to_bool
 
 
@@ -71,10 +71,6 @@ DEFAULT_CONFIG = {
     "WEBSOCKET_PING_TIMEOUT": 20,
 }
 
-# These values will be removed from the Config object in v22.6 and moved
-# to the application state
-DEPRECATED_CONFIG = ("SERVER_RUNNING", "RELOADER_PROCESS", "RELOADED_FILES")
-
 
 class DescriptorMeta(type):
     def __init__(cls, *_):
@@ -132,6 +128,7 @@ class Config(dict, metaclass=DescriptorMeta):
     ):
         defaults = defaults or {}
         super().__init__({**DEFAULT_CONFIG, **defaults})
+        self._configure_warnings()
 
         self._converters = [str, str_to_bool, float, int]
 
@@ -149,7 +146,6 @@ class Config(dict, metaclass=DescriptorMeta):
             self.load_environment_vars(SANIC_PREFIX)
 
         self._configure_header_size()
-        self._configure_warnings()
         self._check_error_format()
         self._init = True
 
@@ -241,7 +237,9 @@ class Config(dict, metaclass=DescriptorMeta):
         """
         Looks for prefixed environment variables and applies them to the
         configuration if present. This is called automatically when Sanic
-        starts up to load environment variables into config.
+        starts up to load environment variables into config. Environment
+        variables should start with the defined prefix and should only
+        contain uppercase letters.
 
         It will automatically hydrate the following types:
 
@@ -267,12 +265,9 @@ class Config(dict, metaclass=DescriptorMeta):
         `See user guide re: config
         <https://sanicframework.org/guide/deployment/configuration.html>`__
         """
-        lower_case_var_found = False
         for key, value in environ.items():
-            if not key.startswith(prefix):
+            if not key.startswith(prefix) or not key.isupper():
                 continue
-            if not key.isupper():
-                lower_case_var_found = True
 
             _, config_key = key.split(prefix, 1)
 
@@ -282,12 +277,6 @@ class Config(dict, metaclass=DescriptorMeta):
                     break
                 except ValueError:
                     pass
-        if lower_case_var_found:
-            deprecation(
-                "Lowercase environment variables will not be "
-                "loaded into Sanic config beginning in v22.9.",
-                22.9,
-            )
 
     def update_config(self, config: Union[bytes, str, dict, Any]):
         """
diff --git a/sanic/errorpages.py b/sanic/errorpages.py
index e5d123b1..c035dce1 100644
--- a/sanic/errorpages.py
+++ b/sanic/errorpages.py
@@ -448,8 +448,8 @@ def exception_response(
             # from the route
             if request.route:
                 try:
-                    if request.route.ctx.error_format:
-                        render_format = request.route.ctx.error_format
+                    if request.route.extra.error_format:
+                        render_format = request.route.extra.error_format
                 except AttributeError:
                     ...
 
diff --git a/sanic/mixins/routes.py b/sanic/mixins/routes.py
index 2fb1e837..c38b32ee 100644
--- a/sanic/mixins/routes.py
+++ b/sanic/mixins/routes.py
@@ -10,6 +10,7 @@ from textwrap import dedent
 from typing import (
     Any,
     Callable,
+    Dict,
     Iterable,
     List,
     Optional,
@@ -38,14 +39,6 @@ from sanic.types import HashableDict
 RouteWrapper = Callable[
     [RouteHandler], Union[RouteHandler, Tuple[Route, RouteHandler]]
 ]
-RESTRICTED_ROUTE_CONTEXT = (
-    "ignore_body",
-    "stream",
-    "hosts",
-    "static",
-    "error_format",
-    "websocket",
-)
 
 
 class RouteMixin(metaclass=SanicMeta):
@@ -1046,24 +1039,12 @@ class RouteMixin(metaclass=SanicMeta):
 
         return types
 
-    def _build_route_context(self, raw):
+    def _build_route_context(self, raw: Dict[str, Any]) -> HashableDict:
         ctx_kwargs = {
             key.replace("ctx_", ""): raw.pop(key)
             for key in {**raw}.keys()
             if key.startswith("ctx_")
         }
-        restricted = [
-            key for key in ctx_kwargs.keys() if key in RESTRICTED_ROUTE_CONTEXT
-        ]
-        if restricted:
-            restricted_arguments = ", ".join(restricted)
-            raise AttributeError(
-                "Cannot use restricted route context: "
-                f"{restricted_arguments}. This limitation is only in place "
-                "until v22.9 when the restricted names will no longer be in"
-                "conflict. See https://github.com/sanic-org/sanic/issues/2303 "
-                "for more information."
-            )
         if raw:
             unexpected_arguments = ", ".join(raw.keys())
             raise TypeError(
diff --git a/sanic/mixins/startup.py b/sanic/mixins/startup.py
index 5365431c..38414f39 100644
--- a/sanic/mixins/startup.py
+++ b/sanic/mixins/startup.py
@@ -559,7 +559,6 @@ class StartupMixin(metaclass=SanicMeta):
 
     def motd(
         self,
-        serve_location: str = "",
         server_settings: Optional[Dict[str, Any]] = None,
     ):
         if (
@@ -569,14 +568,7 @@ class StartupMixin(metaclass=SanicMeta):
             or os.environ.get("SANIC_SERVER_RUNNING")
         ):
             return
-        if serve_location:
-            deprecation(
-                "Specifying a serve_location in the MOTD is deprecated and "
-                "will be removed.",
-                22.9,
-            )
-        else:
-            serve_location = self.get_server_location(server_settings)
+        serve_location = self.get_server_location(server_settings)
         if self.config.MOTD:
             logo = get_logo(coffee=self.state.coffee)
             display, extra = self.get_motd_data(server_settings)
diff --git a/sanic/router.py b/sanic/router.py
index ec4d852f..b68fde3b 100644
--- a/sanic/router.py
+++ b/sanic/router.py
@@ -133,14 +133,14 @@ class Router(BaseRouter):
                 params.update({"requirements": {"host": host}})
 
             route = super().add(**params)  # type: ignore
-            route.ctx.ignore_body = ignore_body
-            route.ctx.stream = stream
-            route.ctx.hosts = hosts
-            route.ctx.static = static
-            route.ctx.error_format = error_format
+            route.extra.ignore_body = ignore_body
+            route.extra.stream = stream
+            route.extra.hosts = hosts
+            route.extra.static = static
+            route.extra.error_format = error_format
 
             if error_format:
-                check_error_format(route.ctx.error_format)
+                check_error_format(route.extra.error_format)
 
             routes.append(route)
 


Test patch:
diff --git a/tests/test_app.py b/tests/test_app.py
index 333ea51a..0e71f9f7 100644
--- a/tests/test_app.py
+++ b/tests/test_app.py
@@ -525,7 +525,7 @@ def test_multiple_uvloop_configs_display_warning(caplog):
 
     counter = Counter([(r[1], r[2]) for r in caplog.record_tuples])
 
-    assert counter[(logging.WARNING, message)] == 2
+    assert counter[(logging.WARNING, message)] == 3
 
 
 def test_cannot_run_fast_and_workers(app: Sanic):
diff --git a/tests/test_config.py b/tests/test_config.py
index e18660c2..a702bc88 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -125,14 +125,9 @@ def test_env_w_custom_converter():
 
 
 def test_env_lowercase():
-    with pytest.warns(None) as record:
-        environ["SANIC_test_answer"] = "42"
-        app = Sanic(name="Test")
-        assert app.config.test_answer == 42
-    assert str(record[0].message) == (
-        "[DEPRECATION v22.9] Lowercase environment variables will not be "
-        "loaded into Sanic config beginning in v22.9."
-    )
+    environ["SANIC_test_answer"] = "42"
+    app = Sanic(name="Test")
+    assert "test_answer" not in app.config
     del environ["SANIC_test_answer"]
 
 
diff --git a/tests/test_errorpages.py b/tests/test_errorpages.py
index ce246894..fda629ca 100644
--- a/tests/test_errorpages.py
+++ b/tests/test_errorpages.py
@@ -97,15 +97,15 @@ def test_auto_fallback_with_content_type(app):
 def test_route_error_format_set_on_auto(app):
     @app.get("/text")
     def text_response(request):
-        return text(request.route.ctx.error_format)
+        return text(request.route.extra.error_format)
 
     @app.get("/json")
     def json_response(request):
-        return json({"format": request.route.ctx.error_format})
+        return json({"format": request.route.extra.error_format})
 
     @app.get("/html")
     def html_response(request):
-        return html(request.route.ctx.error_format)
+        return html(request.route.extra.error_format)
 
     _, response = app.test_client.get("/text")
     assert response.text == "text"