Skip to content

Commit

Permalink
Auto extend with Sanic Extensions (sanic-org#2308)
Browse files Browse the repository at this point in the history
  • Loading branch information
ahopkins authored and ChihweiLHBird committed Jun 1, 2022
1 parent 259465d commit d043aa0
Show file tree
Hide file tree
Showing 16 changed files with 264 additions and 48 deletions.
110 changes: 88 additions & 22 deletions sanic/app.py
Expand Up @@ -28,6 +28,7 @@
from traceback import format_exc
from types import SimpleNamespace
from typing import (
TYPE_CHECKING,
Any,
AnyStr,
Awaitable,
Expand All @@ -41,6 +42,7 @@
Set,
Tuple,
Type,
TypeVar,
Union,
)
from urllib.parse import urlencode, urlunparse
Expand All @@ -53,6 +55,7 @@
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
Expand Down Expand Up @@ -103,11 +106,21 @@
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):
"""
Expand All @@ -125,6 +138,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
"_asgi_client",
"_blueprint_order",
"_delayed_tasks",
"_ext",
"_future_exceptions",
"_future_listeners",
"_future_middleware",
Expand Down Expand Up @@ -1421,26 +1435,15 @@ def _helper(
"#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,
Expand All @@ -1456,7 +1459,7 @@ def _helper(
"backlog": backlog,
}

self.motd(serve_location)
self.motd(self.serve_location)

if sys.stdout.isatty() and not self.state.is_debug:
error_logger.warning(
Expand All @@ -1482,6 +1485,27 @@ def _helper(

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)
Expand Down Expand Up @@ -1790,11 +1814,8 @@ def motd(self, serve_location):
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__}")
Expand All @@ -1814,6 +1835,41 @@ def motd(self, serve_location):
)
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
# -------------------------------------------------------------------- #
Expand Down Expand Up @@ -1875,6 +1931,14 @@ def signalize(self):

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()

Expand All @@ -1890,8 +1954,10 @@ async def _startup(self):
)
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(
Expand Down
39 changes: 39 additions & 0 deletions sanic/application/ext.py
@@ -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
3 changes: 0 additions & 3 deletions sanic/application/motd.py
Expand Up @@ -41,9 +41,6 @@ def output(


class MOTDBasic(MOTD):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)

def display(self):
if self.logo:
logger.debug(self.logo)
Expand Down
9 changes: 7 additions & 2 deletions sanic/application/state.py
Expand Up @@ -5,7 +5,9 @@
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

Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion sanic/blueprint_group.py
Expand Up @@ -5,7 +5,7 @@
from typing import TYPE_CHECKING, List, Optional, Union


if TYPE_CHECKING:
if TYPE_CHECKING: # no cov
from sanic.blueprints import Blueprint


Expand Down
4 changes: 2 additions & 2 deletions sanic/blueprints.py
Expand Up @@ -36,8 +36,8 @@
)


if TYPE_CHECKING:
from sanic import Sanic # noqa
if TYPE_CHECKING: # no cov
from sanic import Sanic


def lazy(func, as_decorator=True):
Expand Down
2 changes: 2 additions & 0 deletions sanic/config.py
Expand Up @@ -18,6 +18,7 @@
DEFAULT_CONFIG = {
"_FALLBACK_ERROR_FORMAT": _default,
"ACCESS_LOG": True,
"AUTO_EXTEND": True,
"AUTO_RELOAD": False,
"EVENT_AUTOREGISTER": False,
"FORWARDED_FOR_HEADER": "X-Forwarded-For",
Expand Down Expand Up @@ -59,6 +60,7 @@ def _is_setter(member: object):

class Config(dict, metaclass=DescriptorMeta):
ACCESS_LOG: bool
AUTO_EXTEND: bool
AUTO_RELOAD: bool
EVENT_AUTOREGISTER: bool
FORWARDED_FOR_HEADER: str
Expand Down
2 changes: 1 addition & 1 deletion sanic/http.py
Expand Up @@ -3,7 +3,7 @@
from typing import TYPE_CHECKING, Optional


if TYPE_CHECKING:
if TYPE_CHECKING: # no cov
from sanic.request import Request
from sanic.response import BaseHTTPResponse

Expand Down
2 changes: 1 addition & 1 deletion sanic/request.py
Expand Up @@ -15,7 +15,7 @@
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

Expand Down
2 changes: 2 additions & 0 deletions sanic/server/runners.py
Expand Up @@ -21,6 +21,7 @@
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
Expand Down Expand Up @@ -116,6 +117,7 @@ def serve(
**asyncio_server_kwargs,
)

setup_ext(app)
if run_async:
return AsyncioServer(
app=app,
Expand Down
4 changes: 3 additions & 1 deletion sanic/views.py
Expand Up @@ -13,7 +13,7 @@
from sanic.models.handler_types import RouteHandler


if TYPE_CHECKING:
if TYPE_CHECKING: # no cov
from sanic import Sanic
from sanic.blueprints import Blueprint

Expand Down Expand Up @@ -81,6 +81,8 @@ def __init_subclass__(

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
Expand Down
4 changes: 2 additions & 2 deletions sanic/worker.py
Expand Up @@ -15,10 +15,10 @@

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()


Expand Down
1 change: 1 addition & 0 deletions setup.py
Expand Up @@ -147,6 +147,7 @@ def open_local(paths, mode="r", encoding="utf8"):
"dev": dev_require,
"docs": docs_require,
"all": all_require,
"ext": ["sanic-ext"],
}

setup_kwargs["install_requires"] = requirements
Expand Down

0 comments on commit d043aa0

Please sign in to comment.