Skip to content

Commit

Permalink
RFC/1684 Context objects (#2063)
Browse files Browse the repository at this point in the history
* Initial setup

* connection context

* Add tests

* move ctx to conn_info

* Move __setattr__ for __fake_slots__ check into base calss
  • Loading branch information
ahopkins authored Mar 17, 2021
1 parent 01f238d commit 8a2ea62
Show file tree
Hide file tree
Showing 9 changed files with 223 additions and 44 deletions.
75 changes: 58 additions & 17 deletions sanic/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from socket import socket
from ssl import Purpose, SSLContext, create_default_context
from traceback import format_exc
from types import SimpleNamespace
from typing import (
Any,
Awaitable,
Expand Down Expand Up @@ -76,6 +77,44 @@ class Sanic(BaseSanic):
The main application instance
"""

__fake_slots__ = (
"_app_registry",
"_asgi_client",
"_blueprint_order",
"_future_routes",
"_future_statics",
"_future_middleware",
"_future_listeners",
"_future_exceptions",
"_future_signals",
"_test_client",
"_test_manager",
"asgi",
"blueprints",
"config",
"configure_logging",
"ctx",
"debug",
"error_handler",
"go_fast",
"is_running",
"is_stopping",
"listeners",
"name",
"named_request_middleware",
"named_response_middleware",
"request_class",
"request_middleware",
"response_middleware",
"router",
"signal_router",
"sock",
"strict_slashes",
"test_mode",
"websocket_enabled",
"websocket_tasks",
)

_app_registry: Dict[str, "Sanic"] = {}
test_mode = False

Expand Down Expand Up @@ -104,31 +143,33 @@ def __init__(
if configure_logging:
logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS)

self.name = name
self._asgi_client = None
self._blueprint_order: List[Blueprint] = []
self._test_client = None
self._test_manager = None
self.asgi = False
self.router = router or Router()
self.signal_router = signal_router or SignalRouter()
self.request_class = request_class
self.error_handler = error_handler or ErrorHandler()
self.config = Config(load_env=load_env)
self.request_middleware: Deque[MiddlewareType] = deque()
self.response_middleware: Deque[MiddlewareType] = deque()
self.blueprints: Dict[str, Blueprint] = {}
self._blueprint_order: List[Blueprint] = []
self.config = Config(load_env=load_env)
self.configure_logging = configure_logging
self.ctx = SimpleNamespace()
self.debug = None
self.error_handler = error_handler or ErrorHandler()
self.is_running = False
self.is_stopping = False
self.listeners: Dict[str, List[ListenerType]] = defaultdict(list)
self.name = name
self.named_request_middleware: Dict[str, Deque[MiddlewareType]] = {}
self.named_response_middleware: Dict[str, Deque[MiddlewareType]] = {}
self.request_class = request_class
self.request_middleware: Deque[MiddlewareType] = deque()
self.response_middleware: Deque[MiddlewareType] = deque()
self.router = router or Router()
self.signal_router = signal_router or SignalRouter()
self.sock = None
self.strict_slashes = strict_slashes
self.listeners: Dict[str, List[ListenerType]] = defaultdict(list)
self.is_stopping = False
self.is_running = False
self.websocket_enabled = False
self.websocket_tasks: Set[Future] = set()
self.named_request_middleware: Dict[str, Deque[MiddlewareType]] = {}
self.named_response_middleware: Dict[str, Deque[MiddlewareType]] = {}
self._test_manager = None
self._test_client = None
self._asgi_client = None

# Register alternative method names
self.go_fast = self.run

Expand Down
18 changes: 18 additions & 0 deletions sanic/base.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from typing import Any, Tuple
from warnings import warn

from sanic.mixins.exceptions import ExceptionMixin
from sanic.mixins.listeners import ListenerMixin
from sanic.mixins.middleware import MiddlewareMixin
Expand Down Expand Up @@ -35,8 +38,23 @@ class BaseSanic(
SignalMixin,
metaclass=Base,
):
__fake_slots__: Tuple[str, ...]

def __str__(self) -> str:
return f"<{self.__class__.__name__} {self.name}>"

def __repr__(self) -> str:
return f'{self.__class__.__name__}(name="{self.name}")'

def __setattr__(self, name: str, value: Any) -> None:
# This is a temporary compat layer so we can raise a warning until
# setting attributes on the app instance can be removed and deprecated
# with a proper implementation of __slots__
if name not in self.__fake_slots__:
warn(
f"Setting variables on {self.__class__.__name__} instances is "
"deprecated and will be removed in version 21.9. You should "
f"change your {self.__class__.__name__} instance to use "
f"instance.ctx.{name} instead."
)
super().__setattr__(name, value)
38 changes: 31 additions & 7 deletions sanic/blueprints.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import asyncio

from collections import defaultdict
from types import SimpleNamespace
from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Set, Union

from sanic_routing.exceptions import NotFound # type: ignore
Expand Down Expand Up @@ -42,6 +43,28 @@ class Blueprint(BaseSanic):
training */*
"""

__fake_slots__ = (
"_apps",
"_future_routes",
"_future_statics",
"_future_middleware",
"_future_listeners",
"_future_exceptions",
"_future_signals",
"ctx",
"exceptions",
"host",
"listeners",
"middlewares",
"name",
"routes",
"statics",
"strict_slashes",
"url_prefix",
"version",
"websocket_routes",
)

def __init__(
self,
name: str,
Expand All @@ -50,19 +73,20 @@ def __init__(
version: Optional[int] = None,
strict_slashes: Optional[bool] = None,
):
self._apps: Set[Sanic] = set()
self.name = name
self.url_prefix = url_prefix
self.host = host

self.routes: List[Route] = []
self.websocket_routes: List[Route] = []
self._apps: Set[Sanic] = set()
self.ctx = SimpleNamespace()
self.exceptions: List[RouteHandler] = []
self.host = host
self.listeners: Dict[str, List[ListenerType]] = {}
self.middlewares: List[MiddlewareType] = []
self.name = name
self.routes: List[Route] = []
self.statics: List[RouteHandler] = []
self.version = version
self.strict_slashes = strict_slashes
self.url_prefix = url_prefix
self.version = version
self.websocket_routes: List[Route] = []

def __repr__(self) -> str:
args = ", ".join(
Expand Down
8 changes: 8 additions & 0 deletions sanic/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ class Request:
"_ip",
"_parsed_url",
"_port",
"_protocol",
"_remote_addr",
"_socket",
"_match_info",
Expand Down Expand Up @@ -153,6 +154,7 @@ def __init__(
self.stream: Optional[Http] = None
self.endpoint: Optional[str] = None
self.route: Optional[Route] = None
self._protocol = None

def __repr__(self):
class_name = self.__class__.__name__
Expand Down Expand Up @@ -205,6 +207,12 @@ async def receive_body(self):
if not self.body:
self.body = b"".join([data async for data in self.stream])

@property
def protocol(self):
if not self._protocol:
self._protocol = self.transport.get_protocol()
return self._protocol

@property
def raw_headers(self):
_, headers = self.head.split(b"\r\n", 1)
Expand Down
19 changes: 13 additions & 6 deletions sanic/server.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

from ssl import SSLContext
from types import SimpleNamespace
from typing import (
TYPE_CHECKING,
Any,
Expand Down Expand Up @@ -62,24 +63,28 @@ class ConnInfo:
"""

__slots__ = (
"sockname",
"client_port",
"client",
"ctx",
"peername",
"server",
"server_port",
"client",
"client_port",
"server",
"sockname",
"ssl",
)

def __init__(self, transport: TransportProtocol, unix=None):
self.ssl: bool = bool(transport.get_extra_info("sslcontext"))
self.ctx = SimpleNamespace()
self.peername = None
self.server = self.client = ""
self.server_port = self.client_port = 0
self.peername = None
self.sockname = addr = transport.get_extra_info("sockname")
self.ssl: bool = bool(transport.get_extra_info("sslcontext"))

if isinstance(addr, str): # UNIX socket
self.server = unix or addr
return

# IPv4 (ip, port) or IPv6 (ip, port, flowinfo, scopeid)
if isinstance(addr, tuple):
self.server = addr[0] if len(addr) == 2 else f"[{addr[0]}]"
Expand All @@ -88,6 +93,7 @@ def __init__(self, transport: TransportProtocol, unix=None):
if addr[1] != (443 if self.ssl else 80):
self.server = f"{self.server}:{addr[1]}"
self.peername = addr = transport.get_extra_info("peername")

if isinstance(addr, tuple):
self.client = addr[0] if len(addr) == 2 else f"[{addr[0]}]"
self.client_port = addr[1]
Expand All @@ -107,6 +113,7 @@ class HttpProtocol(asyncio.Protocol):
"connections",
"signal",
"conn_info",
"ctx",
# request params
"request",
# request config
Expand Down
19 changes: 19 additions & 0 deletions tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,3 +386,22 @@ def test_app_no_registry_env():
):
Sanic.get_app("no-register")
del environ["SANIC_REGISTER"]


def test_app_set_attribute_warning(app):
with pytest.warns(UserWarning) as record:
app.foo = 1

assert len(record) == 1
assert record[0].message.args[0] == (
"Setting variables on Sanic instances is deprecated "
"and will be removed in version 21.9. You should change your "
"Sanic instance to use instance.ctx.foo instead."
)


def test_app_set_context(app):
app.ctx.foo = 1

retrieved = Sanic.get_app(app.name)
assert retrieved.ctx.foo == 1
13 changes: 13 additions & 0 deletions tests/test_blueprints.py
Original file line number Diff line number Diff line change
Expand Up @@ -1024,3 +1024,16 @@ async def handler(request):
for app in (app1, app2):
_, response = app.test_client.get("/")
assert response.text == f"{app.name}.bp.handler"


def test_bp_set_attribute_warning():
bp = Blueprint("bp")
with pytest.warns(UserWarning) as record:
bp.foo = 1

assert len(record) == 1
assert record[0].message.args[0] == (
"Setting variables on Blueprint instances is deprecated "
"and will be removed in version 21.9. You should change your "
"Blueprint instance to use instance.ctx.foo instead."
)
Loading

0 comments on commit 8a2ea62

Please sign in to comment.