Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto extend with Sanic Extensions #2308

Merged
merged 21 commits into from Dec 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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