Auto extend with Sanic Extensions (#2308)

This commit is contained in:
Adam Hopkins
2021-12-25 22:20:06 +02:00
committed by GitHub
parent b91ffed010
commit dc3ccba527
16 changed files with 264 additions and 48 deletions

View File

@@ -28,6 +28,7 @@ from ssl import SSLContext
from traceback import format_exc
from types import SimpleNamespace
from typing import (
TYPE_CHECKING,
Any,
AnyStr,
Awaitable,
@@ -41,6 +42,7 @@ from typing import (
Set,
Tuple,
Type,
TypeVar,
Union,
)
from urllib.parse import urlencode, urlunparse
@@ -53,6 +55,7 @@ from sanic_routing.exceptions import ( # type: ignore
from sanic_routing.route import Route # type: ignore
from sanic import reloader_helpers
from sanic.application.ext import setup_ext
from sanic.application.logo import get_logo
from sanic.application.motd import MOTD
from sanic.application.state import ApplicationState, Mode
@@ -103,11 +106,21 @@ from sanic.tls import process_to_context
from sanic.touchup import TouchUp, TouchUpMeta
if TYPE_CHECKING: # no cov
try:
from sanic_ext import Extend # type: ignore
from sanic_ext.extensions.base import Extension # type: ignore
except ImportError:
Extend = TypeVar("Extend") # type: ignore
if OS_IS_WINDOWS:
enable_windows_color_support()
filterwarnings("once", category=DeprecationWarning)
SANIC_PACKAGES = ("sanic-routing", "sanic-testing", "sanic-ext")
class Sanic(BaseSanic, metaclass=TouchUpMeta):
"""
@@ -125,6 +138,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
"_asgi_client",
"_blueprint_order",
"_delayed_tasks",
"_ext",
"_future_exceptions",
"_future_listeners",
"_future_middleware",
@@ -1421,26 +1435,15 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
"#proxy-configuration"
)
ssl = process_to_context(ssl)
self.debug = debug
self.state.host = host
self.state.port = port
self.state.workers = workers
# Serve
serve_location = ""
proto = "http"
if ssl is not None:
proto = "https"
if unix:
serve_location = f"{unix} {proto}://..."
elif sock:
serve_location = f"{sock.getsockname()} {proto}://..."
elif host and port:
# colon(:) is legal for a host only in an ipv6 address
display_host = f"[{host}]" if ":" in host else host
serve_location = f"{proto}://{display_host}:{port}"
ssl = process_to_context(ssl)
self.state.ssl = ssl
self.state.unix = unix
self.state.sock = sock
server_settings = {
"protocol": protocol,
@@ -1456,7 +1459,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
"backlog": backlog,
}
self.motd(serve_location)
self.motd(self.serve_location)
if sys.stdout.isatty() and not self.state.is_debug:
error_logger.warning(
@@ -1482,6 +1485,27 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
return server_settings
@property
def serve_location(self) -> str:
serve_location = ""
proto = "http"
if self.state.ssl is not None:
proto = "https"
if self.state.unix:
serve_location = f"{self.state.unix} {proto}://..."
elif self.state.sock:
serve_location = f"{self.state.sock.getsockname()} {proto}://..."
elif self.state.host and self.state.port:
# colon(:) is legal for a host only in an ipv6 address
display_host = (
f"[{self.state.host}]"
if ":" in self.state.host
else self.state.host
)
serve_location = f"{proto}://{display_host}:{self.state.port}"
return serve_location
def _build_endpoint_name(self, *parts):
parts = [self.name, *parts]
return ".".join(parts)
@@ -1790,11 +1814,8 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
display["auto-reload"] = reload_display
packages = []
for package_name, module_name in {
"sanic-routing": "sanic_routing",
"sanic-testing": "sanic_testing",
"sanic-ext": "sanic_ext",
}.items():
for package_name in SANIC_PACKAGES:
module_name = package_name.replace("-", "_")
try:
module = import_module(module_name)
packages.append(f"{package_name}=={module.__version__}")
@@ -1814,6 +1835,41 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
)
MOTD.output(logo, serve_location, display, extra)
@property
def ext(self) -> Extend:
if not hasattr(self, "_ext"):
setup_ext(self, fail=True)
if not hasattr(self, "_ext"):
raise RuntimeError(
"Sanic Extensions is not installed. You can add it to your "
"environment using:\n$ pip install sanic[ext]\nor\n$ pip "
"install sanic-ext"
)
return self._ext # type: ignore
def extend(
self,
*,
extensions: Optional[List[Type[Extension]]] = None,
built_in_extensions: bool = True,
config: Optional[Union[Config, Dict[str, Any]]] = None,
**kwargs,
) -> Extend:
if hasattr(self, "_ext"):
raise RuntimeError(
"Cannot extend Sanic after Sanic Extensions has been setup."
)
setup_ext(
self,
extensions=extensions,
built_in_extensions=built_in_extensions,
config=config,
fail=True,
**kwargs,
)
return self.ext
# -------------------------------------------------------------------- #
# Class methods
# -------------------------------------------------------------------- #
@@ -1875,6 +1931,14 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
async def _startup(self):
self._future_registry.clear()
# Startup Sanic Extensions
if not hasattr(self, "_ext"):
setup_ext(self)
if hasattr(self, "_ext"):
self.ext._display()
# Setup routers
self.signalize()
self.finalize()
@@ -1890,8 +1954,10 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
)
self.__class__._uvloop_setting = self.config.USE_UVLOOP
# Startup time optimizations
ErrorHandler.finalize(self.error_handler, config=self.config)
TouchUp.run(self)
self.state.is_started = True
async def _server_event(

39
sanic/application/ext.py Normal file
View File

@@ -0,0 +1,39 @@
from __future__ import annotations
from contextlib import suppress
from importlib import import_module
from typing import TYPE_CHECKING
if TYPE_CHECKING: # no cov
from sanic import Sanic
try:
from sanic_ext import Extend # type: ignore
except ImportError:
...
def setup_ext(app: Sanic, *, fail: bool = False, **kwargs):
if not app.config.AUTO_EXTEND:
return
sanic_ext = None
with suppress(ModuleNotFoundError):
sanic_ext = import_module("sanic_ext")
if not sanic_ext:
if fail:
raise RuntimeError(
"Sanic Extensions is not installed. You can add it to your "
"environment using:\n$ pip install sanic[ext]\nor\n$ pip "
"install sanic-ext"
)
return
if not getattr(app, "_ext", None):
Ext: Extend = getattr(sanic_ext, "Extend")
app._ext = Ext(app, **kwargs)
return app.ext

View File

@@ -41,9 +41,6 @@ class MOTD(ABC):
class MOTDBasic(MOTD):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
def display(self):
if self.logo:
logger.debug(self.logo)

View File

@@ -5,7 +5,9 @@ import logging
from dataclasses import dataclass, field
from enum import Enum, auto
from pathlib import Path
from typing import TYPE_CHECKING, Any, Set, Union
from socket import socket
from ssl import SSLContext
from typing import TYPE_CHECKING, Any, Optional, Set, Union
from sanic.log import logger
@@ -37,8 +39,11 @@ class ApplicationState:
coffee: bool = field(default=False)
fast: bool = field(default=False)
host: str = field(default="")
mode: Mode = field(default=Mode.PRODUCTION)
port: int = field(default=0)
ssl: Optional[SSLContext] = field(default=None)
sock: Optional[socket] = field(default=None)
unix: Optional[str] = field(default=None)
mode: Mode = field(default=Mode.PRODUCTION)
reload_dirs: Set[Path] = field(default_factory=set)
server: Server = field(default=Server.SANIC)
is_running: bool = field(default=False)

View File

@@ -5,7 +5,7 @@ from functools import partial
from typing import TYPE_CHECKING, List, Optional, Union
if TYPE_CHECKING:
if TYPE_CHECKING: # no cov
from sanic.blueprints import Blueprint

View File

@@ -36,8 +36,8 @@ from sanic.models.handler_types import (
)
if TYPE_CHECKING:
from sanic import Sanic # noqa
if TYPE_CHECKING: # no cov
from sanic import Sanic
def lazy(func, as_decorator=True):

View File

@@ -18,6 +18,7 @@ SANIC_PREFIX = "SANIC_"
DEFAULT_CONFIG = {
"_FALLBACK_ERROR_FORMAT": _default,
"ACCESS_LOG": True,
"AUTO_EXTEND": True,
"AUTO_RELOAD": False,
"EVENT_AUTOREGISTER": False,
"FORWARDED_FOR_HEADER": "X-Forwarded-For",
@@ -59,6 +60,7 @@ class DescriptorMeta(type):
class Config(dict, metaclass=DescriptorMeta):
ACCESS_LOG: bool
AUTO_EXTEND: bool
AUTO_RELOAD: bool
EVENT_AUTOREGISTER: bool
FORWARDED_FOR_HEADER: str

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING:
if TYPE_CHECKING: # no cov
from sanic.request import Request
from sanic.response import BaseHTTPResponse

View File

@@ -15,7 +15,7 @@ from typing import (
from sanic_routing.route import Route # type: ignore
if TYPE_CHECKING:
if TYPE_CHECKING: # no cov
from sanic.server import ConnInfo
from sanic.app import Sanic

View File

@@ -21,6 +21,7 @@ from functools import partial
from signal import SIG_IGN, SIGINT, SIGTERM, Signals
from signal import signal as signal_func
from sanic.application.ext import setup_ext
from sanic.compat import OS_IS_WINDOWS, ctrlc_workaround_for_windows
from sanic.log import error_logger, logger
from sanic.models.server_types import Signal
@@ -116,6 +117,7 @@ def serve(
**asyncio_server_kwargs,
)
setup_ext(app)
if run_async:
return AsyncioServer(
app=app,

View File

@@ -13,7 +13,7 @@ from typing import (
from sanic.models.handler_types import RouteHandler
if TYPE_CHECKING:
if TYPE_CHECKING: # no cov
from sanic import Sanic
from sanic.blueprints import Blueprint
@@ -81,6 +81,8 @@ class HTTPMethodView:
def dispatch_request(self, request, *args, **kwargs):
handler = getattr(self, request.method.lower(), None)
if not handler and request.method == "HEAD":
handler = self.get
return handler(request, *args, **kwargs)
@classmethod

View File

@@ -15,10 +15,10 @@ from sanic.server.protocols.websocket_protocol import WebSocketProtocol
try:
import ssl # type: ignore
except ImportError:
except ImportError: # no cov
ssl = None # type: ignore
if UVLOOP_INSTALLED:
if UVLOOP_INSTALLED: # no cov
try_use_uvloop()