Compare commits

..

No commits in common. "ruff-only" and "main" have entirely different histories.

238 changed files with 2797 additions and 1315 deletions

View File

@ -1,4 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*-
# #
# Sanic documentation build configuration file, created by # Sanic documentation build configuration file, created by
# sphinx-quickstart on Sun Dec 25 18:07:21 2016. # sphinx-quickstart on Sun Dec 25 18:07:21 2016.
@ -9,6 +10,7 @@
import os import os
import sys import sys
# Add support for auto-doc # Add support for auto-doc
@ -17,7 +19,8 @@ import sys
root_directory = os.path.dirname(os.getcwd()) root_directory = os.path.dirname(os.getcwd())
sys.path.insert(0, root_directory) sys.path.insert(0, root_directory)
import sanic # noqa: E402 import sanic
# -- General configuration ------------------------------------------------ # -- General configuration ------------------------------------------------

View File

@ -1,7 +1,10 @@
# -*- coding: utf-8 -*-
import asyncio import asyncio
from sanic import Sanic from sanic import Sanic
app = Sanic("Example") app = Sanic("Example")

View File

@ -3,6 +3,7 @@ from random import randint
from sanic import Sanic from sanic import Sanic
from sanic.response import text from sanic.response import text
app = Sanic("Example") app = Sanic("Example")
@ -24,6 +25,5 @@ def key_exist_handler(request):
return text("num does not exist in request") return text("num does not exist in request")
if __name__ == "__main__": if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000, debug=True) app.run(host="0.0.0.0", port=8000, debug=True)

View File

@ -1,8 +1,11 @@
# -*- coding: utf-8 -*-
from functools import wraps from functools import wraps
from sanic import Sanic from sanic import Sanic
from sanic.response import json from sanic.response import json
app = Sanic("Example") app = Sanic("Example")

View File

@ -1,6 +1,7 @@
from sanic import Blueprint, Sanic from sanic import Blueprint, Sanic
from sanic.response import text from sanic.response import text
""" """
Demonstrates that blueprint request middleware are executed in the order they Demonstrates that blueprint request middleware are executed in the order they
are added. And blueprint response middleware are executed in _reverse_ order. are added. And blueprint response middleware are executed in _reverse_ order.

View File

@ -1,6 +1,7 @@
from sanic import Blueprint, Sanic from sanic import Blueprint, Sanic
from sanic.response import file, json from sanic.response import file, json
app = Sanic("Example") app = Sanic("Example")
blueprint = Blueprint("bp_example", url_prefix="/my_blueprint") blueprint = Blueprint("bp_example", url_prefix="/my_blueprint")
blueprint2 = Blueprint("bp_example2", url_prefix="/my_blueprint2") blueprint2 = Blueprint("bp_example2", url_prefix="/my_blueprint2")

View File

@ -2,6 +2,7 @@ from asyncio import sleep
from sanic import Sanic, response from sanic import Sanic, response
app = Sanic("DelayedResponseApp", strict_slashes=True) app = Sanic("DelayedResponseApp", strict_slashes=True)
app.config.AUTO_EXTEND = False app.config.AUTO_EXTEND = False

View File

@ -10,6 +10,7 @@ an external service.
from sanic.exceptions import SanicException from sanic.exceptions import SanicException
from sanic.handlers import ErrorHandler from sanic.handlers import ErrorHandler
""" """
Imports and code relevant for our CustomHandler class Imports and code relevant for our CustomHandler class
(Ordinarily this would be in a separate file) (Ordinarily this would be in a separate file)
@ -38,6 +39,7 @@ server's error_handler to an instance of our CustomHandler
from sanic import Sanic from sanic import Sanic
handler = CustomHandler() handler = CustomHandler()
app = Sanic("Example", error_handler=handler) app = Sanic("Example", error_handler=handler)

View File

@ -1,5 +1,6 @@
from sanic import Sanic, response from sanic import Sanic, response
app = Sanic("Example") app = Sanic("Example")

View File

@ -2,6 +2,7 @@ from sanic import Sanic, response, text
from sanic.handlers import ErrorHandler from sanic.handlers import ErrorHandler
from sanic.server.async_server import AsyncioServer from sanic.server.async_server import AsyncioServer
HTTP_PORT = 9999 HTTP_PORT = 9999
HTTPS_PORT = 8888 HTTPS_PORT = 8888
@ -35,7 +36,9 @@ def proxy(request, path):
@https.main_process_start @https.main_process_start
async def start(app, _): async def start(app, _):
http_server = await http.create_server(port=HTTP_PORT, return_asyncio_server=True) http_server = await http.create_server(
port=HTTP_PORT, return_asyncio_server=True
)
app.add_task(runner(http, http_server)) app.add_task(runner(http, http_server))
app.ctx.http_server = http_server app.ctx.http_server = http_server
app.ctx.http = http app.ctx.http = http
@ -66,6 +69,5 @@ async def runner(app: Sanic, app_server: AsyncioServer):
app.is_running = False app.is_running = False
app.is_stopping = True app.is_stopping = True
if __name__ == "__main__": if __name__ == "__main__":
https.run(port=HTTPS_PORT, debug=True) https.run(port=HTTPS_PORT, debug=True)

View File

@ -5,6 +5,7 @@ import httpx
from sanic import Sanic from sanic import Sanic
from sanic.response import json from sanic.response import json
app = Sanic("Example") app = Sanic("Example")
sem = None sem = None

View File

@ -1,8 +1,10 @@
import logging import logging
from contextvars import ContextVar from contextvars import ContextVar
from sanic import Sanic, response from sanic import Sanic, response
log = logging.getLogger(__name__) log = logging.getLogger(__name__)

View File

@ -1,5 +1,6 @@
import logging import logging
import socket import socket
from os import getenv from os import getenv
from platform import node from platform import node
from uuid import getnode as get_mac from uuid import getnode as get_mac
@ -10,6 +11,7 @@ from sanic import Sanic
from sanic.request import Request from sanic.request import Request
from sanic.response import json from sanic.response import json
log = logging.getLogger("logdna") log = logging.getLogger("logdna")
log.setLevel(logging.INFO) log.setLevel(logging.INFO)
@ -33,7 +35,9 @@ logdna_options = {
"mac": get_mac_address(), "mac": get_mac_address(),
} }
logdna_handler = LogDNAHandler(getenv("LOGDNA_API_KEY"), options=logdna_options) logdna_handler = LogDNAHandler(
getenv("LOGDNA_API_KEY"), options=logdna_options
)
logdna = logging.getLogger(__name__) logdna = logging.getLogger(__name__)
logdna.setLevel(logging.INFO) logdna.setLevel(logging.INFO)
@ -44,7 +48,7 @@ app = Sanic("Example")
@app.middleware @app.middleware
def log_request(request: Request): def log_request(request: Request):
logdna.info(f"I was Here with a new Request to URL: {request.url}") logdna.info("I was Here with a new Request to URL: {}".format(request.url))
@app.route("/") @app.route("/")

View File

@ -4,6 +4,7 @@ Modify header or status in response
from sanic import Sanic, response from sanic import Sanic, response
app = Sanic("Example") app = Sanic("Example")

View File

@ -2,6 +2,7 @@ import logging
from sanic import Sanic, text from sanic import Sanic, text
logging_format = "[%(asctime)s] %(process)d-%(levelname)s " logging_format = "[%(asctime)s] %(process)d-%(levelname)s "
logging_format += "%(module)s::%(funcName)s():l%(lineno)d: " logging_format += "%(module)s::%(funcName)s():l%(lineno)d: "
logging_format += "%(message)s" logging_format += "%(message)s"

View File

@ -11,6 +11,7 @@ Run with xdist params:
import re import re
import pytest import pytest
from sanic_testing import SanicTestClient from sanic_testing import SanicTestClient
from sanic_testing.testing import PORT as PORT_BASE from sanic_testing.testing import PORT as PORT_BASE

View File

@ -1,5 +1,6 @@
from sanic import Sanic, response from sanic import Sanic, response
app = Sanic("Example") app = Sanic("Example")

View File

@ -4,6 +4,7 @@ from sanic.response import stream, text
from sanic.views import HTTPMethodView from sanic.views import HTTPMethodView
from sanic.views import stream as stream_decorator from sanic.views import stream as stream_decorator
bp = Blueprint("bp_example") bp = Blueprint("bp_example")
app = Sanic("Example") app = Sanic("Example")

View File

@ -4,6 +4,7 @@ from sanic import Sanic, response
from sanic.config import Config from sanic.config import Config
from sanic.exceptions import RequestTimeout from sanic.exceptions import RequestTimeout
Config.REQUEST_TIMEOUT = 1 Config.REQUEST_TIMEOUT = 1
app = Sanic("Example") app = Sanic("Example")

View File

@ -6,6 +6,7 @@ from sanic import Sanic
from sanic.exceptions import SanicException from sanic.exceptions import SanicException
from sanic.handlers import ErrorHandler from sanic.handlers import ErrorHandler
rollbar.init(getenv("ROLLBAR_API_KEY")) rollbar.init(getenv("ROLLBAR_API_KEY"))

View File

@ -10,6 +10,7 @@ from pathlib import Path
from sanic import Sanic, response from sanic import Sanic, response
app = Sanic("Example") app = Sanic("Example")
@ -42,7 +43,9 @@ async def handler_file(request):
@app.route("/file_stream") @app.route("/file_stream")
async def handler_file_stream(request): async def handler_file_stream(request):
return await response.file_stream(Path("../") / "setup.py", chunk_size=1024) return await response.file_stream(
Path("../") / "setup.py", chunk_size=1024
)
@app.post("/stream", stream=True) @app.post("/stream", stream=True)

View File

@ -4,6 +4,7 @@ import uvloop
from sanic import Sanic, response from sanic import Sanic, response
app = Sanic("Example") app = Sanic("Example")

View File

@ -1,4 +1,5 @@
import asyncio import asyncio
from signal import SIGINT, signal from signal import SIGINT, signal
import uvloop import uvloop
@ -6,6 +7,7 @@ import uvloop
from sanic import Sanic, response from sanic import Sanic, response
from sanic.server import AsyncioServer from sanic.server import AsyncioServer
app = Sanic("Example") app = Sanic("Example")
@ -33,10 +35,11 @@ async def after_server_stop(app, loop):
async def test(request): async def test(request):
return response.json({"answer": "42"}) return response.json({"answer": "42"})
if __name__ == "__main__": if __name__ == "__main__":
asyncio.set_event_loop(uvloop.new_event_loop()) asyncio.set_event_loop(uvloop.new_event_loop())
serv_coro = app.create_server(host="0.0.0.0", port=8000, return_asyncio_server=True) serv_coro = app.create_server(
host="0.0.0.0", port=8000, return_asyncio_server=True
)
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
serv_task = asyncio.ensure_future(serv_coro, loop=loop) serv_task = asyncio.ensure_future(serv_coro, loop=loop)
signal(SIGINT, lambda s, f: loop.stop()) signal(SIGINT, lambda s, f: loop.stop())

View File

@ -6,6 +6,7 @@ from sentry_sdk.integrations.sanic import SanicIntegration
from sanic import Sanic from sanic import Sanic
from sanic.response import json from sanic.response import json
sentry_init( sentry_init(
dsn=getenv("SENTRY_DSN"), dsn=getenv("SENTRY_DSN"),
integrations=[SanicIntegration()], integrations=[SanicIntegration()],

View File

@ -2,6 +2,7 @@ from sanic import Sanic
from sanic.response import text from sanic.response import text
from sanic.views import HTTPMethodView from sanic.views import HTTPMethodView
app = Sanic("some_name") app = Sanic("some_name")

View File

@ -1,5 +1,6 @@
from sanic import Sanic from sanic import Sanic
app = Sanic("Example") app = Sanic("Example")
app.static("/", "./static") app.static("/", "./static")

View File

@ -1,6 +1,7 @@
from sanic import Sanic from sanic import Sanic
from sanic import response as res from sanic import response as res
app = Sanic("Example") app = Sanic("Example")

View File

@ -4,6 +4,7 @@ from sanic import Sanic, response
from sanic.exceptions import ServerError from sanic.exceptions import ServerError
from sanic.log import logger as log from sanic.log import logger as log
app = Sanic("Example") app = Sanic("Example")
@ -19,7 +20,7 @@ def test_sync(request):
@app.route("/dynamic/<name>/<i:int>") @app.route("/dynamic/<name>/<i:int>")
def test_params(request, name, i): def test_params(request, name, i):
return response.text(f"yeehaww {name} {i}") return response.text("yeehaww {} {}".format(name, i))
@app.route("/exception") @app.route("/exception")
@ -42,7 +43,9 @@ async def test_file(request):
@app.route("/file_stream") @app.route("/file_stream")
async def test_file_stream(request): async def test_file_stream(request):
return await response.file_stream(os.path.abspath("setup.py"), chunk_size=1024) return await response.file_stream(
os.path.abspath("setup.py"), chunk_size=1024
)
# ----------------------------------------------- # # ----------------------------------------------- #

View File

@ -1,5 +1,6 @@
from sanic import Sanic, response from sanic import Sanic, response
app = Sanic("Example") app = Sanic("Example")

View File

@ -1,5 +1,6 @@
from sanic import Sanic, response from sanic import Sanic, response
app = Sanic("Example") app = Sanic("Example")
@ -13,7 +14,7 @@ async def index(request):
@app.route("/posts/<post_id>") @app.route("/posts/<post_id>")
async def post_handler(request, post_id): async def post_handler(request, post_id):
return response.text(f"Post - {post_id}") return response.text("Post - {}".format(post_id))
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -2,6 +2,7 @@ from sanic import Sanic
from sanic.blueprints import Blueprint from sanic.blueprints import Blueprint
from sanic.response import json from sanic.response import json
app = Sanic(name="blue-print-group-version-example") app = Sanic(name="blue-print-group-version-example")
bp1 = Blueprint(name="ultron", url_prefix="/ultron") bp1 = Blueprint(name="ultron", url_prefix="/ultron")

View File

@ -1,6 +1,7 @@
from sanic import Sanic, response from sanic import Sanic, response
from sanic.blueprints import Blueprint from sanic.blueprints import Blueprint
# Usage # Usage
# curl -H "Host: example.com" localhost:8000 # curl -H "Host: example.com" localhost:8000
# curl -H "Host: sub.example.com" localhost:8000 # curl -H "Host: sub.example.com" localhost:8000
@ -11,7 +12,9 @@ app = Sanic("Example")
bp = Blueprint("bp", host="bp.example.com") bp = Blueprint("bp", host="bp.example.com")
@app.route("/", host=["example.com", "somethingelse.com", "therestofyourdomains.com"]) @app.route(
"/", host=["example.com", "somethingelse.com", "therestofyourdomains.com"]
)
async def hello_0(request): async def hello_0(request):
return response.text("Some defaults") return response.text("Some defaults")

View File

@ -1,6 +1,7 @@
from sanic import Sanic from sanic import Sanic
from sanic.response import redirect from sanic.response import redirect
app = Sanic("Example") app = Sanic("Example")

View File

@ -7,10 +7,14 @@ from emoji import EMOJI
COLUMN_PATTERN = re.compile(r"---:1\s*(.*?)\s*:--:1\s*(.*?)\s*:---", re.DOTALL) COLUMN_PATTERN = re.compile(r"---:1\s*(.*?)\s*:--:1\s*(.*?)\s*:---", re.DOTALL)
PYTHON_HIGHLIGHT_PATTERN = re.compile(r"```python\{+.*?\}", re.DOTALL) PYTHON_HIGHLIGHT_PATTERN = re.compile(r"```python\{+.*?\}", re.DOTALL)
BASH_HIGHLIGHT_PATTERN = re.compile(r"```bash\{+.*?\}", re.DOTALL) BASH_HIGHLIGHT_PATTERN = re.compile(r"```bash\{+.*?\}", re.DOTALL)
NOTIFICATION_PATTERN = re.compile(r":::\s*(\w+)\s*(.*?)\n([\s\S]*?):::", re.MULTILINE) NOTIFICATION_PATTERN = re.compile(
r":::\s*(\w+)\s*(.*?)\n([\s\S]*?):::", re.MULTILINE
)
EMOJI_PATTERN = re.compile(r":(\w+):") EMOJI_PATTERN = re.compile(r":(\w+):")
CURRENT_DIR = Path(__file__).parent CURRENT_DIR = Path(__file__).parent
SOURCE_DIR = CURRENT_DIR.parent.parent.parent.parent / "sanic-guide" / "src" / "en" SOURCE_DIR = (
CURRENT_DIR.parent.parent.parent.parent / "sanic-guide" / "src" / "en"
)
def convert_columns(content: str): def convert_columns(content: str):

View File

@ -1,5 +0,0 @@
[tool.ruff]
extend = "../pyproject.toml"
[tool.ruff.isort]
known-first-party = ["webapp"]

View File

@ -13,7 +13,9 @@ def do_footer(builder: Builder, request: Request) -> None:
def _pagination(request: Request) -> Builder: def _pagination(request: Request) -> Builder:
return E.div(_pagination_left(request), _pagination_right(request), class_="level") return E.div(
_pagination_left(request), _pagination_right(request), class_="level"
)
def _pagination_left(request: Request) -> Builder: def _pagination_left(request: Request) -> Builder:
@ -62,7 +64,9 @@ def _content() -> Builder:
href="https://github.com/sanic-org/sanic/blob/master/LICENSE", href="https://github.com/sanic-org/sanic/blob/master/LICENSE",
target="_blank", target="_blank",
rel="nofollow noopener noreferrer", rel="nofollow noopener noreferrer",
).br()(E.small(f"Copyright © 2018-{year} Sanic Community Organization")), ).br()(
E.small(f"Copyright © 2018-{year} Sanic Community Organization")
),
) )
return E.div( return E.div(
inner, inner,

View File

@ -1,14 +1,17 @@
from webapp.display.layouts.models import MenuItem
from html5tagger import Builder, E # type: ignore from html5tagger import Builder, E # type: ignore
from sanic import Request from sanic import Request
from webapp.display.layouts.models import MenuItem
def do_navbar(builder: Builder, request: Request) -> None: def do_navbar(builder: Builder, request: Request) -> None:
navbar_items = [ navbar_items = [
_render_navbar_item(item, request) for item in request.app.config.NAVBAR _render_navbar_item(item, request)
for item in request.app.config.NAVBAR
] ]
container = E.div(_search_form(request), *navbar_items, class_="navbar-end") container = E.div(
_search_form(request), *navbar_items, class_="navbar-end"
)
builder.nav( builder.nav(
E.div(container, class_="navbar-menu"), E.div(container, class_="navbar-menu"),
@ -43,7 +46,10 @@ def _render_navbar_item(item: MenuItem, request: Request) -> Builder:
return E.div( return E.div(
E.a(item.label, class_="navbar-link"), E.a(item.label, class_="navbar-link"),
E.div( E.div(
*(_render_navbar_item(subitem, request) for subitem in item.items), *(
_render_navbar_item(subitem, request)
for subitem in item.items
),
class_="navbar-dropdown", class_="navbar-dropdown",
), ),
class_="navbar-item has-dropdown is-hoverable", class_="navbar-item has-dropdown is-hoverable",

View File

@ -1,9 +1,9 @@
from html5tagger import Builder, E # type: ignore
from sanic import Request
from webapp.display.layouts.models import MenuItem from webapp.display.layouts.models import MenuItem
from webapp.display.text import slugify from webapp.display.text import slugify
from html5tagger import Builder, E # type: ignore
from sanic import Request
def do_sidebar(builder: Builder, request: Request) -> None: def do_sidebar(builder: Builder, request: Request) -> None:
builder.a(class_="burger")(E.span().span().span().span()) builder.a(class_="burger")(E.span().span().span().span())
@ -15,7 +15,9 @@ def _menu_items(request: Request) -> list[Builder]:
_sanic_logo(request), _sanic_logo(request),
*_sidebar_items(request), *_sidebar_items(request),
E.hr(), E.hr(),
E.p("Current with version ").strong(request.app.config.GENERAL.current_version), E.p("Current with version ").strong(
request.app.config.GENERAL.current_version
),
E.hr(), E.hr(),
E.p("Want more? ").a( E.p("Want more? ").a(
"sanicbook.com", href="https://sanicbook.com", target="_blank" "sanicbook.com", href="https://sanicbook.com", target="_blank"
@ -71,7 +73,9 @@ def _single_sidebar_item(item: MenuItem, request: Request) -> Builder:
kwargs = {} kwargs = {}
classes: list[str] = [] classes: list[str] = []
li_classes = "menu-item" li_classes = "menu-item"
_, page, _ = request.app.ctx.get_page(request.ctx.language, item.path or "") _, page, _ = request.app.ctx.get_page(
request.ctx.language, item.path or ""
)
if request.path == path: if request.path == path:
classes.append("is-active") classes.append("is-active")
if item.href: if item.href:

View File

@ -1,12 +1,12 @@
from contextlib import contextmanager from contextlib import contextmanager
from typing import Generator from typing import Generator
from sanic import Request
from webapp.display.layouts.elements.footer import do_footer from webapp.display.layouts.elements.footer import do_footer
from webapp.display.layouts.elements.navbar import do_navbar from webapp.display.layouts.elements.navbar import do_navbar
from webapp.display.layouts.elements.sidebar import do_sidebar from webapp.display.layouts.elements.sidebar import do_sidebar
from sanic import Request
from .base import BaseLayout from .base import BaseLayout

View File

@ -1,7 +1,6 @@
import re import re
from textwrap import dedent from textwrap import dedent
from html5tagger import HTML, Builder, E # type: ignore
from mistune import HTMLRenderer, create_markdown, escape from mistune import HTMLRenderer, create_markdown, escape
from mistune.directives import RSTDirective, TableOfContents from mistune.directives import RSTDirective, TableOfContents
from mistune.util import safe_entity from mistune.util import safe_entity
@ -9,6 +8,8 @@ from pygments import highlight
from pygments.formatters import html from pygments.formatters import html
from pygments.lexers import get_lexer_by_name from pygments.lexers import get_lexer_by_name
from html5tagger import HTML, Builder, E # type: ignore
from .code_style import SanicCodeStyle from .code_style import SanicCodeStyle
from .plugins.attrs import Attributes from .plugins.attrs import Attributes
from .plugins.columns import Column from .plugins.columns import Column
@ -36,9 +37,9 @@ class DocsRenderer(HTMLRenderer):
class_="code-block__copy", class_="code-block__copy",
onclick="copyCode(this)", onclick="copyCode(this)",
): ):
builder.div(class_="code-block__rectangle code-block__filled").div( builder.div(
class_="code-block__rectangle code-block__outlined" class_="code-block__rectangle code-block__filled"
) ).div(class_="code-block__rectangle code-block__outlined")
else: else:
builder.pre(E.code(escape(code))) builder.pre(E.code(escape(code)))
return str(builder) return str(builder)
@ -46,7 +47,9 @@ class DocsRenderer(HTMLRenderer):
def heading(self, text: str, level: int, **attrs) -> str: def heading(self, text: str, level: int, **attrs) -> str:
ident = slugify(text) ident = slugify(text)
if level > 1: if level > 1:
text += self._make_tag("a", {"href": f"#{ident}", "class": "anchor"}, "#") text += self._make_tag(
"a", {"href": f"#{ident}", "class": "anchor"}, "#"
)
return self._make_tag( return self._make_tag(
f"h{level}", {"id": ident, "class": f"is-size-{level}"}, text f"h{level}", {"id": ident, "class": f"is-size-{level}"}, text
) )
@ -90,7 +93,9 @@ class DocsRenderer(HTMLRenderer):
def _make_tag( def _make_tag(
self, tag: str, attributes: dict[str, str], text: str | None = None self, tag: str, attributes: dict[str, str], text: str | None = None
) -> str: ) -> str:
attrs = " ".join(f'{key}="{value}"' for key, value in attributes.items()) attrs = " ".join(
f'{key}="{value}"' for key, value in attributes.items()
)
if text is None: if text is None:
return f"<{tag} {attrs} />" return f"<{tag} {attrs} />"
return f"<{tag} {attrs}>{text}</{tag}>" return f"<{tag} {attrs}>{text}</{tag}>"

View File

@ -10,6 +10,7 @@ from html import escape
from docstring_parser import Docstring, DocstringParam, DocstringRaises from docstring_parser import Docstring, DocstringParam, DocstringRaises
from docstring_parser import parse as parse_docstring from docstring_parser import parse as parse_docstring
from docstring_parser.common import DocstringExample from docstring_parser.common import DocstringExample
from html5tagger import HTML, Builder, E # type: ignore from html5tagger import HTML, Builder, E # type: ignore
from ..markdown import render_markdown, slugify from ..markdown import render_markdown, slugify
@ -119,7 +120,9 @@ def _extract_docobjects(package_name: str) -> dict[str, DocObject]:
docstrings = {} docstrings = {}
package = importlib.import_module(package_name) package = importlib.import_module(package_name)
for _, name, _ in pkgutil.walk_packages(package.__path__, package_name + "."): for _, name, _ in pkgutil.walk_packages(
package.__path__, package_name + "."
):
module = importlib.import_module(name) module = importlib.import_module(name)
for obj_name, obj in inspect.getmembers(module): for obj_name, obj in inspect.getmembers(module):
if ( if (
@ -153,7 +156,9 @@ def _docobject_to_html(
) -> None: ) -> None:
anchor_id = slugify(docobject.full_name.replace(".", "-")) anchor_id = slugify(docobject.full_name.replace(".", "-"))
anchor = E.a("#", class_="anchor", href=f"#{anchor_id}") anchor = E.a("#", class_="anchor", href=f"#{anchor_id}")
class_name, heading = _define_heading_and_class(docobject, anchor, as_method) class_name, heading = _define_heading_and_class(
docobject, anchor, as_method
)
with builder.div(class_=class_name): with builder.div(class_=class_name):
builder(heading) builder(heading)
@ -207,7 +212,9 @@ def _docobject_to_html(
if docobject.docstring.params: if docobject.docstring.params:
with builder.div(class_="box mt-5"): with builder.div(class_="box mt-5"):
builder.h5("Parameters", class_="is-size-5 has-text-weight-bold") builder.h5(
"Parameters", class_="is-size-5 has-text-weight-bold"
)
_render_params(builder, docobject.docstring.params) _render_params(builder, docobject.docstring.params)
if docobject.docstring.returns: if docobject.docstring.returns:
@ -232,7 +239,9 @@ def _signature_to_html(
parts = [] parts = []
parts.append("<span class='function-signature'>") parts.append("<span class='function-signature'>")
for decorator in decorators: for decorator in decorators:
parts.append(f"<span class='function-decorator'>@{decorator}</span><br>") parts.append(
f"<span class='function-decorator'>@{decorator}</span><br>"
)
parts.append( parts.append(
f"<span class='is-italic'>{object_type}</span> " f"<span class='is-italic'>{object_type}</span> "
f"<span class='has-text-weight-bold'>{name}</span>(" f"<span class='has-text-weight-bold'>{name}</span>("
@ -246,7 +255,9 @@ def _signature_to_html(
annotation = "" annotation = ""
if param.annotation != inspect.Parameter.empty: if param.annotation != inspect.Parameter.empty:
annotation = escape(str(param.annotation)) annotation = escape(str(param.annotation))
parts.append(f": <span class='param-annotation'>{annotation}</span>") parts.append(
f": <span class='param-annotation'>{annotation}</span>"
)
if param.default != inspect.Parameter.empty: if param.default != inspect.Parameter.empty:
default = escape(str(param.default)) default = escape(str(param.default))
if annotation == "str": if annotation == "str":
@ -257,7 +268,9 @@ def _signature_to_html(
parts.append(")") parts.append(")")
if signature.return_annotation != inspect.Signature.empty: if signature.return_annotation != inspect.Signature.empty:
return_annotation = escape(str(signature.return_annotation)) return_annotation = escape(str(signature.return_annotation))
parts.append(f": -> <span class='return-annotation'>{return_annotation}</span>") parts.append(
f": -> <span class='return-annotation'>{return_annotation}</span>"
)
parts.append("</span>") parts.append("</span>")
return "".join(parts) return "".join(parts)
@ -305,7 +318,10 @@ def _render_params(builder: Builder, params: list[DocstringParam]) -> None:
builder.dd( builder.dd(
HTML( HTML(
render_markdown( render_markdown(
param.description or param.arg_name or param.type_name or "" param.description
or param.arg_name
or param.type_name
or ""
) )
) )
) )
@ -318,7 +334,11 @@ def _render_raises(builder: Builder, raises: list[DocstringRaises]) -> None:
with builder.dl(class_="mt-2"): with builder.dl(class_="mt-2"):
builder.dt(raise_.type_name, class_="is-family-monospace") builder.dt(raise_.type_name, class_="is-family-monospace")
builder.dd( builder.dd(
HTML(render_markdown(raise_.description or raise_.type_name or "")) HTML(
render_markdown(
raise_.description or raise_.type_name or ""
)
)
) )
@ -334,7 +354,11 @@ def _render_returns(builder: Builder, docobject: DocObject) -> None:
if not return_type or return_type == inspect.Signature.empty: if not return_type or return_type == inspect.Signature.empty:
return_type = "N/A" return_type = "N/A"
term = "Return" if not docobject.docstring.returns.is_generator else "Yields" term = (
"Return"
if not docobject.docstring.returns.is_generator
else "Yields"
)
builder.h5(term, class_="is-size-5 has-text-weight-bold") builder.h5(term, class_="is-size-5 has-text-weight-bold")
with builder.dl(class_="mt-2"): with builder.dl(class_="mt-2"):
builder.dt(return_type, class_="is-family-monospace") builder.dt(return_type, class_="is-family-monospace")
@ -349,11 +373,17 @@ def _render_returns(builder: Builder, docobject: DocObject) -> None:
) )
def _render_examples(builder: Builder, examples: list[DocstringExample]) -> None: def _render_examples(
builder: Builder, examples: list[DocstringExample]
) -> None:
with builder.div(class_="box mt-5"): with builder.div(class_="box mt-5"):
builder.h5("Examples", class_="is-size-5 has-text-weight-bold") builder.h5("Examples", class_="is-size-5 has-text-weight-bold")
for example in examples: for example in examples:
with builder.div(class_="mt-2"): with builder.div(class_="mt-2"):
builder( builder(
HTML(render_markdown(example.description or example.snippet or "")) HTML(
render_markdown(
example.description or example.snippet or ""
)
)
) )

View File

@ -2,6 +2,7 @@ from __future__ import annotations
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from typing import Type
from frontmatter import parse from frontmatter import parse
@ -11,8 +12,10 @@ from ..layouts.main import MainLayout
from ..markdown import render_markdown from ..markdown import render_markdown
from .docobject import organize_docobjects from .docobject import organize_docobjects
_PAGE_CACHE: dict[str, dict[str, tuple[Page | None, Page | None, Page | None]]] = {} _PAGE_CACHE: dict[
_LAYOUTS_CACHE: dict[str, type[BaseLayout]] = { str, dict[str, tuple[Page | None, Page | None, Page | None]]
] = {}
_LAYOUTS_CACHE: dict[str, Type[BaseLayout]] = {
"home": HomeLayout, "home": HomeLayout,
"main": MainLayout, "main": MainLayout,
} }
@ -40,7 +43,7 @@ class Page:
DEFAULT_LANGUAGE = _DEFAULT DEFAULT_LANGUAGE = _DEFAULT
def get_layout(self) -> type[BaseLayout]: def get_layout(self) -> Type[BaseLayout]:
return _LAYOUTS_CACHE[self.meta.layout] return _LAYOUTS_CACHE[self.meta.layout]
@property @property

View File

@ -1,12 +1,13 @@
from __future__ import annotations from __future__ import annotations
from contextlib import contextmanager from contextlib import contextmanager
from typing import Type
from webapp.display.base import BaseRenderer
from html5tagger import HTML, Builder # type: ignore from html5tagger import HTML, Builder # type: ignore
from sanic import Request from sanic import Request
from webapp.display.base import BaseRenderer
from ..layouts.base import BaseLayout from ..layouts.base import BaseLayout
from .page import Page from .page import Page
@ -20,9 +21,13 @@ class PageRenderer(BaseRenderer):
self._body(request, builder, language, path) self._body(request, builder, language, path)
return builder return builder
def _body(self, request: Request, builder: Builder, language: str, path: str): def _body(
self, request: Request, builder: Builder, language: str, path: str
):
prev_page, current_page, next_page = Page.get(language, path) prev_page, current_page, next_page = Page.get(language, path)
request.ctx.language = Page.DEFAULT_LANGUAGE if language == "api" else language request.ctx.language = (
Page.DEFAULT_LANGUAGE if language == "api" else language
)
request.ctx.current_page = current_page request.ctx.current_page = current_page
request.ctx.previous_page = prev_page request.ctx.previous_page = prev_page
request.ctx.next_page = next_page request.ctx.next_page = next_page
@ -34,7 +39,9 @@ class PageRenderer(BaseRenderer):
@contextmanager @contextmanager
def _base(self, request: Request, builder: Builder, page: Page | None): def _base(self, request: Request, builder: Builder, page: Page | None):
layout_type: type[BaseLayout] = page.get_layout() if page else BaseLayout layout_type: Type[BaseLayout] = (
page.get_layout() if page else BaseLayout
)
layout = layout_type(builder) layout = layout_type(builder)
with layout(request, builder.full): with layout(request, builder.full):
yield yield

View File

@ -2,11 +2,12 @@ from re import Match
from textwrap import dedent from textwrap import dedent
from typing import Any from typing import Any
from html5tagger import HTML, E
from mistune.block_parser import BlockParser from mistune.block_parser import BlockParser
from mistune.core import BlockState from mistune.core import BlockState
from mistune.directives import DirectivePlugin from mistune.directives import DirectivePlugin
from html5tagger import HTML, E
class Attributes(DirectivePlugin): class Attributes(DirectivePlugin):
def __call__(self, directive, md): def __call__(self, directive, md):
@ -15,7 +16,9 @@ class Attributes(DirectivePlugin):
if md.renderer.NAME == "html": if md.renderer.NAME == "html":
md.renderer.register("attrs", self._render) md.renderer.register("attrs", self._render)
def parse(self, block: BlockParser, m: Match, state: BlockState) -> dict[str, Any]: def parse(
self, block: BlockParser, m: Match, state: BlockState
) -> dict[str, Any]:
info = m.groupdict() info = m.groupdict()
options = dict(self.parse_options(m)) options = dict(self.parse_options(m))
new_state = block.state_cls() new_state = block.state_cls()

View File

@ -10,7 +10,9 @@ from mistune.markdown import Markdown
class Column(DirectivePlugin): class Column(DirectivePlugin):
def parse(self, block: BlockParser, m: Match, state: BlockState) -> dict[str, Any]: def parse(
self, block: BlockParser, m: Match, state: BlockState
) -> dict[str, Any]:
info = m.groupdict() info = m.groupdict()
new_state = block.state_cls() new_state = block.state_cls()
@ -34,7 +36,9 @@ class Column(DirectivePlugin):
def _render_column(self, renderer: HTMLRenderer, text: str, **attrs): def _render_column(self, renderer: HTMLRenderer, text: str, **attrs):
start = ( start = (
'<div class="columns mt-3 is-multiline">\n' if attrs.get("first") else "" '<div class="columns mt-3 is-multiline">\n'
if attrs.get("first")
else ""
) )
end = "</div>\n" if attrs.get("last") else "" end = "</div>\n" if attrs.get("last") else ""
col = f'<div class="column is-half">{text}</div>\n' col = f'<div class="column is-half">{text}</div>\n'

View File

@ -16,12 +16,16 @@ class Hook(DirectivePlugin):
for type_ in ("column", "tab"): for type_ in ("column", "tab"):
if token["type"] == type_: if token["type"] == type_:
maybe_next = ( maybe_next = (
state.tokens[idx + 1] if idx + 1 < len(state.tokens) else None state.tokens[idx + 1]
if idx + 1 < len(state.tokens)
else None
) )
token.setdefault("attrs", {}) token.setdefault("attrs", {})
if prev and prev["type"] != type_: if prev and prev["type"] != type_:
token["attrs"]["first"] = True token["attrs"]["first"] = True
if (maybe_next and maybe_next["type"] != type_) or not maybe_next: if (
maybe_next and maybe_next["type"] != type_
) or not maybe_next:
token["attrs"]["last"] = True token["attrs"]["last"] = True
prev = token prev = token

View File

@ -3,16 +3,19 @@ from re import Match
from textwrap import dedent from textwrap import dedent
from typing import Any from typing import Any
from html5tagger import HTML, E
from mistune import HTMLRenderer from mistune import HTMLRenderer
from mistune.block_parser import BlockParser from mistune.block_parser import BlockParser
from mistune.core import BlockState from mistune.core import BlockState
from mistune.directives import DirectivePlugin, RSTDirective from mistune.directives import DirectivePlugin, RSTDirective
from mistune.markdown import Markdown from mistune.markdown import Markdown
from html5tagger import HTML, E
class Mermaid(DirectivePlugin): class Mermaid(DirectivePlugin):
def parse(self, block: BlockParser, m: Match, state: BlockState) -> dict[str, Any]: def parse(
self, block: BlockParser, m: Match, state: BlockState
) -> dict[str, Any]:
info = m.groupdict() info = m.groupdict()
new_state = block.state_cls() new_state = block.state_cls()

View File

@ -1,6 +1,7 @@
from html5tagger import HTML, E
from mistune.directives import Admonition from mistune.directives import Admonition
from html5tagger import HTML, E
class Notification(Admonition): class Notification(Admonition):
SUPPORTED_NAMES = { SUPPORTED_NAMES = {
@ -19,8 +20,12 @@ class Notification(Admonition):
if md.renderer.NAME == "html": if md.renderer.NAME == "html":
md.renderer.register("admonition", self._render_admonition) md.renderer.register("admonition", self._render_admonition)
md.renderer.register("admonition_title", self._render_admonition_title) md.renderer.register(
md.renderer.register("admonition_content", self._render_admonition_content) "admonition_title", self._render_admonition_title
)
md.renderer.register(
"admonition_content", self._render_admonition_content
)
def _render_admonition(self, _, text, name, **attrs) -> str: def _render_admonition(self, _, text, name, **attrs) -> str:
return str( return str(

View File

@ -10,7 +10,9 @@ from mistune.markdown import Markdown
class Tabs(DirectivePlugin): class Tabs(DirectivePlugin):
def parse(self, block: BlockParser, m: Match, state: BlockState) -> dict[str, Any]: def parse(
self, block: BlockParser, m: Match, state: BlockState
) -> dict[str, Any]:
info = m.groupdict() info = m.groupdict()
new_state = block.state_cls() new_state = block.state_cls()
@ -39,7 +41,9 @@ class Tabs(DirectivePlugin):
def _render_tab(self, renderer: HTMLRenderer, text: str, **attrs): def _render_tab(self, renderer: HTMLRenderer, text: str, **attrs):
start = '<div class="tabs mt-6"><ul>\n' if attrs.get("first") else "" start = '<div class="tabs mt-6"><ul>\n' if attrs.get("first") else ""
end = ( end = (
'</ul></div><div class="tab-display"></div>\n' if attrs.get("last") else "" '</ul></div><div class="tab-display"></div>\n'
if attrs.get("last")
else ""
) )
content = f'<div class="tab-content">{text}</div>\n' content = f'<div class="tab-content">{text}</div>\n'
tab = f'<li><a>{attrs["title"]}</a>{content}</li>\n' tab = f'<li><a>{attrs["title"]}</a>{content}</li>\n'

View File

@ -1,11 +1,11 @@
from contextlib import contextmanager from contextlib import contextmanager
from urllib.parse import unquote from urllib.parse import unquote
from webapp.display.search.search import Searcher
from html5tagger import Builder, E # type: ignore from html5tagger import Builder, E # type: ignore
from sanic import Request from sanic import Request
from webapp.display.search.search import Searcher
from ..base import BaseRenderer from ..base import BaseRenderer
from ..layouts.main import MainLayout from ..layouts.main import MainLayout

View File

@ -5,7 +5,6 @@ from pathlib import Path
from typing import ClassVar from typing import ClassVar
from msgspec import Struct from msgspec import Struct
from webapp.display.page import Page from webapp.display.page import Page
@ -92,7 +91,9 @@ def _inverse_document_frequency(docs: list[Document]) -> dict[str, float]:
return {word: num_docs / count for word, count in word_count.items()} return {word: num_docs / count for word, count in word_count.items()}
def _tf_idf_vector(document: Document, idf: dict[str, float]) -> dict[str, float]: def _tf_idf_vector(
document: Document, idf: dict[str, float]
) -> dict[str, float]:
"""Calculate the TF-IDF vector for a document.""" """Calculate the TF-IDF vector for a document."""
return { return {
word: tf * idf[word] word: tf * idf[word]
@ -101,7 +102,9 @@ def _tf_idf_vector(document: Document, idf: dict[str, float]) -> dict[str, float
} }
def _cosine_similarity(vec1: dict[str, float], vec2: dict[str, float]) -> float: def _cosine_similarity(
vec1: dict[str, float], vec2: dict[str, float]
) -> float:
"""Calculate the cosine similarity between two vectors.""" """Calculate the cosine similarity between two vectors."""
if not vec1 or not vec2: if not vec1 or not vec2:
return 0.0 return 0.0
@ -123,7 +126,9 @@ def _search(
tf_idf_query = _tf_idf_vector( tf_idf_query = _tf_idf_vector(
Document(page=dummy_page, language=language).process(stemmer), idf Document(page=dummy_page, language=language).process(stemmer), idf
) )
similarities = [_cosine_similarity(tf_idf_query, vector) for vector in vectors] similarities = [
_cosine_similarity(tf_idf_query, vector) for vector in vectors
]
return [ return [
(similarity, document) (similarity, document)
for similarity, document in sorted( for similarity, document in sorted(
@ -150,13 +155,16 @@ class Searcher:
} }
self._vectors = { self._vectors = {
language: [ language: [
_tf_idf_vector(document, self._idf[language]) for document in documents _tf_idf_vector(document, self._idf[language])
for document in documents
] ]
for language, documents in self._documents.items() for language, documents in self._documents.items()
} }
self._stemmer = stemmer self._stemmer = stemmer
def search(self, query: str, language: str) -> list[tuple[float, Document]]: def search(
self, query: str, language: str
) -> list[tuple[float, Document]]:
return _search( return _search(
query, query,
language, language,

View File

@ -1,11 +1,11 @@
# from urllib.parse import unquote # from urllib.parse import unquote
from sanic import Blueprint, Request, Sanic, html
from webapp.display.page import Page from webapp.display.page import Page
from webapp.display.search.renderer import SearchRenderer from webapp.display.search.renderer import SearchRenderer
from webapp.display.search.search import Document, Searcher, Stemmer from webapp.display.search.search import Document, Searcher, Stemmer
from sanic import Blueprint, Request, Sanic, html
bp = Blueprint("search", url_prefix="/<language>/search") bp = Blueprint("search", url_prefix="/<language>/search")

View File

@ -1,7 +1,6 @@
from pathlib import Path from pathlib import Path
from msgspec import yaml from msgspec import yaml
from webapp.display.layouts.models import GeneralConfig, MenuItem from webapp.display.layouts.models import GeneralConfig, MenuItem

View File

@ -1,7 +1,5 @@
from pathlib import Path from pathlib import Path
from sanic import Request, Sanic, html, redirect
from webapp.display.layouts.models import MenuItem from webapp.display.layouts.models import MenuItem
from webapp.display.page import Page, PageRenderer from webapp.display.page import Page, PageRenderer
from webapp.endpoint.view import bp from webapp.endpoint.view import bp
@ -9,6 +7,8 @@ from webapp.worker.config import load_config, load_menu
from webapp.worker.reload import setup_livereload from webapp.worker.reload import setup_livereload
from webapp.worker.style import setup_style from webapp.worker.style import setup_style
from sanic import Request, Sanic, html, redirect
def _compile_sidebar_order(items: list[MenuItem]) -> list[str]: def _compile_sidebar_order(items: list[MenuItem]) -> list[str]:
order = [] order = []
@ -28,9 +28,13 @@ def create_app(root: Path) -> Sanic:
app.config.STYLE_DIR = root / "style" app.config.STYLE_DIR = root / "style"
app.config.NODE_MODULES_DIR = root / "node_modules" app.config.NODE_MODULES_DIR = root / "node_modules"
app.config.LANGUAGES = ["en"] app.config.LANGUAGES = ["en"]
app.config.SIDEBAR = load_menu(app.config.CONFIG_DIR / "en" / "sidebar.yaml") app.config.SIDEBAR = load_menu(
app.config.CONFIG_DIR / "en" / "sidebar.yaml"
)
app.config.NAVBAR = load_menu(app.config.CONFIG_DIR / "en" / "navbar.yaml") app.config.NAVBAR = load_menu(app.config.CONFIG_DIR / "en" / "navbar.yaml")
app.config.GENERAL = load_config(app.config.CONFIG_DIR / "en" / "general.yaml") app.config.GENERAL = load_config(
app.config.CONFIG_DIR / "en" / "general.yaml"
)
setup_livereload(app) setup_livereload(app)
setup_style(app) setup_style(app)
@ -62,6 +66,8 @@ def create_app(root: Path) -> Sanic:
@app.on_request @app.on_request
async def set_language(request: Request): async def set_language(request: Request):
request.ctx.language = request.match_info.get("language", Page.DEFAULT_LANGUAGE) request.ctx.language = request.match_info.get(
"language", Page.DEFAULT_LANGUAGE
)
return app return app

View File

@ -5,6 +5,7 @@ from queue import Empty, Queue
from typing import Any from typing import Any
import ujson import ujson
from sanic import Request, Sanic, Websocket from sanic import Request, Sanic, Websocket
@ -53,12 +54,16 @@ class Livereload:
"serverName": SERVER_NAME, "serverName": SERVER_NAME,
} }
def __init__(self, reload_queue: Queue, debug: bool, state: dict[str, Any]): def __init__(
self, reload_queue: Queue, debug: bool, state: dict[str, Any]
):
self.reload_queue = reload_queue self.reload_queue = reload_queue
self.app = Sanic(self.SERVER_NAME) self.app = Sanic(self.SERVER_NAME)
self.debug = debug self.debug = debug
self.state = state self.state = state
self.app.static("/livereload.js", Path(__file__).parent / "livereload.js") self.app.static(
"/livereload.js", Path(__file__).parent / "livereload.js"
)
self.app.add_websocket_route( self.app.add_websocket_route(
self.livereload_handler, "/livereload", name="livereload" self.livereload_handler, "/livereload", name="livereload"
) )
@ -104,5 +109,7 @@ class Livereload:
break break
def _run_reload_server(reload_queue: Queue, debug: bool, state: dict[str, Any]): def _run_reload_server(
reload_queue: Queue, debug: bool, state: dict[str, Any]
):
Livereload(reload_queue, debug, state).run() Livereload(reload_queue, debug, state).run()

View File

@ -1,11 +1,11 @@
# from scss.compiler import compile_string # from scss.compiler import compile_string
from pygments.formatters import html from pygments.formatters import html
from sanic import Sanic
from sass import compile as compile_scss from sass import compile as compile_scss
from webapp.display.code_style import SanicCodeStyle from webapp.display.code_style import SanicCodeStyle
from sanic import Sanic
def setup_style(app: Sanic) -> None: def setup_style(app: Sanic) -> None:
index = app.config.STYLE_DIR / "index.scss" index = app.config.STYLE_DIR / "index.scss"

View File

@ -2,28 +2,20 @@
requires = ["setuptools", "wheel"] requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[tool.ruff] [tool.black]
extend-select = ["I", "W", "UP", "C4", "ISC"] line-length = 79
# Worth selecting but still too broken: ASYNC, S, B, DTZ, FA
ignore = [
"D100",
"D101",
"D102",
"D103",
"E402",
"E741",
"F811",
"F821",
# ruff format complains about these:
"ISC001",
"W191",
]
show-source = true
show-fixes = true
[tool.ruff.isort] [tool.isort]
known-first-party = ["sanic"] atomic = true
known-third-party = ["pytest"] default_section = "THIRDPARTY"
include_trailing_comma = true
known_first_party = "sanic"
known_third_party = "pytest"
line_length = 79
lines_after_imports = 2
lines_between_types = 1
multi_line_output = 3
profile = "black"
[[tool.mypy.overrides]] [[tool.mypy.overrides]]
module = [ module = [

View File

@ -36,6 +36,7 @@ from sanic.response import (
) )
from sanic.server.websockets.impl import WebsocketImplProtocol as Websocket from sanic.server.websockets.impl import WebsocketImplProtocol as Websocket
DefaultSanic: TypeAlias = "Sanic[Config, SimpleNamespace]" DefaultSanic: TypeAlias = "Sanic[Config, SimpleNamespace]"
""" """
A type alias for a Sanic app with a default config and namespace. A type alias for a Sanic app with a default config and namespace.

View File

@ -1,6 +1,7 @@
from sanic.cli.app import SanicCLI from sanic.cli.app import SanicCLI
from sanic.compat import OS_IS_WINDOWS, enable_windows_color_support from sanic.compat import OS_IS_WINDOWS, enable_windows_color_support
if OS_IS_WINDOWS: if OS_IS_WINDOWS:
enable_windows_color_support() enable_windows_color_support()

View File

@ -5,6 +5,7 @@ import logging
import logging.config import logging.config
import re import re
import sys import sys
from asyncio import ( from asyncio import (
AbstractEventLoop, AbstractEventLoop,
CancelledError, CancelledError,
@ -31,12 +32,19 @@ from typing import (
Callable, Callable,
ClassVar, ClassVar,
Coroutine, Coroutine,
Deque,
Dict,
Generic, Generic,
Iterable, Iterable,
Iterator, Iterator,
List,
Literal, Literal,
Optional,
Set,
Tuple,
Type, Type,
TypeVar, TypeVar,
Union,
cast, cast,
overload, overload,
) )
@ -88,6 +96,7 @@ from sanic.worker.inspector import Inspector
from sanic.worker.loader import CertLoader from sanic.worker.loader import CertLoader
from sanic.worker.manager import WorkerManager from sanic.worker.manager import WorkerManager
if TYPE_CHECKING: if TYPE_CHECKING:
try: try:
from sanic_ext import Extend # type: ignore from sanic_ext import Extend # type: ignore
@ -164,7 +173,7 @@ class Sanic(
"websocket_tasks", "websocket_tasks",
) )
_app_registry: ClassVar[dict[str, Sanic]] = {} _app_registry: ClassVar[Dict[str, "Sanic"]] = {}
test_mode: ClassVar[bool] = False test_mode: ClassVar[bool] = False
@overload @overload
@ -173,19 +182,19 @@ class Sanic(
name: str, name: str,
config: None = None, config: None = None,
ctx: None = None, ctx: None = None,
router: Router | None = None, router: Optional[Router] = None,
signal_router: SignalRouter | None = None, signal_router: Optional[SignalRouter] = None,
error_handler: ErrorHandler | None = None, error_handler: Optional[ErrorHandler] = None,
env_prefix: str | None = SANIC_PREFIX, env_prefix: Optional[str] = SANIC_PREFIX,
request_class: type[Request] | None = None, request_class: Optional[Type[Request]] = None,
strict_slashes: bool = False, strict_slashes: bool = False,
log_config: dict[str, Any] | None = None, log_config: Optional[Dict[str, Any]] = None,
configure_logging: bool = True, configure_logging: bool = True,
dumps: Callable[..., AnyStr] | None = None, dumps: Optional[Callable[..., AnyStr]] = None,
loads: Callable[..., Any] | None = None, loads: Optional[Callable[..., Any]] = None,
inspector: bool = False, inspector: bool = False,
inspector_class: type[Inspector] | None = None, inspector_class: Optional[Type[Inspector]] = None,
certloader_class: type[CertLoader] | None = None, certloader_class: Optional[Type[CertLoader]] = None,
) -> None: ) -> None:
... ...
@ -193,21 +202,21 @@ class Sanic(
def __init__( def __init__(
self: Sanic[config_type, SimpleNamespace], self: Sanic[config_type, SimpleNamespace],
name: str, name: str,
config: config_type | None = None, config: Optional[config_type] = None,
ctx: None = None, ctx: None = None,
router: Router | None = None, router: Optional[Router] = None,
signal_router: SignalRouter | None = None, signal_router: Optional[SignalRouter] = None,
error_handler: ErrorHandler | None = None, error_handler: Optional[ErrorHandler] = None,
env_prefix: str | None = SANIC_PREFIX, env_prefix: Optional[str] = SANIC_PREFIX,
request_class: type[Request] | None = None, request_class: Optional[Type[Request]] = None,
strict_slashes: bool = False, strict_slashes: bool = False,
log_config: dict[str, Any] | None = None, log_config: Optional[Dict[str, Any]] = None,
configure_logging: bool = True, configure_logging: bool = True,
dumps: Callable[..., AnyStr] | None = None, dumps: Optional[Callable[..., AnyStr]] = None,
loads: Callable[..., Any] | None = None, loads: Optional[Callable[..., Any]] = None,
inspector: bool = False, inspector: bool = False,
inspector_class: type[Inspector] | None = None, inspector_class: Optional[Type[Inspector]] = None,
certloader_class: type[CertLoader] | None = None, certloader_class: Optional[Type[CertLoader]] = None,
) -> None: ) -> None:
... ...
@ -216,20 +225,20 @@ class Sanic(
self: Sanic[Config, ctx_type], self: Sanic[Config, ctx_type],
name: str, name: str,
config: None = None, config: None = None,
ctx: ctx_type | None = None, ctx: Optional[ctx_type] = None,
router: Router | None = None, router: Optional[Router] = None,
signal_router: SignalRouter | None = None, signal_router: Optional[SignalRouter] = None,
error_handler: ErrorHandler | None = None, error_handler: Optional[ErrorHandler] = None,
env_prefix: str | None = SANIC_PREFIX, env_prefix: Optional[str] = SANIC_PREFIX,
request_class: type[Request] | None = None, request_class: Optional[Type[Request]] = None,
strict_slashes: bool = False, strict_slashes: bool = False,
log_config: dict[str, Any] | None = None, log_config: Optional[Dict[str, Any]] = None,
configure_logging: bool = True, configure_logging: bool = True,
dumps: Callable[..., AnyStr] | None = None, dumps: Optional[Callable[..., AnyStr]] = None,
loads: Callable[..., Any] | None = None, loads: Optional[Callable[..., Any]] = None,
inspector: bool = False, inspector: bool = False,
inspector_class: type[Inspector] | None = None, inspector_class: Optional[Type[Inspector]] = None,
certloader_class: type[CertLoader] | None = None, certloader_class: Optional[Type[CertLoader]] = None,
) -> None: ) -> None:
... ...
@ -237,42 +246,42 @@ class Sanic(
def __init__( def __init__(
self: Sanic[config_type, ctx_type], self: Sanic[config_type, ctx_type],
name: str, name: str,
config: config_type | None = None, config: Optional[config_type] = None,
ctx: ctx_type | None = None, ctx: Optional[ctx_type] = None,
router: Router | None = None, router: Optional[Router] = None,
signal_router: SignalRouter | None = None, signal_router: Optional[SignalRouter] = None,
error_handler: ErrorHandler | None = None, error_handler: Optional[ErrorHandler] = None,
env_prefix: str | None = SANIC_PREFIX, env_prefix: Optional[str] = SANIC_PREFIX,
request_class: type[Request] | None = None, request_class: Optional[Type[Request]] = None,
strict_slashes: bool = False, strict_slashes: bool = False,
log_config: dict[str, Any] | None = None, log_config: Optional[Dict[str, Any]] = None,
configure_logging: bool = True, configure_logging: bool = True,
dumps: Callable[..., AnyStr] | None = None, dumps: Optional[Callable[..., AnyStr]] = None,
loads: Callable[..., Any] | None = None, loads: Optional[Callable[..., Any]] = None,
inspector: bool = False, inspector: bool = False,
inspector_class: type[Inspector] | None = None, inspector_class: Optional[Type[Inspector]] = None,
certloader_class: type[CertLoader] | None = None, certloader_class: Optional[Type[CertLoader]] = None,
) -> None: ) -> None:
... ...
def __init__( def __init__(
self, self,
name: str, name: str,
config: config_type | None = None, config: Optional[config_type] = None,
ctx: ctx_type | None = None, ctx: Optional[ctx_type] = None,
router: Router | None = None, router: Optional[Router] = None,
signal_router: SignalRouter | None = None, signal_router: Optional[SignalRouter] = None,
error_handler: ErrorHandler | None = None, error_handler: Optional[ErrorHandler] = None,
env_prefix: str | None = SANIC_PREFIX, env_prefix: Optional[str] = SANIC_PREFIX,
request_class: type[Request] | None = None, request_class: Optional[Type[Request]] = None,
strict_slashes: bool = False, strict_slashes: bool = False,
log_config: dict[str, Any] | None = None, log_config: Optional[Dict[str, Any]] = None,
configure_logging: bool = True, configure_logging: bool = True,
dumps: Callable[..., AnyStr] | None = None, dumps: Optional[Callable[..., AnyStr]] = None,
loads: Callable[..., Any] | None = None, loads: Optional[Callable[..., Any]] = None,
inspector: bool = False, inspector: bool = False,
inspector_class: type[Inspector] | None = None, inspector_class: Optional[Type[Inspector]] = None,
certloader_class: type[CertLoader] | None = None, certloader_class: Optional[Type[CertLoader]] = None,
) -> None: ) -> None:
super().__init__(name=name) super().__init__(name=name)
# logging # logging
@ -294,39 +303,41 @@ class Sanic(
self.config.INSPECTOR = inspector self.config.INSPECTOR = inspector
# Then we can do the rest # Then we can do the rest
self._asgi_app: ASGIApp | None = None self._asgi_app: Optional[ASGIApp] = None
self._asgi_lifespan: Lifespan | None = None self._asgi_lifespan: Optional[Lifespan] = None
self._asgi_client: Any = None self._asgi_client: Any = None
self._blueprint_order: list[Blueprint] = [] self._blueprint_order: List[Blueprint] = []
self._delayed_tasks: list[str] = [] self._delayed_tasks: List[str] = []
self._future_registry: FutureRegistry = FutureRegistry() self._future_registry: FutureRegistry = FutureRegistry()
self._inspector: Inspector | None = None self._inspector: Optional[Inspector] = None
self._manager: WorkerManager | None = None self._manager: Optional[WorkerManager] = None
self._state: ApplicationState = ApplicationState(app=self) self._state: ApplicationState = ApplicationState(app=self)
self._task_registry: dict[str, Task | None] = {} self._task_registry: Dict[str, Union[Task, None]] = {}
self._test_client: Any = None self._test_client: Any = None
self._test_manager: Any = None self._test_manager: Any = None
self.asgi = False self.asgi = False
self.auto_reload = False self.auto_reload = False
self.blueprints: dict[str, Blueprint] = {} self.blueprints: Dict[str, Blueprint] = {}
self.certloader_class: type[CertLoader] = certloader_class or CertLoader self.certloader_class: Type[CertLoader] = (
certloader_class or CertLoader
)
self.configure_logging: bool = configure_logging self.configure_logging: bool = configure_logging
self.ctx: ctx_type = cast(ctx_type, ctx or SimpleNamespace()) self.ctx: ctx_type = cast(ctx_type, ctx or SimpleNamespace())
self.error_handler: ErrorHandler = error_handler or ErrorHandler() self.error_handler: ErrorHandler = error_handler or ErrorHandler()
self.inspector_class: type[Inspector] = inspector_class or Inspector self.inspector_class: Type[Inspector] = inspector_class or Inspector
self.listeners: dict[str, list[ListenerType[Any]]] = defaultdict(list) self.listeners: Dict[str, List[ListenerType[Any]]] = defaultdict(list)
self.named_request_middleware: dict[str, deque[Middleware]] = {} self.named_request_middleware: Dict[str, Deque[Middleware]] = {}
self.named_response_middleware: dict[str, deque[Middleware]] = {} self.named_response_middleware: Dict[str, Deque[Middleware]] = {}
self.request_class: type[Request] = request_class or Request self.request_class: Type[Request] = request_class or Request
self.request_middleware: deque[Middleware] = deque() self.request_middleware: Deque[Middleware] = deque()
self.response_middleware: deque[Middleware] = deque() self.response_middleware: Deque[Middleware] = deque()
self.router: Router = router or Router() self.router: Router = router or Router()
self.shared_ctx: SharedContext = SharedContext() self.shared_ctx: SharedContext = SharedContext()
self.signal_router: SignalRouter = signal_router or SignalRouter() self.signal_router: SignalRouter = signal_router or SignalRouter()
self.sock: socket | None = None self.sock: Optional[socket] = None
self.strict_slashes: bool = strict_slashes self.strict_slashes: bool = strict_slashes
self.websocket_enabled: bool = False self.websocket_enabled: bool = False
self.websocket_tasks: set[Future[Any]] = set() self.websocket_tasks: Set[Future[Any]] = set()
# Register alternative method names # Register alternative method names
self.go_fast = self.run self.go_fast = self.run
@ -385,11 +396,15 @@ class Sanic(
try: try:
_event = ListenerEvent[event.upper()] _event = ListenerEvent[event.upper()]
except (ValueError, AttributeError): except (ValueError, AttributeError):
valid = ", ".join(x.lower() for x in ListenerEvent.__members__.keys()) valid = ", ".join(
map(lambda x: x.lower(), ListenerEvent.__members__.keys())
)
raise BadRequest(f"Invalid event: {event}. Use one of: {valid}") raise BadRequest(f"Invalid event: {event}. Use one of: {valid}")
if "." in _event: if "." in _event:
self.signal(_event.value)(partial(self._listener, listener=listener)) self.signal(_event.value)(
partial(self._listener, listener=listener)
)
else: else:
self.listeners[_event.value].append(listener) self.listeners[_event.value].append(listener)
@ -397,11 +412,11 @@ class Sanic(
def register_middleware( def register_middleware(
self, self,
middleware: MiddlewareType | Middleware, middleware: Union[MiddlewareType, Middleware],
attach_to: str = "request", attach_to: str = "request",
*, *,
priority: Default | int = _default, priority: Union[Default, int] = _default,
) -> MiddlewareType | Middleware: ) -> Union[MiddlewareType, Middleware]:
"""Register a middleware to be called before a request is handled. """Register a middleware to be called before a request is handled.
Args: Args:
@ -446,7 +461,7 @@ class Sanic(
route_names: Iterable[str], route_names: Iterable[str],
attach_to: str = "request", attach_to: str = "request",
*, *,
priority: Default | int = _default, priority: Union[Default, int] = _default,
): ):
"""Used to register named middleqare (middleware typically on blueprints) """Used to register named middleqare (middleware typically on blueprints)
@ -497,7 +512,7 @@ class Sanic(
def _apply_exception_handler( def _apply_exception_handler(
self, self,
handler: FutureException, handler: FutureException,
route_names: list[str] | None = None, route_names: Optional[List[str]] = None,
): ):
"""Decorate a function to be registered as a handler for exceptions """Decorate a function to be registered as a handler for exceptions
@ -516,7 +531,9 @@ class Sanic(
def _apply_listener(self, listener: FutureListener): def _apply_listener(self, listener: FutureListener):
return self.register_listener(listener.listener, listener.event) return self.register_listener(listener.listener, listener.event)
def _apply_route(self, route: FutureRoute, overwrite: bool = False) -> list[Route]: def _apply_route(
self, route: FutureRoute, overwrite: bool = False
) -> List[Route]:
params = route._asdict() params = route._asdict()
params["overwrite"] = overwrite params["overwrite"] = overwrite
websocket = params.pop("websocket", False) websocket = params.pop("websocket", False)
@ -550,7 +567,7 @@ class Sanic(
def _apply_middleware( def _apply_middleware(
self, self,
middleware: FutureMiddleware, middleware: FutureMiddleware,
route_names: list[str] | None = None, route_names: Optional[List[str]] = None,
): ):
with self.amend(): with self.amend():
if route_names: if route_names:
@ -571,8 +588,8 @@ class Sanic(
self, self,
event: str, event: str,
*, *,
condition: dict[str, str] | None = None, condition: Optional[Dict[str, str]] = None,
context: dict[str, Any] | None = None, context: Optional[Dict[str, Any]] = None,
fail_not_found: bool = True, fail_not_found: bool = True,
inline: Literal[True], inline: Literal[True],
reverse: bool = False, reverse: bool = False,
@ -584,8 +601,8 @@ class Sanic(
self, self,
event: str, event: str,
*, *,
condition: dict[str, str] | None = None, condition: Optional[Dict[str, str]] = None,
context: dict[str, Any] | None = None, context: Optional[Dict[str, Any]] = None,
fail_not_found: bool = True, fail_not_found: bool = True,
inline: Literal[False] = False, inline: Literal[False] = False,
reverse: bool = False, reverse: bool = False,
@ -596,12 +613,12 @@ class Sanic(
self, self,
event: str, event: str,
*, *,
condition: dict[str, str] | None = None, condition: Optional[Dict[str, str]] = None,
context: dict[str, Any] | None = None, context: Optional[Dict[str, Any]] = None,
fail_not_found: bool = True, fail_not_found: bool = True,
inline: bool = False, inline: bool = False,
reverse: bool = False, reverse: bool = False,
) -> Coroutine[Any, Any, Awaitable[Task | Any]]: ) -> Coroutine[Any, Any, Awaitable[Union[Task, Any]]]:
"""Dispatches an event to the signal router. """Dispatches an event to the signal router.
Args: Args:
@ -645,7 +662,9 @@ class Sanic(
fail_not_found=fail_not_found, fail_not_found=fail_not_found,
) )
async def event(self, event: str, timeout: int | float | None = None) -> None: async def event(
self, event: str, timeout: Optional[Union[int, float]] = None
) -> None:
"""Wait for a specific event to be triggered. """Wait for a specific event to be triggered.
This method waits for a named event to be triggered and can be used This method waits for a named event to be triggered and can be used
@ -730,7 +749,9 @@ class Sanic(
async def report(exception: Exception) -> None: async def report(exception: Exception) -> None:
await handler(self, exception) await handler(self, exception)
self.add_signal(handler=report, event=Event.SERVER_EXCEPTION_REPORT.value) self.add_signal(
handler=report, event=Event.SERVER_EXCEPTION_REPORT.value
)
return report return report
@ -759,13 +780,13 @@ class Sanic(
def blueprint( def blueprint(
self, self,
blueprint: Blueprint | (Iterable[Blueprint] | BlueprintGroup), blueprint: Union[Blueprint, Iterable[Blueprint], BlueprintGroup],
*, *,
url_prefix: str | None = None, url_prefix: Optional[str] = None,
version: int | float | str | None = None, version: Optional[Union[int, float, str]] = None,
strict_slashes: bool | None = None, strict_slashes: Optional[bool] = None,
version_prefix: str | None = None, version_prefix: Optional[str] = None,
name_prefix: str | None = None, name_prefix: Optional[str] = None,
) -> None: ) -> None:
"""Register a blueprint on the application. """Register a blueprint on the application.
@ -791,7 +812,7 @@ class Sanic(
app.blueprint(bp, url_prefix='/blueprint') app.blueprint(bp, url_prefix='/blueprint')
``` ```
""" # noqa: E501 """ # noqa: E501
options: dict[str, Any] = {} options: Dict[str, Any] = {}
if url_prefix is not None: if url_prefix is not None:
options["url_prefix"] = url_prefix options["url_prefix"] = url_prefix
if version is not None: if version is not None:
@ -804,7 +825,7 @@ class Sanic(
options["name_prefix"] = name_prefix options["name_prefix"] = name_prefix
if isinstance(blueprint, (Iterable, BlueprintGroup)): if isinstance(blueprint, (Iterable, BlueprintGroup)):
for item in blueprint: for item in blueprint:
params: dict[str, Any] = {**options} params: Dict[str, Any] = {**options}
if isinstance(blueprint, BlueprintGroup): if isinstance(blueprint, BlueprintGroup):
merge_from = [ merge_from = [
options.get("url_prefix", ""), options.get("url_prefix", ""),
@ -819,12 +840,14 @@ class Sanic(
for _attr in ["version", "strict_slashes"]: for _attr in ["version", "strict_slashes"]:
if getattr(item, _attr) is None: if getattr(item, _attr) is None:
params[_attr] = getattr(blueprint, _attr) or options.get( params[_attr] = getattr(
_attr blueprint, _attr
) ) or options.get(_attr)
if item.version_prefix == "/v": if item.version_prefix == "/v":
if blueprint.version_prefix == "/v": if blueprint.version_prefix == "/v":
params["version_prefix"] = options.get("version_prefix") params["version_prefix"] = options.get(
"version_prefix"
)
else: else:
params["version_prefix"] = blueprint.version_prefix params["version_prefix"] = blueprint.version_prefix
name_prefix = getattr(blueprint, "name_prefix", None) name_prefix = getattr(blueprint, "name_prefix", None)
@ -834,14 +857,17 @@ class Sanic(
return return
if blueprint.name in self.blueprints: if blueprint.name in self.blueprints:
assert self.blueprints[blueprint.name] is blueprint, ( assert self.blueprints[blueprint.name] is blueprint, (
f'A blueprint with the name "{blueprint.name}" is already registered. ' 'A blueprint with the name "%s" is already registered. '
"Blueprint names must be unique." "Blueprint names must be unique." % (blueprint.name,)
) )
else: else:
self.blueprints[blueprint.name] = blueprint self.blueprints[blueprint.name] = blueprint
self._blueprint_order.append(blueprint) self._blueprint_order.append(blueprint)
if self.strict_slashes is not None and blueprint.strict_slashes is None: if (
self.strict_slashes is not None
and blueprint.strict_slashes is None
):
blueprint.strict_slashes = self.strict_slashes blueprint.strict_slashes = self.strict_slashes
blueprint.register(self, options) blueprint.register(self, options)
@ -897,7 +923,7 @@ class Sanic(
# http://subdomain.example.com/view-name # http://subdomain.example.com/view-name
""" # noqa: E501 """ # noqa: E501
# find the route by the supplied view name # find the route by the supplied view name
kw: dict[str, str] = {} kw: Dict[str, str] = {}
# special static files url_for # special static files url_for
if "." not in view_name: if "." not in view_name:
@ -911,7 +937,9 @@ class Sanic(
route = self.router.find_route_by_view_name(view_name, **kw) route = self.router.find_route_by_view_name(view_name, **kw)
if not route: if not route:
raise URLBuildError(f"Endpoint with name `{view_name}` was not found") raise URLBuildError(
f"Endpoint with name `{view_name}` was not found"
)
uri = route.path uri = route.path
@ -950,7 +978,9 @@ class Sanic(
scheme = kwargs.pop("_scheme", "") scheme = kwargs.pop("_scheme", "")
if route.extra.hosts and external: if route.extra.hosts and external:
if not host and len(route.extra.hosts) > 1: if not host and len(route.extra.hosts) > 1:
raise ValueError(f"Host is ambiguous: {', '.join(route.extra.hosts)}") raise ValueError(
f"Host is ambiguous: {', '.join(route.extra.hosts)}"
)
elif host and host not in route.extra.hosts: elif host and host not in route.extra.hosts:
raise ValueError( raise ValueError(
f"Requested host ({host}) is not available for this " f"Requested host ({host}) is not available for this "
@ -1066,7 +1096,10 @@ class Sanic(
context={"request": request, "exception": exception}, context={"request": request, "exception": exception},
) )
if request.stream is not None and request.stream.stage is not Stage.HANDLER: if (
request.stream is not None
and request.stream.stage is not Stage.HANDLER
):
error_logger.exception(exception, exc_info=True) error_logger.exception(exception, exc_info=True)
logger.error( logger.error(
"The error response will not be sent to the client for " "The error response will not be sent to the client for "
@ -1113,7 +1146,10 @@ class Sanic(
response = self.error_handler.default(request, e) response = self.error_handler.default(request, e)
elif self.debug: elif self.debug:
response = HTTPResponse( response = HTTPResponse(
(f"Error while handling error: {e}\n" f"Stack: {format_exc()}"), (
f"Error while handling error: {e}\n"
f"Stack: {format_exc()}"
),
status=500, status=500,
) )
else: else:
@ -1158,7 +1194,9 @@ class Sanic(
) )
await response.eof() await response.eof()
else: else:
raise ServerError(f"Invalid response type {response!r} (need HTTPResponse)") raise ServerError(
f"Invalid response type {response!r} (need HTTPResponse)"
)
async def handle_request(self, request: Request) -> None: # no cov async def handle_request(self, request: Request) -> None: # no cov
"""Handles a request by dispatching it to the appropriate handler. """Handles a request by dispatching it to the appropriate handler.
@ -1183,11 +1221,13 @@ class Sanic(
# Define `response` var here to remove warnings about # Define `response` var here to remove warnings about
# allocation before assignment below. # allocation before assignment below.
response: ( response: Optional[
BaseHTTPResponse Union[
| (Coroutine[Any, Any, BaseHTTPResponse | None] | ResponseStream) BaseHTTPResponse,
| None Coroutine[Any, Any, Optional[BaseHTTPResponse]],
) = None ResponseStream,
]
] = None
run_middleware = True run_middleware = True
try: try:
await self.dispatch( await self.dispatch(
@ -1245,8 +1285,10 @@ class Sanic(
if handler is None: if handler is None:
raise ServerError( raise ServerError(
"'None' was returned while requesting a " (
"handler from the router" "'None' was returned while requesting a "
"handler from the router"
)
) )
# Run response handler # Run response handler
@ -1305,14 +1347,17 @@ class Sanic(
else: else:
if not hasattr(handler, "is_websocket"): if not hasattr(handler, "is_websocket"):
raise ServerError( raise ServerError(
f"Invalid response type {response!r} " "(need HTTPResponse)" f"Invalid response type {response!r} "
"(need HTTPResponse)"
) )
except CancelledError: # type: ignore except CancelledError: # type: ignore
raise raise
except Exception as e: except Exception as e:
# Response Generation Failed # Response Generation Failed
await self.handle_exception(request, e, run_middleware=run_middleware) await self.handle_exception(
request, e, run_middleware=run_middleware
)
async def _websocket_handler( async def _websocket_handler(
self, handler, request, *args, subprotocols=None, **kwargs self, handler, request, *args, subprotocols=None, **kwargs
@ -1391,7 +1436,9 @@ class Sanic(
# Execution # Execution
# -------------------------------------------------------------------- # # -------------------------------------------------------------------- #
async def _run_request_middleware(self, request, middleware_collection): # no cov async def _run_request_middleware(
self, request, middleware_collection
): # no cov
request._request_middleware_started = True request._request_middleware_started = True
for middleware in middleware_collection: for middleware in middleware_collection:
@ -1468,7 +1515,9 @@ class Sanic(
task.cancel() task.cancel()
@staticmethod @staticmethod
async def _listener(app: Sanic, loop: AbstractEventLoop, listener: ListenerType): async def _listener(
app: Sanic, loop: AbstractEventLoop, listener: ListenerType
):
try: try:
maybe_coro = listener(app) # type: ignore maybe_coro = listener(app) # type: ignore
except TypeError: except TypeError:
@ -1497,7 +1546,9 @@ class Sanic(
if isawaitable(task): if isawaitable(task):
await task await task
except CancelledError: except CancelledError:
error_logger.warning(f"Task {task} was cancelled before it completed.") error_logger.warning(
f"Task {task} was cancelled before it completed."
)
raise raise
except Exception as e: except Exception as e:
await app.dispatch( await app.dispatch(
@ -1515,7 +1566,7 @@ class Sanic(
app, app,
loop, loop,
*, *,
name: str | None = None, name: Optional[str] = None,
register: bool = True, register: bool = True,
) -> Task: ) -> Task:
if not isinstance(task, Future): if not isinstance(task, Future):
@ -1577,11 +1628,11 @@ class Sanic(
def add_task( def add_task(
self, self,
task: Future[Any] | (Coroutine[Any, Any, Any] | Awaitable[Any]), task: Union[Future[Any], Coroutine[Any, Any, Any], Awaitable[Any]],
*, *,
name: str | None = None, name: Optional[str] = None,
register: bool = True, register: bool = True,
) -> Task[Any] | None: ) -> Optional[Task[Any]]:
"""Schedule a task to run later, after the loop has started. """Schedule a task to run later, after the loop has started.
While this is somewhat similar to `asyncio.create_task`, it can be While this is somewhat similar to `asyncio.create_task`, it can be
@ -1606,14 +1657,18 @@ class Sanic(
""" # noqa: E501 """ # noqa: E501
try: try:
loop = self.loop # Will raise SanicError if loop is not started loop = self.loop # Will raise SanicError if loop is not started
return self._loop_add_task(task, self, loop, name=name, register=register) return self._loop_add_task(
task, self, loop, name=name, register=register
)
except SanicException: except SanicException:
task_name = f"sanic.delayed_task.{hash(task)}" task_name = f"sanic.delayed_task.{hash(task)}"
if not self._delayed_tasks: if not self._delayed_tasks:
self.after_server_start(partial(self.dispatch_delayed_tasks)) self.after_server_start(partial(self.dispatch_delayed_tasks))
if name: if name:
raise RuntimeError("Cannot name task outside of a running application") raise RuntimeError(
"Cannot name task outside of a running application"
)
self.signal(task_name)(partial(self.run_delayed_task, task=task)) self.signal(task_name)(partial(self.run_delayed_task, task=task))
self._delayed_tasks.append(task_name) self._delayed_tasks.append(task_name)
@ -1624,14 +1679,18 @@ class Sanic(
... ...
@overload @overload
def get_task(self, name: str, *, raise_exception: Literal[False]) -> Task | None: def get_task(
self, name: str, *, raise_exception: Literal[False]
) -> Optional[Task]:
... ...
@overload @overload
def get_task(self, name: str, *, raise_exception: bool) -> Task | None: def get_task(self, name: str, *, raise_exception: bool) -> Optional[Task]:
... ...
def get_task(self, name: str, *, raise_exception: bool = True) -> Task | None: def get_task(
self, name: str, *, raise_exception: bool = True
) -> Optional[Task]:
"""Get a named task. """Get a named task.
This method is used to get a task by its name. Optionally, you can This method is used to get a task by its name. Optionally, you can
@ -1649,13 +1708,15 @@ class Sanic(
return self._task_registry[name] return self._task_registry[name]
except KeyError: except KeyError:
if raise_exception: if raise_exception:
raise SanicException(f'Registered task named "{name}" not found.') raise SanicException(
f'Registered task named "{name}" not found.'
)
return None return None
async def cancel_task( async def cancel_task(
self, self,
name: str, name: str,
msg: str | None = None, msg: Optional[str] = None,
*, *,
raise_exception: bool = True, raise_exception: bool = True,
) -> None: ) -> None:
@ -1690,7 +1751,7 @@ class Sanic(
""" # noqa: E501 """ # noqa: E501
task = self.get_task(name, raise_exception=raise_exception) task = self.get_task(name, raise_exception=raise_exception)
if task and not task.cancelled(): if task and not task.cancelled():
args: tuple[str, ...] = () args: Tuple[str, ...] = ()
if msg: if msg:
if sys.version_info >= (3, 9): if sys.version_info >= (3, 9):
args = (msg,) args = (msg,)
@ -1723,7 +1784,7 @@ class Sanic(
} }
def shutdown_tasks( def shutdown_tasks(
self, timeout: float | None = None, increment: float = 0.1 self, timeout: Optional[float] = None, increment: float = 0.1
) -> None: ) -> None:
"""Cancel all tasks except the server task. """Cancel all tasks except the server task.
@ -1761,7 +1822,11 @@ class Sanic(
Iterable[Task[Any]]: The tasks that are currently registered with Iterable[Task[Any]]: The tasks that are currently registered with
the application. the application.
""" """
return (task for task in iter(self._task_registry.values()) if task is not None) return (
task
for task in iter(self._task_registry.values())
if task is not None
)
# -------------------------------------------------------------------- # # -------------------------------------------------------------------- #
# ASGI # ASGI
@ -1788,7 +1853,7 @@ class Sanic(
# Configuration # Configuration
# -------------------------------------------------------------------- # # -------------------------------------------------------------------- #
def update_config(self, config: Any) -> None: def update_config(self, config: Union[bytes, str, dict, Any]) -> None:
"""Update the application configuration. """Update the application configuration.
This method is used to update the application configuration. It can This method is used to update the application configuration. It can
@ -1798,7 +1863,7 @@ class Sanic(
See [Configuration](/en/guide/deployment/configuration) for details. See [Configuration](/en/guide/deployment/configuration) for details.
Args: Args:
config (bytes | str | dict | Any): The configuration object, config (Union[bytes, str, dict, Any]): The configuration object,
dictionary, or path to a configuration file. dictionary, or path to a configuration file.
""" """
@ -1838,7 +1903,7 @@ class Sanic(
return self._state return self._state
@property @property
def reload_dirs(self) -> set[Path]: def reload_dirs(self) -> Set[Path]:
"""The directories that are monitored for auto-reload. """The directories that are monitored for auto-reload.
Returns: Returns:
@ -1883,9 +1948,9 @@ class Sanic(
def extend( def extend(
self, self,
*, *,
extensions: list[type[Extension]] | None = None, extensions: Optional[List[Type[Extension]]] = None,
built_in_extensions: bool = True, built_in_extensions: bool = True,
config: Config | dict[str, Any] | None = None, config: Optional[Union[Config, Dict[str, Any]]] = None,
**kwargs, **kwargs,
) -> Extend: ) -> Extend:
"""Extend Sanic with additional functionality using Sanic Extensions. """Extend Sanic with additional functionality using Sanic Extensions.
@ -2003,7 +2068,9 @@ class Sanic(
del cls._app_registry[name] del cls._app_registry[name]
@classmethod @classmethod
def get_app(cls, name: str | None = None, *, force_create: bool = False) -> Sanic: def get_app(
cls, name: Optional[str] = None, *, force_create: bool = False
) -> Sanic:
"""Retrieve an instantiated Sanic instance by name. """Retrieve an instantiated Sanic instance by name.
This method is best used when needing to get access to an already This method is best used when needing to get access to an already
@ -2210,7 +2277,9 @@ class Sanic(
self.finalize() self.finalize()
route_names = [route.extra.ident for route in self.router.routes] route_names = [route.extra.ident for route in self.router.routes]
duplicates = {name for name in route_names if route_names.count(name) > 1} duplicates = {
name for name in route_names if route_names.count(name) > 1
}
if duplicates: if duplicates:
names = ", ".join(duplicates) names = ", ".join(duplicates)
message = ( message = (
@ -2247,7 +2316,7 @@ class Sanic(
self, self,
concern: str, concern: str,
action: str, action: str,
loop: AbstractEventLoop | None = None, loop: Optional[AbstractEventLoop] = None,
) -> None: ) -> None:
event = f"server.{concern}.{action}" event = f"server.{concern}.{action}"
if action not in ("before", "after") or concern not in ( if action not in ("before", "after") or concern not in (
@ -2255,7 +2324,9 @@ class Sanic(
"shutdown", "shutdown",
): ):
raise SanicException(f"Invalid server event: {event}") raise SanicException(f"Invalid server event: {event}")
logger.debug(f"Triggering server events: {event}", extra={"verbosity": 1}) logger.debug(
f"Triggering server events: {event}", extra={"verbosity": 1}
)
reverse = concern == "shutdown" reverse = concern == "shutdown"
if loop is None: if loop is None:
loop = self.loop loop = self.loop
@ -2276,7 +2347,7 @@ class Sanic(
def refresh( def refresh(
self, self,
passthru: dict[str, Any] | None = None, passthru: Optional[Dict[str, Any]] = None,
) -> Sanic: ) -> Sanic:
"""Refresh the application instance. **This is used internally by Sanic**. """Refresh the application instance. **This is used internally by Sanic**.
@ -2321,7 +2392,9 @@ class Sanic(
Inspector: An instance of Inspector. Inspector: An instance of Inspector.
""" """
if environ.get("SANIC_WORKER_PROCESS") or not self._inspector: if environ.get("SANIC_WORKER_PROCESS") or not self._inspector:
raise SanicException("Can only access the inspector from the main process") raise SanicException(
"Can only access the inspector from the main process"
)
return self._inspector return self._inspector
@property @property
@ -2354,5 +2427,7 @@ class Sanic(
""" """
if environ.get("SANIC_WORKER_PROCESS") or not self._manager: if environ.get("SANIC_WORKER_PROCESS") or not self._manager:
raise SanicException("Can only access the manager from the main process") raise SanicException(
"Can only access the manager from the main process"
)
return self._manager return self._manager

View File

@ -4,6 +4,7 @@ from contextlib import suppress
from importlib import import_module from importlib import import_module
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from sanic import Sanic from sanic import Sanic

View File

@ -1,9 +1,11 @@
import re import re
import sys import sys
from os import environ from os import environ
from sanic.helpers import is_atty from sanic.helpers import is_atty
BASE_LOGO = """ BASE_LOGO = """
Sanic Sanic
@ -61,7 +63,10 @@ def get_logo(full: bool = False, coffee: bool = False) -> str:
else BASE_LOGO else BASE_LOGO
) )
if sys.platform == "darwin" and environ.get("TERM_PROGRAM") == "Apple_Terminal": if (
sys.platform == "darwin"
and environ.get("TERM_PROGRAM") == "Apple_Terminal"
):
logo = ansi_pattern.sub("", logo) logo = ansi_pattern.sub("", logo)
return logo return logo

View File

@ -79,7 +79,9 @@ class MOTDTTY(MOTD):
def set_variables(self): # no cov def set_variables(self): # no cov
"""Set the variables used for display.""" """Set the variables used for display."""
fallback = (108, 24) fallback = (108, 24)
terminal_width = max(get_terminal_size(fallback=fallback).columns, fallback[0]) terminal_width = max(
get_terminal_size(fallback=fallback).columns, fallback[0]
)
self.max_value_width = terminal_width - fallback[0] + 36 self.max_value_width = terminal_width - fallback[0] + 36
self.key_width = 4 self.key_width = 4

View File

@ -1,10 +1,12 @@
import os import os
import sys import sys
import time import time
from contextlib import contextmanager from contextlib import contextmanager
from queue import Queue from queue import Queue
from threading import Thread from threading import Thread
if os.name == "nt": # noqa if os.name == "nt": # noqa
import ctypes # noqa import ctypes # noqa
@ -45,16 +47,21 @@ class Spinner: # noqa
@staticmethod @staticmethod
def cursor(): def cursor():
while True: while True:
yield from "|/-\\" for cursor in "|/-\\":
yield cursor
@staticmethod @staticmethod
def hide(): def hide():
if os.name == "nt": if os.name == "nt":
ci = _CursorInfo() ci = _CursorInfo()
handle = ctypes.windll.kernel32.GetStdHandle(-11) handle = ctypes.windll.kernel32.GetStdHandle(-11)
ctypes.windll.kernel32.GetConsoleCursorInfo(handle, ctypes.byref(ci)) ctypes.windll.kernel32.GetConsoleCursorInfo(
handle, ctypes.byref(ci)
)
ci.visible = False ci.visible = False
ctypes.windll.kernel32.SetConsoleCursorInfo(handle, ctypes.byref(ci)) ctypes.windll.kernel32.SetConsoleCursorInfo(
handle, ctypes.byref(ci)
)
elif os.name == "posix": elif os.name == "posix":
sys.stdout.write("\033[?25l") sys.stdout.write("\033[?25l")
sys.stdout.flush() sys.stdout.flush()
@ -64,9 +71,13 @@ class Spinner: # noqa
if os.name == "nt": if os.name == "nt":
ci = _CursorInfo() ci = _CursorInfo()
handle = ctypes.windll.kernel32.GetStdHandle(-11) handle = ctypes.windll.kernel32.GetStdHandle(-11)
ctypes.windll.kernel32.GetConsoleCursorInfo(handle, ctypes.byref(ci)) ctypes.windll.kernel32.GetConsoleCursorInfo(
handle, ctypes.byref(ci)
)
ci.visible = True ci.visible = True
ctypes.windll.kernel32.SetConsoleCursorInfo(handle, ctypes.byref(ci)) ctypes.windll.kernel32.SetConsoleCursorInfo(
handle, ctypes.byref(ci)
)
elif os.name == "posix": elif os.name == "posix":
sys.stdout.write("\033[?25h") sys.stdout.write("\033[?25h")
sys.stdout.flush() sys.stdout.flush()

View File

@ -1,16 +1,18 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from socket import socket from socket import socket
from ssl import SSLContext from ssl import SSLContext
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Union
from sanic.application.constants import Mode, Server, ServerStage from sanic.application.constants import Mode, Server, ServerStage
from sanic.log import VerbosityFilter, logger from sanic.log import VerbosityFilter, logger
from sanic.server.async_server import AsyncioServer from sanic.server.async_server import AsyncioServer
if TYPE_CHECKING: if TYPE_CHECKING:
from sanic import Sanic from sanic import Sanic
@ -19,9 +21,9 @@ if TYPE_CHECKING:
class ApplicationServerInfo: class ApplicationServerInfo:
"""Information about a server instance.""" """Information about a server instance."""
settings: dict[str, Any] settings: Dict[str, Any]
stage: ServerStage = field(default=ServerStage.STOPPED) stage: ServerStage = field(default=ServerStage.STOPPED)
server: AsyncioServer | None = field(default=None) server: Optional[AsyncioServer] = field(default=None)
@dataclass @dataclass
@ -38,11 +40,11 @@ class ApplicationState:
fast: bool = field(default=False) fast: bool = field(default=False)
host: str = field(default="") host: str = field(default="")
port: int = field(default=0) port: int = field(default=0)
ssl: SSLContext | None = field(default=None) ssl: Optional[SSLContext] = field(default=None)
sock: socket | None = field(default=None) sock: Optional[socket] = field(default=None)
unix: str | None = field(default=None) unix: Optional[str] = field(default=None)
mode: Mode = field(default=Mode.PRODUCTION) mode: Mode = field(default=Mode.PRODUCTION)
reload_dirs: set[Path] = field(default_factory=set) reload_dirs: Set[Path] = field(default_factory=set)
auto_reload: bool = field(default=False) auto_reload: bool = field(default=False)
server: Server = field(default=Server.SANIC) server: Server = field(default=Server.SANIC)
is_running: bool = field(default=False) is_running: bool = field(default=False)
@ -51,7 +53,7 @@ class ApplicationState:
verbosity: int = field(default=0) verbosity: int = field(default=0)
workers: int = field(default=0) workers: int = field(default=0)
primary: bool = field(default=True) primary: bool = field(default=True)
server_info: list[ApplicationServerInfo] = field(default_factory=list) server_info: List[ApplicationServerInfo] = field(default_factory=list)
# This property relates to the ApplicationState instance and should # This property relates to the ApplicationState instance and should
# not be changed except in the __post_init__ method # not be changed except in the __post_init__ method
@ -62,12 +64,14 @@ class ApplicationState:
def __setattr__(self, name: str, value: Any) -> None: def __setattr__(self, name: str, value: Any) -> None:
if self._init and name == "_init": if self._init and name == "_init":
raise RuntimeError("Cannot change the value of _init after instantiation") raise RuntimeError(
"Cannot change the value of _init after instantiation"
)
super().__setattr__(name, value) super().__setattr__(name, value)
if self._init and hasattr(self, f"set_{name}"): if self._init and hasattr(self, f"set_{name}"):
getattr(self, f"set_{name}")(value) getattr(self, f"set_{name}")(value)
def set_mode(self, value: str | Mode): def set_mode(self, value: Union[str, Mode]):
if hasattr(self.app, "error_handler"): if hasattr(self.app, "error_handler"):
self.app.error_handler.debug = self.app.debug self.app.error_handler.debug = self.app.debug
if getattr(self.app, "configure_logging", False) and self.app.debug: if getattr(self.app, "configure_logging", False) and self.app.debug:
@ -103,7 +107,9 @@ class ApplicationState:
if all(info.stage is ServerStage.SERVING for info in self.server_info): if all(info.stage is ServerStage.SERVING for info in self.server_info):
return ServerStage.SERVING return ServerStage.SERVING
elif any(info.stage is ServerStage.SERVING for info in self.server_info): elif any(
info.stage is ServerStage.SERVING for info in self.server_info
):
return ServerStage.PARTIAL return ServerStage.PARTIAL
return ServerStage.STOPPED return ServerStage.STOPPED

View File

@ -1,7 +1,8 @@
from __future__ import annotations from __future__ import annotations
import warnings import warnings
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Optional
from sanic.compat import Header from sanic.compat import Header
from sanic.exceptions import BadRequest, ServerError from sanic.exceptions import BadRequest, ServerError
@ -14,6 +15,7 @@ from sanic.response import BaseHTTPResponse
from sanic.server import ConnInfo from sanic.server import ConnInfo
from sanic.server.websockets.connection import WebSocketConnection from sanic.server.websockets.connection import WebSocketConnection
if TYPE_CHECKING: if TYPE_CHECKING:
from sanic import Sanic from sanic import Sanic
@ -107,9 +109,9 @@ class ASGIApp:
request: Request request: Request
transport: MockTransport transport: MockTransport
lifespan: Lifespan lifespan: Lifespan
ws: WebSocketConnection | None ws: Optional[WebSocketConnection]
stage: Stage stage: Stage
response: BaseHTTPResponse | None response: Optional[BaseHTTPResponse]
@classmethod @classmethod
async def create( async def create(
@ -140,7 +142,9 @@ class ASGIApp:
] ]
) )
except UnicodeDecodeError: except UnicodeDecodeError:
raise BadRequest("Header names can only contain US-ASCII characters") raise BadRequest(
"Header names can only contain US-ASCII characters"
)
if scope["type"] == "http": if scope["type"] == "http":
version = scope["http_version"] version = scope["http_version"]
@ -149,7 +153,9 @@ class ASGIApp:
version = "1.1" version = "1.1"
method = "GET" method = "GET"
instance.ws = instance.transport.create_websocket_connection(send, receive) instance.ws = instance.transport.create_websocket_connection(
send, receive
)
else: else:
raise ServerError("Received unknown ASGI scope") raise ServerError("Received unknown ASGI scope")
@ -183,7 +189,7 @@ class ASGIApp:
return instance return instance
async def read(self) -> bytes | None: async def read(self) -> Optional[bytes]:
""" """
Read and stream the body in chunks from an incoming ASGI message. Read and stream the body in chunks from an incoming ASGI message.
""" """

View File

@ -1,4 +1,5 @@
import re import re
from typing import Any, Optional from typing import Any, Optional
from sanic.base.meta import SanicMeta from sanic.base.meta import SanicMeta
@ -10,6 +11,7 @@ from sanic.mixins.routes import RouteMixin
from sanic.mixins.signals import SignalMixin from sanic.mixins.signals import SignalMixin
from sanic.mixins.static import StaticMixin from sanic.mixins.static import StaticMixin
VALID_NAME = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_\-]*$") VALID_NAME = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_\-]*$")
@ -24,7 +26,9 @@ class BaseSanic(
): ):
__slots__ = ("name",) __slots__ = ("name",)
def __init__(self, name: Optional[str] = None, *args: Any, **kwargs: Any) -> None: def __init__(
self, name: Optional[str] = None, *args: Any, **kwargs: Any
) -> None:
class_name = self.__class__.__name__ class_name = self.__class__.__name__
if name is None: if name is None:

View File

@ -1,3 +1,4 @@
from .blueprints import BlueprintGroup from .blueprints import BlueprintGroup
__all__ = ["BlueprintGroup"] # noqa: F405 __all__ = ["BlueprintGroup"] # noqa: F405

View File

@ -2,6 +2,7 @@ from __future__ import annotations
import asyncio import asyncio
import sys import sys
from collections import defaultdict from collections import defaultdict
from collections.abc import MutableSequence from collections.abc import MutableSequence
from copy import deepcopy from copy import deepcopy
@ -13,9 +14,15 @@ from typing import (
TYPE_CHECKING, TYPE_CHECKING,
Any, Any,
Callable, Callable,
Dict,
Iterable, Iterable,
Iterator, Iterator,
List,
Optional,
Sequence, Sequence,
Set,
Tuple,
Union,
overload, overload,
) )
@ -32,6 +39,7 @@ from sanic.models.handler_types import (
RouteHandler, RouteHandler,
) )
if TYPE_CHECKING: if TYPE_CHECKING:
from sanic import Sanic from sanic import Sanic
@ -114,10 +122,10 @@ class Blueprint(BaseSanic):
def __init__( def __init__(
self, self,
name: str, name: str,
url_prefix: str | None = None, url_prefix: Optional[str] = None,
host: list[str] | str | None = None, host: Optional[Union[List[str], str]] = None,
version: int | str | float | None = None, version: Optional[Union[int, str, float]] = None,
strict_slashes: bool | None = None, strict_slashes: Optional[bool] = None,
version_prefix: str = "/v", version_prefix: str = "/v",
): ):
super().__init__(name=name) super().__init__(name=name)
@ -128,7 +136,9 @@ class Blueprint(BaseSanic):
self.host = host self.host = host
self.strict_slashes = strict_slashes self.strict_slashes = strict_slashes
self.url_prefix = ( self.url_prefix = (
url_prefix[:-1] if url_prefix and url_prefix.endswith("/") else url_prefix url_prefix[:-1]
if url_prefix and url_prefix.endswith("/")
else url_prefix
) )
self.version = version self.version = version
self.version_prefix = version_prefix self.version_prefix = version_prefix
@ -151,7 +161,7 @@ class Blueprint(BaseSanic):
return f"Blueprint({args})" return f"Blueprint({args})"
@property @property
def apps(self) -> set[Sanic]: def apps(self) -> Set[Sanic]:
"""Get the set of apps that this blueprint is registered to. """Get the set of apps that this blueprint is registered to.
Returns: Returns:
@ -162,7 +172,9 @@ class Blueprint(BaseSanic):
an app. an app.
""" """
if not self._apps: if not self._apps:
raise SanicException(f"{self} has not yet been registered to an app") raise SanicException(
f"{self} has not yet been registered to an app"
)
return self._apps return self._apps
@property @property
@ -184,23 +196,23 @@ class Blueprint(BaseSanic):
def reset(self) -> None: def reset(self) -> None:
"""Reset the blueprint to its initial state.""" """Reset the blueprint to its initial state."""
self._apps: set[Sanic] = set() self._apps: Set[Sanic] = set()
self._allow_route_overwrite = False self._allow_route_overwrite = False
self.exceptions: list[RouteHandler] = [] self.exceptions: List[RouteHandler] = []
self.listeners: dict[str, list[ListenerType[Any]]] = {} self.listeners: Dict[str, List[ListenerType[Any]]] = {}
self.middlewares: list[MiddlewareType] = [] self.middlewares: List[MiddlewareType] = []
self.routes: list[Route] = [] self.routes: List[Route] = []
self.statics: list[RouteHandler] = [] self.statics: List[RouteHandler] = []
self.websocket_routes: list[Route] = [] self.websocket_routes: List[Route] = []
def copy( def copy(
self, self,
name: str, name: str,
url_prefix: str | Default | None = _default, url_prefix: Optional[Union[str, Default]] = _default,
version: int | str | float | Default | None = _default, version: Optional[Union[int, str, float, Default]] = _default,
version_prefix: str | Default = _default, version_prefix: Union[str, Default] = _default,
allow_route_overwrite: bool | Default = _default, allow_route_overwrite: Union[bool, Default] = _default,
strict_slashes: bool | Default | None = _default, strict_slashes: Optional[Union[bool, Default]] = _default,
with_registration: bool = True, with_registration: bool = True,
with_ctx: bool = False, with_ctx: bool = False,
): ):
@ -265,12 +277,12 @@ class Blueprint(BaseSanic):
@staticmethod @staticmethod
def group( def group(
*blueprints: Blueprint | BlueprintGroup, *blueprints: Union[Blueprint, BlueprintGroup],
url_prefix: str | None = None, url_prefix: Optional[str] = None,
version: int | str | float | None = None, version: Optional[Union[int, str, float]] = None,
strict_slashes: bool | None = None, strict_slashes: Optional[bool] = None,
version_prefix: str = "/v", version_prefix: str = "/v",
name_prefix: str | None = "", name_prefix: Optional[str] = "",
) -> BlueprintGroup: ) -> BlueprintGroup:
"""Group multiple blueprints (or other blueprint groups) together. """Group multiple blueprints (or other blueprint groups) together.
@ -341,7 +353,9 @@ class Blueprint(BaseSanic):
opt_strict_slashes = options.get("strict_slashes", None) opt_strict_slashes = options.get("strict_slashes", None)
opt_version_prefix = options.get("version_prefix", self.version_prefix) opt_version_prefix = options.get("version_prefix", self.version_prefix)
opt_name_prefix = options.get("name_prefix", None) opt_name_prefix = options.get("name_prefix", None)
error_format = options.get("error_format", app.config.FALLBACK_ERROR_FORMAT) error_format = options.get(
"error_format", app.config.FALLBACK_ERROR_FORMAT
)
routes = [] routes = []
middleware = [] middleware = []
@ -367,7 +381,9 @@ class Blueprint(BaseSanic):
version_prefix = prefix version_prefix = prefix
break break
version = self._extract_value(future.version, opt_version, self.version) version = self._extract_value(
future.version, opt_version, self.version
)
strict_slashes = self._extract_value( strict_slashes = self._extract_value(
future.strict_slashes, opt_strict_slashes, self.strict_slashes future.strict_slashes, opt_strict_slashes, self.strict_slashes
) )
@ -403,16 +419,22 @@ class Blueprint(BaseSanic):
continue continue
registered.add(apply_route) registered.add(apply_route)
route = app._apply_route(apply_route, overwrite=self._allow_route_overwrite) route = app._apply_route(
apply_route, overwrite=self._allow_route_overwrite
)
# If it is a copied BP, then make sure all of the names of routes # If it is a copied BP, then make sure all of the names of routes
# matchup with the new BP name # matchup with the new BP name
if self.copied_from: if self.copied_from:
for r in route: for r in route:
r.name = r.name.replace(self.copied_from, self.name) r.name = r.name.replace(self.copied_from, self.name)
r.extra.ident = r.extra.ident.replace(self.copied_from, self.name) r.extra.ident = r.extra.ident.replace(
self.copied_from, self.name
)
operation = routes.extend if isinstance(route, list) else routes.append operation = (
routes.extend if isinstance(route, list) else routes.append
)
operation(route) operation(route)
# Static Files # Static Files
@ -457,7 +479,7 @@ class Blueprint(BaseSanic):
continue continue
future.condition.update({"__blueprint__": self.name}) future.condition.update({"__blueprint__": self.name})
# Force exclusive to be False # Force exclusive to be False
app._apply_signal((*future[:-1], False)) app._apply_signal(tuple((*future[:-1], False)))
self.routes += [route for route in routes if isinstance(route, Route)] self.routes += [route for route in routes if isinstance(route, Route)]
self.websocket_routes += [ self.websocket_routes += [
@ -490,9 +512,11 @@ class Blueprint(BaseSanic):
condition = kwargs.pop("condition", {}) condition = kwargs.pop("condition", {})
condition.update({"__blueprint__": self.name}) condition.update({"__blueprint__": self.name})
kwargs["condition"] = condition kwargs["condition"] = condition
await asyncio.gather(*[app.dispatch(*args, **kwargs) for app in self.apps]) await asyncio.gather(
*[app.dispatch(*args, **kwargs) for app in self.apps]
)
def event(self, event: str, timeout: int | float | None = None): def event(self, event: str, timeout: Optional[Union[int, float]] = None):
"""Wait for a signal event to be dispatched. """Wait for a signal event to be dispatched.
Args: Args:
@ -526,7 +550,7 @@ class Blueprint(BaseSanic):
return value return value
@staticmethod @staticmethod
def _setup_uri(base: str, prefix: str | None): def _setup_uri(base: str, prefix: Optional[str]):
uri = base uri = base
if prefix: if prefix:
uri = prefix uri = prefix
@ -539,7 +563,7 @@ class Blueprint(BaseSanic):
@staticmethod @staticmethod
def register_futures( def register_futures(
apps: set[Sanic], bp: Blueprint, futures: Sequence[tuple[Any, ...]] apps: Set[Sanic], bp: Blueprint, futures: Sequence[Tuple[Any, ...]]
): ):
"""Register futures to the apps. """Register futures to the apps.
@ -551,7 +575,7 @@ class Blueprint(BaseSanic):
""" """
for app in apps: for app in apps:
app._future_registry.update({(bp, item) for item in futures}) app._future_registry.update(set((bp, item) for item in futures))
if sys.version_info < (3, 9): if sys.version_info < (3, 9):
@ -643,13 +667,13 @@ class BlueprintGroup(bpg_base):
def __init__( def __init__(
self, self,
url_prefix: str | None = None, url_prefix: Optional[str] = None,
version: int | str | float | None = None, version: Optional[Union[int, str, float]] = None,
strict_slashes: bool | None = None, strict_slashes: Optional[bool] = None,
version_prefix: str = "/v", version_prefix: str = "/v",
name_prefix: str | None = "", name_prefix: Optional[str] = "",
): ):
self._blueprints: list[Blueprint] = [] self._blueprints: List[Blueprint] = []
self._url_prefix = url_prefix self._url_prefix = url_prefix
self._version = version self._version = version
self._version_prefix = version_prefix self._version_prefix = version_prefix
@ -657,7 +681,7 @@ class BlueprintGroup(bpg_base):
self._name_prefix = name_prefix self._name_prefix = name_prefix
@property @property
def url_prefix(self) -> int | str | float | None: def url_prefix(self) -> Optional[Union[int, str, float]]:
"""The URL prefix for the Blueprint Group. """The URL prefix for the Blueprint Group.
Returns: Returns:
@ -667,7 +691,7 @@ class BlueprintGroup(bpg_base):
return self._url_prefix return self._url_prefix
@property @property
def blueprints(self) -> list[Blueprint]: def blueprints(self) -> List[Blueprint]:
"""A list of all the available blueprints under this group. """A list of all the available blueprints under this group.
Returns: Returns:
@ -677,7 +701,7 @@ class BlueprintGroup(bpg_base):
return self._blueprints return self._blueprints
@property @property
def version(self) -> str | int | float | None: def version(self) -> Optional[Union[str, int, float]]:
"""API Version for the Blueprint Group, if any. """API Version for the Blueprint Group, if any.
Returns: Returns:
@ -686,7 +710,7 @@ class BlueprintGroup(bpg_base):
return self._version return self._version
@property @property
def strict_slashes(self) -> bool | None: def strict_slashes(self) -> Optional[bool]:
"""Whether to enforce strict slashes for the Blueprint Group. """Whether to enforce strict slashes for the Blueprint Group.
Returns: Returns:
@ -704,7 +728,7 @@ class BlueprintGroup(bpg_base):
return self._version_prefix return self._version_prefix
@property @property
def name_prefix(self) -> str | None: def name_prefix(self) -> Optional[str]:
"""Name prefix for the Blueprint Group. """Name prefix for the Blueprint Group.
This is mainly needed when blueprints are copied in order to This is mainly needed when blueprints are copied in order to
@ -731,7 +755,9 @@ class BlueprintGroup(bpg_base):
def __getitem__(self, item: slice) -> MutableSequence[Blueprint]: def __getitem__(self, item: slice) -> MutableSequence[Blueprint]:
... ...
def __getitem__(self, item: int | slice) -> Blueprint | MutableSequence[Blueprint]: def __getitem__(
self, item: Union[int, slice]
) -> Union[Blueprint, MutableSequence[Blueprint]]:
"""Get the Blueprint object at the specified index. """Get the Blueprint object at the specified index.
This method returns a blueprint inside the group specified by This method returns a blueprint inside the group specified by
@ -759,8 +785,8 @@ class BlueprintGroup(bpg_base):
def __setitem__( def __setitem__(
self, self,
index: int | slice, index: Union[int, slice],
item: Blueprint | Iterable[Blueprint], item: Union[Blueprint, Iterable[Blueprint]],
) -> None: ) -> None:
"""Set the Blueprint object at the specified index. """Set the Blueprint object at the specified index.
@ -798,7 +824,7 @@ class BlueprintGroup(bpg_base):
def __delitem__(self, index: slice) -> None: def __delitem__(self, index: slice) -> None:
... ...
def __delitem__(self, index: int | slice) -> None: def __delitem__(self, index: Union[int, slice]) -> None:
"""Delete the Blueprint object at the specified index. """Delete the Blueprint object at the specified index.
Abstract method implemented to turn the `BlueprintGroup` class Abstract method implemented to turn the `BlueprintGroup` class

View File

@ -1,6 +1,7 @@
import os import os
import shutil import shutil
import sys import sys
from argparse import Namespace from argparse import Namespace
from functools import partial from functools import partial
from textwrap import indent from textwrap import indent
@ -56,7 +57,9 @@ Or, a path to a directory to run as a simple HTTP server:
) )
self.parser._positionals.title = "Required\n========\n Positional" self.parser._positionals.title = "Required\n========\n Positional"
self.parser._optionals.title = "Optional\n========\n General" self.parser._optionals.title = "Optional\n========\n General"
self.main_process = os.environ.get("SANIC_RELOADER_PROCESS", "") != "true" self.main_process = (
os.environ.get("SANIC_RELOADER_PROCESS", "") != "true"
)
self.args: Namespace = Namespace() self.args: Namespace = Namespace()
self.groups: List[Group] = [] self.groups: List[Group] = []
self.inspecting = False self.inspecting = False
@ -124,7 +127,11 @@ Or, a path to a directory to run as a simple HTTP server:
key = key.lstrip("-") key = key.lstrip("-")
except ValueError: except ValueError:
value = False if arg.startswith("--no-") else True value = False if arg.startswith("--no-") else True
key = arg.replace("--no-", "").lstrip("-").replace("-", "_") key = (
arg.replace("--no-", "")
.lstrip("-")
.replace("-", "_")
)
setattr(self.args, key, value) setattr(self.args, key, value)
kwargs = {**self.args.__dict__} kwargs = {**self.args.__dict__}
@ -174,7 +181,8 @@ Or, a path to a directory to run as a simple HTTP server:
" Example Module: project.sanic_server.app" " Example Module: project.sanic_server.app"
) )
error_logger.error( error_logger.error(
"\nThe error below might have caused the above one:\n" f"{e.msg}" "\nThe error below might have caused the above one:\n"
f"{e.msg}"
) )
sys.exit(1) sys.exit(1)
else: else:
@ -188,7 +196,7 @@ Or, a path to a directory to run as a simple HTTP server:
if self.args.tlshost: if self.args.tlshost:
ssl.append(None) ssl.append(None)
if self.args.cert is not None or self.args.key is not None: if self.args.cert is not None or self.args.key is not None:
ssl.append({"cert": self.args.cert, "key": self.args.key}) ssl.append(dict(cert=self.args.cert, key=self.args.key))
if self.args.tls: if self.args.tls:
ssl += self.args.tls ssl += self.args.tls
if not ssl: if not ssl:

View File

@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
from argparse import ArgumentParser, _ArgumentGroup from argparse import ArgumentParser, _ArgumentGroup
from typing import List, Optional, Type, Union
from sanic_routing import __version__ as __routing_version__ from sanic_routing import __version__ as __routing_version__
@ -9,14 +10,14 @@ from sanic.http.constants import HTTP
class Group: class Group:
name: str | None name: Optional[str]
container: ArgumentParser | _ArgumentGroup container: Union[ArgumentParser, _ArgumentGroup]
_registry: list[type[Group]] = [] _registry: List[Type[Group]] = []
def __init_subclass__(cls) -> None: def __init_subclass__(cls) -> None:
Group._registry.append(cls) Group._registry.append(cls)
def __init__(self, parser: ArgumentParser, title: str | None): def __init__(self, parser: ArgumentParser, title: Optional[str]):
self.parser = parser self.parser = parser
if title: if title:
@ -244,7 +245,10 @@ class DevelopmentGroup(Group):
"--auto-reload", "--auto-reload",
dest="auto_reload", dest="auto_reload",
action="store_true", action="store_true",
help=("Watch source directory for file changes and reload on " "changes"), help=(
"Watch source directory for file changes and reload on "
"changes"
),
) )
self.container.add_argument( self.container.add_argument(
"-R", "-R",

View File

@ -1,9 +1,10 @@
from __future__ import annotations from __future__ import annotations
import sys import sys
from http.client import RemoteDisconnected from http.client import RemoteDisconnected
from textwrap import indent from textwrap import indent
from typing import Any from typing import Any, Dict, Optional
from urllib.error import URLError from urllib.error import URLError
from urllib.request import Request as URequest from urllib.request import Request as URequest
from urllib.request import urlopen from urllib.request import urlopen
@ -12,6 +13,7 @@ from sanic.application.logo import get_logo
from sanic.application.motd import MOTDTTY from sanic.application.motd import MOTDTTY
from sanic.log import Colors from sanic.log import Colors
try: # no cov try: # no cov
from ujson import dumps, loads from ujson import dumps, loads
except ModuleNotFoundError: # no cov except ModuleNotFoundError: # no cov
@ -25,7 +27,7 @@ class InspectorClient:
port: int, port: int,
secure: bool, secure: bool,
raw: bool, raw: bool,
api_key: str | None, api_key: Optional[str],
) -> None: ) -> None:
self.scheme = "https" if secure else "http" self.scheme = "https" if secure else "http"
self.host = host self.host = host
@ -45,7 +47,11 @@ class InspectorClient:
return return
result = self.request(action, **kwargs).get("result") result = self.request(action, **kwargs).get("result")
if result: if result:
out = dumps(result) if isinstance(result, (list, dict)) else str(result) out = (
dumps(result)
if isinstance(result, (list, dict))
else str(result)
)
sys.stdout.write(out + "\n") sys.stdout.write(out + "\n")
def info(self) -> None: def info(self) -> None:
@ -83,7 +89,7 @@ class InspectorClient:
def request(self, action: str, method: str = "POST", **kwargs: Any) -> Any: def request(self, action: str, method: str = "POST", **kwargs: Any) -> Any:
url = f"{self.base_url}/{action}" url = f"{self.base_url}/{action}"
params: dict[str, Any] = {"method": method, "headers": {}} params: Dict[str, Any] = {"method": method, "headers": {}}
if kwargs: if kwargs:
params["data"] = dumps(kwargs).encode() params["data"] = dumps(kwargs).encode()
params["headers"]["content-type"] = "application/json" params["headers"]["content-type"] = "application/json"

View File

@ -3,16 +3,25 @@ import os
import platform import platform
import signal import signal
import sys import sys
from contextlib import contextmanager from contextlib import contextmanager
from enum import Enum from enum import Enum
from typing import Awaitable, Literal, Union from typing import Awaitable, Union
from multidict import CIMultiDict # type: ignore from multidict import CIMultiDict # type: ignore
from sanic.helpers import Default from sanic.helpers import Default
from sanic.log import error_logger from sanic.log import error_logger
StartMethod = Union[Default, Literal["fork"], Literal["forkserver"], Literal["spawn"]]
if sys.version_info < (3, 8): # no cov
StartMethod = Union[Default, str]
else: # no cov
from typing import Literal
StartMethod = Union[
Default, Literal["fork"], Literal["forkserver"], Literal["spawn"]
]
OS_IS_WINDOWS = os.name == "nt" OS_IS_WINDOWS = os.name == "nt"
PYPY_IMPLEMENTATION = platform.python_implementation() == "PyPy" PYPY_IMPLEMENTATION = platform.python_implementation() == "PyPy"
@ -133,10 +142,7 @@ if use_trio: # pragma: no cover
return trio.Path(path).stat() return trio.Path(path).stat()
open_async = trio.open_file open_async = trio.open_file
CancelledErrors: tuple[type[BaseException], ...] = ( CancelledErrors = tuple([asyncio.CancelledError, trio.Cancelled])
asyncio.CancelledError,
trio.Cancelled,
)
else: else:
if PYPY_IMPLEMENTATION: if PYPY_IMPLEMENTATION:
pypy_os_module_patch() pypy_os_module_patch()
@ -150,7 +156,7 @@ else:
async def open_async(file, mode="r", **kwargs): async def open_async(file, mode="r", **kwargs):
return aio_open(file, mode, **kwargs) return aio_open(file, mode, **kwargs)
CancelledErrors = (asyncio.CancelledError,) CancelledErrors = tuple([asyncio.CancelledError])
def ctrlc_workaround_for_windows(app): def ctrlc_workaround_for_windows(app):

View File

@ -1,10 +1,12 @@
from __future__ import annotations from __future__ import annotations
import sys
from abc import ABCMeta from abc import ABCMeta
from inspect import getmembers, isclass, isdatadescriptor from inspect import getmembers, isclass, isdatadescriptor
from os import environ from os import environ
from pathlib import Path from pathlib import Path
from typing import Any, Callable, Literal, Sequence, Union from typing import Any, Callable, Dict, Optional, Sequence, Union
from warnings import filterwarnings from warnings import filterwarnings
from sanic.constants import LocalCertCreator from sanic.constants import LocalCertCreator
@ -14,14 +16,20 @@ from sanic.http import Http
from sanic.log import error_logger from sanic.log import error_logger
from sanic.utils import load_module_from_file_location, str_to_bool from sanic.utils import load_module_from_file_location, str_to_bool
FilterWarningType = Union[
Literal["default"], if sys.version_info >= (3, 8):
Literal["error"], from typing import Literal
Literal["ignore"],
Literal["always"], FilterWarningType = Union[
Literal["module"], Literal["default"],
Literal["once"], Literal["error"],
] Literal["ignore"],
Literal["always"],
Literal["module"],
Literal["once"],
]
else:
FilterWarningType = str
SANIC_PREFIX = "SANIC_" SANIC_PREFIX = "SANIC_"
@ -92,25 +100,25 @@ class Config(dict, metaclass=DescriptorMeta):
EVENT_AUTOREGISTER: bool EVENT_AUTOREGISTER: bool
DEPRECATION_FILTER: FilterWarningType DEPRECATION_FILTER: FilterWarningType
FORWARDED_FOR_HEADER: str FORWARDED_FOR_HEADER: str
FORWARDED_SECRET: str | None FORWARDED_SECRET: Optional[str]
GRACEFUL_SHUTDOWN_TIMEOUT: float GRACEFUL_SHUTDOWN_TIMEOUT: float
INSPECTOR: bool INSPECTOR: bool
INSPECTOR_HOST: str INSPECTOR_HOST: str
INSPECTOR_PORT: int INSPECTOR_PORT: int
INSPECTOR_TLS_KEY: Path | str | Default INSPECTOR_TLS_KEY: Union[Path, str, Default]
INSPECTOR_TLS_CERT: Path | str | Default INSPECTOR_TLS_CERT: Union[Path, str, Default]
INSPECTOR_API_KEY: str INSPECTOR_API_KEY: str
KEEP_ALIVE_TIMEOUT: int KEEP_ALIVE_TIMEOUT: int
KEEP_ALIVE: bool KEEP_ALIVE: bool
LOCAL_CERT_CREATOR: str | LocalCertCreator LOCAL_CERT_CREATOR: Union[str, LocalCertCreator]
LOCAL_TLS_KEY: Path | str | Default LOCAL_TLS_KEY: Union[Path, str, Default]
LOCAL_TLS_CERT: Path | str | Default LOCAL_TLS_CERT: Union[Path, str, Default]
LOCALHOST: str LOCALHOST: str
MOTD: bool MOTD: bool
MOTD_DISPLAY: dict[str, str] MOTD_DISPLAY: Dict[str, str]
NOISY_EXCEPTIONS: bool NOISY_EXCEPTIONS: bool
PROXIES_COUNT: int | None PROXIES_COUNT: Optional[int]
REAL_IP_HEADER: str | None REAL_IP_HEADER: Optional[str]
REQUEST_BUFFER_SIZE: int REQUEST_BUFFER_SIZE: int
REQUEST_MAX_HEADER_SIZE: int REQUEST_MAX_HEADER_SIZE: int
REQUEST_ID_HEADER: str REQUEST_ID_HEADER: str
@ -119,19 +127,21 @@ class Config(dict, metaclass=DescriptorMeta):
RESPONSE_TIMEOUT: int RESPONSE_TIMEOUT: int
SERVER_NAME: str SERVER_NAME: str
TLS_CERT_PASSWORD: str TLS_CERT_PASSWORD: str
TOUCHUP: Default | bool TOUCHUP: Union[Default, bool]
USE_UVLOOP: Default | bool USE_UVLOOP: Union[Default, bool]
WEBSOCKET_MAX_SIZE: int WEBSOCKET_MAX_SIZE: int
WEBSOCKET_PING_INTERVAL: int WEBSOCKET_PING_INTERVAL: int
WEBSOCKET_PING_TIMEOUT: int WEBSOCKET_PING_TIMEOUT: int
def __init__( def __init__(
self, self,
defaults: dict[str, str | bool | int | float | None] | None = None, defaults: Optional[
env_prefix: str | None = SANIC_PREFIX, Dict[str, Union[str, bool, int, float, None]]
keep_alive: bool | None = None, ] = None,
env_prefix: Optional[str] = SANIC_PREFIX,
keep_alive: Optional[bool] = None,
*, *,
converters: Sequence[Callable[[str], Any]] | None = None, converters: Optional[Sequence[Callable[[str], Any]]] = None,
): ):
defaults = defaults or {} defaults = defaults or {}
super().__init__({**DEFAULT_CONFIG, **defaults}) super().__init__({**DEFAULT_CONFIG, **defaults})
@ -199,7 +209,7 @@ class Config(dict, metaclass=DescriptorMeta):
``` ```
""" """
kwargs.update({k: v for item in other for k, v in dict(item).items()}) kwargs.update({k: v for item in other for k, v in dict(item).items()})
setters: dict[str, Any] = { setters: Dict[str, Any] = {
k: kwargs.pop(k) k: kwargs.pop(k)
for k in {**kwargs}.keys() for k in {**kwargs}.keys()
if k in self.__class__.__setters__ if k in self.__class__.__setters__
@ -227,7 +237,9 @@ class Config(dict, metaclass=DescriptorMeta):
if attr == "LOCAL_CERT_CREATOR" and not isinstance( if attr == "LOCAL_CERT_CREATOR" and not isinstance(
self.LOCAL_CERT_CREATOR, LocalCertCreator self.LOCAL_CERT_CREATOR, LocalCertCreator
): ):
self.LOCAL_CERT_CREATOR = LocalCertCreator[self.LOCAL_CERT_CREATOR.upper()] self.LOCAL_CERT_CREATOR = LocalCertCreator[
self.LOCAL_CERT_CREATOR.upper()
]
elif attr == "DEPRECATION_FILTER": elif attr == "DEPRECATION_FILTER":
self._configure_warnings() self._configure_warnings()
@ -264,7 +276,7 @@ class Config(dict, metaclass=DescriptorMeta):
module=r"sanic.*", module=r"sanic.*",
) )
def _check_error_format(self, format: str | None = None): def _check_error_format(self, format: Optional[str] = None):
check_error_format(format or self.FALLBACK_ERROR_FORMAT) check_error_format(format or self.FALLBACK_ERROR_FORMAT)
def load_environment_vars(self, prefix=SANIC_PREFIX): def load_environment_vars(self, prefix=SANIC_PREFIX):
@ -320,7 +332,7 @@ class Config(dict, metaclass=DescriptorMeta):
except ValueError: except ValueError:
pass pass
def update_config(self, config: bytes | str | dict | Any): def update_config(self, config: Union[bytes, str, dict, Any]):
"""Update app.config. """Update app.config.
.. note:: .. note::

View File

@ -1,3 +1,4 @@
from .response import Cookie, CookieJar from .response import Cookie, CookieJar
__all__ = ("Cookie", "CookieJar") __all__ = ("Cookie", "CookieJar")

View File

@ -1,10 +1,12 @@
import re import re
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from sanic.cookies.response import Cookie from sanic.cookies.response import Cookie
from sanic.log import deprecation from sanic.log import deprecation
from sanic.request.parameters import RequestParameters from sanic.request.parameters import RequestParameters
COOKIE_NAME_RESERVED_CHARS = re.compile( COOKIE_NAME_RESERVED_CHARS = re.compile(
'[\x00-\x1F\x7F-\xFF()<>@,;:\\\\"/[\\]?={} \x09]' '[\x00-\x1F\x7F-\xFF()<>@,;:\\\\"/[\\]?={} \x09]'
) )
@ -147,7 +149,9 @@ class CookieRequestParameters(RequestParameters):
except KeyError: except KeyError:
return super().get(name, default) return super().get(name, default)
def getlist(self, name: str, default: Optional[Any] = None) -> Optional[Any]: def getlist(
self, name: str, default: Optional[Any] = None
) -> Optional[Any]:
try: try:
return self._get_prefixed_cookie(name) return self._get_prefixed_cookie(name)
except KeyError: except KeyError:

View File

@ -2,25 +2,31 @@ from __future__ import annotations
import re import re
import string import string
import sys
from datetime import datetime from datetime import datetime
from typing import TYPE_CHECKING, Any, Union from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
from sanic.exceptions import ServerError from sanic.exceptions import ServerError
from sanic.log import deprecation from sanic.log import deprecation
if TYPE_CHECKING: if TYPE_CHECKING:
from sanic.compat import Header from sanic.compat import Header
from typing import Literal if sys.version_info < (3, 8): # no cov
SameSite = str
else: # no cov
from typing import Literal
SameSite = Union[ SameSite = Union[
Literal["Strict"], Literal["Strict"],
Literal["Lax"], Literal["Lax"],
Literal["None"], Literal["None"],
Literal["strict"], Literal["strict"],
Literal["lax"], Literal["lax"],
Literal["none"], Literal["none"],
] ]
DEFAULT_MAX_AGE = 0 DEFAULT_MAX_AGE = 0
SAMESITE_VALUES = ("strict", "lax", "none") SAMESITE_VALUES = ("strict", "lax", "none")
@ -174,7 +180,7 @@ class CookieJar(dict):
return CookieJar.HEADER_KEY return CookieJar.HEADER_KEY
@property @property
def cookie_headers(self) -> dict[str, str]: # no cov def cookie_headers(self) -> Dict[str, str]: # no cov
"""Deprecated in v24.3""" """Deprecated in v24.3"""
deprecation( deprecation(
"The CookieJar.coookie_headers property has been deprecated " "The CookieJar.coookie_headers property has been deprecated "
@ -185,7 +191,7 @@ class CookieJar(dict):
return {key: self.header_key for key in self} return {key: self.header_key for key in self}
@property @property
def cookies(self) -> list[Cookie]: def cookies(self) -> List[Cookie]:
"""A list of cookies in the CookieJar. """A list of cookies in the CookieJar.
Returns: Returns:
@ -197,10 +203,10 @@ class CookieJar(dict):
self, self,
key: str, key: str,
path: str = "/", path: str = "/",
domain: str | None = None, domain: Optional[str] = None,
host_prefix: bool = False, host_prefix: bool = False,
secure_prefix: bool = False, secure_prefix: bool = False,
) -> Cookie | None: ) -> Optional[Cookie]:
"""Fetch a cookie from the CookieJar. """Fetch a cookie from the CookieJar.
Args: Args:
@ -230,7 +236,7 @@ class CookieJar(dict):
self, self,
key: str, key: str,
path: str = "/", path: str = "/",
domain: str | None = None, domain: Optional[str] = None,
host_prefix: bool = False, host_prefix: bool = False,
secure_prefix: bool = False, secure_prefix: bool = False,
) -> bool: ) -> bool:
@ -265,14 +271,14 @@ class CookieJar(dict):
value: str, value: str,
*, *,
path: str = "/", path: str = "/",
domain: str | None = None, domain: Optional[str] = None,
secure: bool = True, secure: bool = True,
max_age: int | None = None, max_age: Optional[int] = None,
expires: datetime | None = None, expires: Optional[datetime] = None,
httponly: bool = False, httponly: bool = False,
samesite: SameSite | None = "Lax", samesite: Optional[SameSite] = "Lax",
partitioned: bool = False, partitioned: bool = False,
comment: str | None = None, comment: Optional[str] = None,
host_prefix: bool = False, host_prefix: bool = False,
secure_prefix: bool = False, secure_prefix: bool = False,
) -> Cookie: ) -> Cookie:
@ -356,7 +362,7 @@ class CookieJar(dict):
key: str, key: str,
*, *,
path: str = "/", path: str = "/",
domain: str | None = None, domain: Optional[str] = None,
host_prefix: bool = False, host_prefix: bool = False,
secure_prefix: bool = False, secure_prefix: bool = False,
) -> None: ) -> None:
@ -384,7 +390,7 @@ class CookieJar(dict):
:type secure_prefix: bool :type secure_prefix: bool
""" """
# remove it from header # remove it from header
cookies: list[Cookie] = self.headers.popall(self.HEADER_KEY, []) cookies: List[Cookie] = self.headers.popall(self.HEADER_KEY, [])
for cookie in cookies: for cookie in cookies:
if ( if (
cookie.key != Cookie.make_key(key, host_prefix, secure_prefix) cookie.key != Cookie.make_key(key, host_prefix, secure_prefix)
@ -475,14 +481,14 @@ class Cookie(dict):
value: str, value: str,
*, *,
path: str = "/", path: str = "/",
domain: str | None = None, domain: Optional[str] = None,
secure: bool = True, secure: bool = True,
max_age: int | None = None, max_age: Optional[int] = None,
expires: datetime | None = None, expires: Optional[datetime] = None,
httponly: bool = False, httponly: bool = False,
samesite: SameSite | None = "Lax", samesite: Optional[SameSite] = "Lax",
partitioned: bool = False, partitioned: bool = False,
comment: str | None = None, comment: Optional[str] = None,
host_prefix: bool = False, host_prefix: bool = False,
secure_prefix: bool = False, secure_prefix: bool = False,
): ):
@ -496,7 +502,9 @@ class Cookie(dict):
"Cannot set host_prefix on a cookie without secure=True" "Cannot set host_prefix on a cookie without secure=True"
) )
if path != "/": if path != "/":
raise ServerError("Cannot set host_prefix on a cookie unless path='/'") raise ServerError(
"Cannot set host_prefix on a cookie unless path='/'"
)
if domain: if domain:
raise ServerError( raise ServerError(
"Cannot set host_prefix on a cookie with a defined domain" "Cannot set host_prefix on a cookie with a defined domain"
@ -553,7 +561,7 @@ class Cookie(dict):
# in v24.3 when this is no longer a dict # in v24.3 when this is no longer a dict
def _set_value(self, key: str, value: Any) -> None: def _set_value(self, key: str, value: Any) -> None:
if key not in self._keys: if key not in self._keys:
raise KeyError(f"Unknown cookie property: {key}={value}") raise KeyError("Unknown cookie property: %s=%s" % (key, value))
if value is not None: if value is not None:
if key.lower() == "max-age" and not str(value).isdigit(): if key.lower() == "max-age" and not str(value).isdigit():
@ -596,18 +604,21 @@ class Cookie(dict):
def __str__(self): def __str__(self):
"""Format as a Set-Cookie header value.""" """Format as a Set-Cookie header value."""
output = [f"{self.key}={_quote(self.value)}"] output = ["%s=%s" % (self.key, _quote(self.value))]
key_index = list(self._keys) key_index = list(self._keys)
for key, value in sorted(self.items(), key=lambda x: key_index.index(x[0])): for key, value in sorted(
self.items(), key=lambda x: key_index.index(x[0])
):
if value is not None and value is not False: if value is not None and value is not False:
if key == "max-age": if key == "max-age":
try: try:
output.append("%s=%d" % (self._keys[key], value)) output.append("%s=%d" % (self._keys[key], value))
except TypeError: except TypeError:
output.append(f"{self._keys[key]}={value}") output.append("%s=%s" % (self._keys[key], value))
elif key == "expires": elif key == "expires":
output.append( output.append(
"{}={}".format( "%s=%s"
% (
self._keys[key], self._keys[key],
value.strftime("%a, %d-%b-%Y %T GMT"), value.strftime("%a, %d-%b-%Y %T GMT"),
) )
@ -615,7 +626,7 @@ class Cookie(dict):
elif key in self._flags: elif key in self._flags:
output.append(self._keys[key]) output.append(self._keys[key])
else: else:
output.append(f"{self._keys[key]}={value}") output.append("%s=%s" % (self._keys[key], value))
return "; ".join(output) return "; ".join(output)
@ -629,7 +640,7 @@ class Cookie(dict):
self._set_value("path", value) self._set_value("path", value)
@property @property
def expires(self) -> datetime | None: # no cov def expires(self) -> Optional[datetime]: # no cov
"""The expiration date of the cookie. Defaults to `None`.""" """The expiration date of the cookie. Defaults to `None`."""
return self.get("expires") return self.get("expires")
@ -638,7 +649,7 @@ class Cookie(dict):
self._set_value("expires", value) self._set_value("expires", value)
@property @property
def comment(self) -> str | None: # no cov def comment(self) -> Optional[str]: # no cov
"""A comment for the cookie. Defaults to `None`.""" """A comment for the cookie. Defaults to `None`."""
return self.get("comment") return self.get("comment")
@ -647,7 +658,7 @@ class Cookie(dict):
self._set_value("comment", value) self._set_value("comment", value)
@property @property
def domain(self) -> str | None: # no cov def domain(self) -> Optional[str]: # no cov
"""The domain of the cookie. Defaults to `None`.""" """The domain of the cookie. Defaults to `None`."""
return self.get("domain") return self.get("domain")
@ -656,7 +667,7 @@ class Cookie(dict):
self._set_value("domain", value) self._set_value("domain", value)
@property @property
def max_age(self) -> int | None: # no cov def max_age(self) -> Optional[int]: # no cov
"""The maximum age of the cookie in seconds. Defaults to `None`.""" """The maximum age of the cookie in seconds. Defaults to `None`."""
return self.get("max-age") return self.get("max-age")
@ -683,7 +694,7 @@ class Cookie(dict):
self._set_value("httponly", value) self._set_value("httponly", value)
@property @property
def samesite(self) -> SameSite | None: # no cov def samesite(self) -> Optional[SameSite]: # no cov
"""The SameSite attribute for the cookie. Defaults to `"Lax"`.""" """The SameSite attribute for the cookie. Defaults to `"Lax"`."""
return self.get("samesite") return self.get("samesite")

View File

@ -16,6 +16,7 @@ from __future__ import annotations
import sys import sys
import typing as t import typing as t
from functools import partial from functools import partial
from traceback import extract_tb from traceback import extract_tb
@ -25,6 +26,7 @@ from sanic.log import deprecation, logger
from sanic.pages.error import ErrorPage from sanic.pages.error import ErrorPage
from sanic.response import html, json, text from sanic.response import html, json, text
dumps: t.Callable[..., str] dumps: t.Callable[..., str]
try: try:
from ujson import dumps from ujson import dumps
@ -71,7 +73,7 @@ class BaseRenderer:
self.debug = debug self.debug = debug
@property @property
def headers(self) -> dict[str, str]: def headers(self) -> t.Dict[str, str]:
"""The headers to be used for the response.""" """The headers to be used for the response."""
if isinstance(self.exception, SanicException): if isinstance(self.exception, SanicException):
return getattr(self.exception, "headers", {}) return getattr(self.exception, "headers", {})
@ -190,7 +192,8 @@ class TextRenderer(BaseRenderer):
lines += [ lines += [
f"{self.exception.__class__.__name__}: {self.exception} while " f"{self.exception.__class__.__name__}: {self.exception} while "
f"handling path {self.request.path}", f"handling path {self.request.path}",
f"Traceback of {self.request.app.name} " "(most recent call last):\n", f"Traceback of {self.request.app.name} "
"(most recent call last):\n",
] ]
while exc_value: while exc_value:
@ -323,8 +326,8 @@ def exception_response(
exception: Exception, exception: Exception,
debug: bool, debug: bool,
fallback: str, fallback: str,
base: type[BaseRenderer], base: t.Type[BaseRenderer],
renderer: type[BaseRenderer] | None = None, renderer: t.Optional[t.Type[BaseRenderer]] = None,
) -> HTTPResponse: ) -> HTTPResponse:
"""Render a response for the default FALLBACK exception handler.""" """Render a response for the default FALLBACK exception handler."""
if not renderer: if not renderer:
@ -387,7 +390,9 @@ def guess_mime(req: Request, fallback: str) -> str:
if m: if m:
format = CONFIG_BY_MIME[m.mime] format = CONFIG_BY_MIME[m.mime]
source = formats[format] source = formats[format]
logger.debug(f"The client accepts {m.header}, using '{format}' from {source}") logger.debug(
f"The client accepts {m.header}, using '{format}' from {source}"
)
else: else:
logger.debug(f"No format found, the client accepts {req.accept!r}") logger.debug(f"No format found, the client accepts {req.accept!r}")
return m.mime return m.mime

View File

@ -69,7 +69,9 @@ class SanicException(Exception):
) -> None: ) -> None:
self.context = context self.context = context
self.extra = extra self.extra = extra
status_code = status_code or getattr(self.__class__, "status_code", None) status_code = status_code or getattr(
self.__class__, "status_code", None
)
quiet = quiet or getattr(self.__class__, "quiet", None) quiet = quiet or getattr(self.__class__, "quiet", None)
headers = headers or getattr(self.__class__, "headers", {}) headers = headers or getattr(self.__class__, "headers", {})
if message is None: if message is None:
@ -619,7 +621,9 @@ class Unauthorized(HTTPException):
# if auth-scheme is specified, set "WWW-Authenticate" header # if auth-scheme is specified, set "WWW-Authenticate" header
if scheme is not None: if scheme is not None:
values = [f'{k!s}="{v!s}"' for k, v in challenges.items()] values = [
'{!s}="{!s}"'.format(k, v) for k, v in challenges.items()
]
challenge = ", ".join(values) challenge = ", ".join(values)
self.headers = { self.headers = {

View File

@ -2,6 +2,7 @@ from .content_range import ContentRangeHandler
from .directory import DirectoryHandler from .directory import DirectoryHandler
from .error import ErrorHandler from .error import ErrorHandler
__all__ = ( __all__ = (
"ContentRangeHandler", "ContentRangeHandler",
"DirectoryHandler", "DirectoryHandler",

View File

@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import os import os
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from sanic.exceptions import ( from sanic.exceptions import (
@ -10,6 +11,7 @@ from sanic.exceptions import (
) )
from sanic.models.protocol_types import Range from sanic.models.protocol_types import Range
if TYPE_CHECKING: if TYPE_CHECKING:
from sanic import Request from sanic import Request
@ -31,19 +33,27 @@ class ContentRangeHandler(Range):
raise HeaderNotFound("Range Header Not Found") raise HeaderNotFound("Range Header Not Found")
unit, _, value = tuple(map(str.strip, _range.partition("="))) unit, _, value = tuple(map(str.strip, _range.partition("=")))
if unit != "bytes": if unit != "bytes":
raise InvalidRangeType(f"{unit} is not a valid Range Type", self) raise InvalidRangeType(
"%s is not a valid Range Type" % (unit,), self
)
start_b, _, end_b = tuple(map(str.strip, value.partition("-"))) start_b, _, end_b = tuple(map(str.strip, value.partition("-")))
try: try:
self.start = int(start_b) if start_b else None self.start = int(start_b) if start_b else None
except ValueError: except ValueError:
raise RangeNotSatisfiable(f"'{start_b}' is invalid for Content Range", self) raise RangeNotSatisfiable(
"'%s' is invalid for Content Range" % (start_b,), self
)
try: try:
self.end = int(end_b) if end_b else None self.end = int(end_b) if end_b else None
except ValueError: except ValueError:
raise RangeNotSatisfiable(f"'{end_b}' is invalid for Content Range", self) raise RangeNotSatisfiable(
"'%s' is invalid for Content Range" % (end_b,), self
)
if self.end is None: if self.end is None:
if self.start is None: if self.start is None:
raise RangeNotSatisfiable("Invalid for Content Range parameters", self) raise RangeNotSatisfiable(
"Invalid for Content Range parameters", self
)
else: else:
# this case represents `Content-Range: bytes 5-` # this case represents `Content-Range: bytes 5-`
self.end = self.total - 1 self.end = self.total - 1
@ -53,9 +63,14 @@ class ContentRangeHandler(Range):
self.start = self.total - self.end self.start = self.total - self.end
self.end = self.total - 1 self.end = self.total - 1
if self.start >= self.end: if self.start >= self.end:
raise RangeNotSatisfiable("Invalid for Content Range parameters", self) raise RangeNotSatisfiable(
"Invalid for Content Range parameters", self
)
self.size = self.end - self.start + 1 self.size = self.end - self.start + 1
self.headers = {"Content-Range": f"bytes {self.start}-{self.end}/{self.total}"} self.headers = {
"Content-Range": "bytes %s-%s/%s"
% (self.start, self.end, self.total)
}
def __bool__(self): def __bool__(self):
return hasattr(self, "size") and self.size > 0 return hasattr(self, "size") and self.size > 0

View File

@ -4,7 +4,7 @@ from datetime import datetime
from operator import itemgetter from operator import itemgetter
from pathlib import Path from pathlib import Path
from stat import S_ISDIR from stat import S_ISDIR
from typing import Iterable, Sequence, cast from typing import Dict, Iterable, Optional, Sequence, Union, cast
from sanic.exceptions import NotFound from sanic.exceptions import NotFound
from sanic.pages.directory_page import DirectoryPage, FileInfo from sanic.pages.directory_page import DirectoryPage, FileInfo
@ -28,7 +28,7 @@ class DirectoryHandler:
uri: str, uri: str,
directory: Path, directory: Path,
directory_view: bool = False, directory_view: bool = False,
index: str | Sequence[str] | None = None, index: Optional[Union[str, Sequence[str]]] = None,
) -> None: ) -> None:
if isinstance(index, str): if isinstance(index, str):
index = [index] index = [index]
@ -60,7 +60,9 @@ class DirectoryHandler:
return await file(index_file) return await file(index_file)
if self.directory_view: if self.directory_view:
return self._index(self.directory / current, path, request.app.debug) return self._index(
self.directory / current, path, request.app.debug
)
if self.index: if self.index:
raise NotFound("File not found") raise NotFound("File not found")
@ -70,16 +72,20 @@ class DirectoryHandler:
def _index(self, location: Path, path: str, debug: bool): def _index(self, location: Path, path: str, debug: bool):
# Remove empty path elements, append slash # Remove empty path elements, append slash
if "//" in path or not path.endswith("/"): if "//" in path or not path.endswith("/"):
return redirect("/" + "".join([f"{p}/" for p in path.split("/") if p])) return redirect(
"/" + "".join([f"{p}/" for p in path.split("/") if p])
)
# Render file browser # Render file browser
page = DirectoryPage(self._iter_files(location), path, debug) page = DirectoryPage(self._iter_files(location), path, debug)
return html(page.render()) return html(page.render())
def _prepare_file(self, path: Path) -> dict[str, int | str]: def _prepare_file(self, path: Path) -> Dict[str, Union[int, str]]:
stat = path.stat() stat = path.stat()
modified = ( modified = (
datetime.fromtimestamp(stat.st_mtime).isoformat()[:19].replace("T", " ") datetime.fromtimestamp(stat.st_mtime)
.isoformat()[:19]
.replace("T", " ")
) )
is_dir = S_ISDIR(stat.st_mode) is_dir = S_ISDIR(stat.st_mode)
icon = "📁" if is_dir else "📄" icon = "📁" if is_dir else "📄"

View File

@ -1,5 +1,7 @@
from __future__ import annotations from __future__ import annotations
from typing import Dict, List, Optional, Tuple, Type
from sanic.errorpages import BaseRenderer, TextRenderer, exception_response from sanic.errorpages import BaseRenderer, TextRenderer, exception_response
from sanic.exceptions import ServerError from sanic.exceptions import ServerError
from sanic.log import error_logger from sanic.log import error_logger
@ -23,20 +25,20 @@ class ErrorHandler:
def __init__( def __init__(
self, self,
base: type[BaseRenderer] = TextRenderer, base: Type[BaseRenderer] = TextRenderer,
): ):
self.cached_handlers: dict[ self.cached_handlers: Dict[
tuple[type[BaseException], str | None], RouteHandler | None Tuple[Type[BaseException], Optional[str]], Optional[RouteHandler]
] = {} ] = {}
self.debug = False self.debug = False
self.base = base self.base = base
def _full_lookup(self, exception, route_name: str | None = None): def _full_lookup(self, exception, route_name: Optional[str] = None):
return self.lookup(exception, route_name) return self.lookup(exception, route_name)
def _add( def _add(
self, self,
key: tuple[type[BaseException], str | None], key: Tuple[Type[BaseException], Optional[str]],
handler: RouteHandler, handler: RouteHandler,
) -> None: ) -> None:
if key in self.cached_handlers: if key in self.cached_handlers:
@ -51,7 +53,7 @@ class ErrorHandler:
raise ServerError(message) raise ServerError(message)
self.cached_handlers[key] = handler self.cached_handlers[key] = handler
def add(self, exception, handler, route_names: list[str] | None = None): def add(self, exception, handler, route_names: Optional[List[str]] = None):
"""Add a new exception handler to an already existing handler object. """Add a new exception handler to an already existing handler object.
Args: Args:
@ -70,7 +72,7 @@ class ErrorHandler:
else: else:
self._add((exception, None), handler) self._add((exception, None), handler)
def lookup(self, exception, route_name: str | None = None): def lookup(self, exception, route_name: Optional[str] = None):
"""Lookup the existing instance of `ErrorHandler` and fetch the registered handler for a specific type of exception. """Lookup the existing instance of `ErrorHandler` and fetch the registered handler for a specific type of exception.
This method leverages a dict lookup to speedup the retrieval process. This method leverages a dict lookup to speedup the retrieval process.
@ -96,7 +98,9 @@ class ErrorHandler:
exception_key = (ancestor, name) exception_key = (ancestor, name)
if exception_key in self.cached_handlers: if exception_key in self.cached_handlers:
handler = self.cached_handlers[exception_key] handler = self.cached_handlers[exception_key]
self.cached_handlers[(exception_class, route_name)] = handler self.cached_handlers[
(exception_class, route_name)
] = handler
return handler return handler
if ancestor is BaseException: if ancestor is BaseException:
@ -131,11 +135,13 @@ class ErrorHandler:
url = repr(request.url) url = repr(request.url)
except AttributeError: # no cov except AttributeError: # no cov
url = "unknown" url = "unknown"
response_message = f'Exception raised in exception handler "{handler.__name__}" for uri: {url}' response_message = (
error_logger.exception(response_message) "Exception raised in exception handler " '"%s" for uri: %s'
)
error_logger.exception(response_message, handler.__name__, url)
if self.debug: if self.debug:
return text(response_message, 500) return text(response_message % (handler.__name__, url), 500)
else: else:
return text("An error occurred while handling an error", 500) return text("An error occurred while handling an error", 500)
return response return response
@ -194,4 +200,6 @@ class ErrorHandler:
except AttributeError: # no cov except AttributeError: # no cov
url = "unknown" url = "unknown"
error_logger.exception("Exception occurred while handling uri: %s", url) error_logger.exception(
"Exception occurred while handling uri: %s", url
)

View File

@ -1,12 +1,14 @@
from __future__ import annotations from __future__ import annotations
import re import re
from typing import Any, Dict, Iterable, Tuple, Union
from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Union
from urllib.parse import unquote from urllib.parse import unquote
from sanic.exceptions import InvalidHeader from sanic.exceptions import InvalidHeader
from sanic.helpers import STATUS_CODES from sanic.helpers import STATUS_CODES
# TODO: # TODO:
# - the Options object should be a typed object to allow for less casting # - the Options object should be a typed object to allow for less casting
# across the application (in request.py for example) # across the application (in request.py for example)
@ -19,7 +21,9 @@ _token, _quoted = r"([\w!#$%&'*+\-.^_`|~]+)", r'"([^"]*)"'
_param = re.compile(rf";\s*{_token}=(?:{_token}|{_quoted})", re.ASCII) _param = re.compile(rf";\s*{_token}=(?:{_token}|{_quoted})", re.ASCII)
_ipv6 = "(?:[0-9A-Fa-f]{0,4}:){2,7}[0-9A-Fa-f]{0,4}" _ipv6 = "(?:[0-9A-Fa-f]{0,4}:){2,7}[0-9A-Fa-f]{0,4}"
_ipv6_re = re.compile(_ipv6) _ipv6_re = re.compile(_ipv6)
_host_re = re.compile(r"((?:\[" + _ipv6 + r"\])|[a-zA-Z0-9.\-]{1,253})(?::(\d{1,5}))?") _host_re = re.compile(
r"((?:\[" + _ipv6 + r"\])|[a-zA-Z0-9.\-]{1,253})(?::(\d{1,5}))?"
)
# RFC's quoted-pair escapes are mostly ignored by browsers. Chrome, Firefox and # RFC's quoted-pair escapes are mostly ignored by browsers. Chrome, Firefox and
# curl all have different escaping, that we try to handle as well as possible, # curl all have different escaping, that we try to handle as well as possible,
@ -81,8 +85,8 @@ class MediaType:
def match( def match(
self, self,
mime_with_params: str | MediaType, mime_with_params: Union[str, MediaType],
) -> MediaType | None: ) -> Optional[MediaType]:
"""Match this media type against another media type. """Match this media type against another media type.
Check if this media type matches the given mime type/subtype. Check if this media type matches the given mime type/subtype.
@ -120,7 +124,9 @@ class MediaType:
or mt.subtype == "*" or mt.subtype == "*"
) )
# Type match # Type match
and (self.type == mt.type or self.type == "*" or mt.type == "*") and (
self.type == mt.type or self.type == "*" or mt.type == "*"
)
) )
else None else None
) )
@ -135,7 +141,7 @@ class MediaType:
return any(part == "*" for part in (self.subtype, self.type)) return any(part == "*" for part in (self.subtype, self.type))
@classmethod @classmethod
def _parse(cls, mime_with_params: str) -> MediaType | None: def _parse(cls, mime_with_params: str) -> Optional[MediaType]:
mtype = mime_with_params.strip() mtype = mime_with_params.strip()
if "/" not in mime_with_params: if "/" not in mime_with_params:
return None return None
@ -145,10 +151,12 @@ class MediaType:
if not type_ or not subtype: if not type_ or not subtype:
raise ValueError(f"Invalid media type: {mtype}") raise ValueError(f"Invalid media type: {mtype}")
params = { params = dict(
key.strip(): value.strip() [
for key, value in (param.split("=", 1) for param in raw_params) (key.strip(), value.strip())
} for key, value in (param.split("=", 1) for param in raw_params)
]
)
return cls(type_.lstrip(), subtype.rstrip(), **params) return cls(type_.lstrip(), subtype.rstrip(), **params)
@ -165,7 +173,7 @@ class Matched:
header (MediaType): The header to match against, if any. header (MediaType): The header to match against, if any.
""" """
def __init__(self, mime: str, header: MediaType | None): def __init__(self, mime: str, header: Optional[MediaType]):
self.mime = mime self.mime = mime
self.header = header self.header = header
@ -192,7 +200,7 @@ class Matched:
) )
) )
def _compare(self, other) -> tuple[bool, Matched]: def _compare(self, other) -> Tuple[bool, Matched]:
if isinstance(other, str): if isinstance(other, str):
parsed = Matched.parse(other) parsed = Matched.parse(other)
if self.mime == other: if self.mime == other:
@ -207,7 +215,7 @@ class Matched:
f"mime types of '{self.mime}' and '{other}'" f"mime types of '{self.mime}' and '{other}'"
) )
def match(self, other: str | Matched) -> Matched | None: def match(self, other: Union[str, Matched]) -> Optional[Matched]:
"""Match this MIME string against another MIME string. """Match this MIME string against another MIME string.
Check if this MIME string matches the given MIME string. Wildcards are supported both ways on both type and subtype. Check if this MIME string matches the given MIME string. Wildcards are supported both ways on both type and subtype.
@ -288,7 +296,7 @@ class AcceptList(list):
return ", ".join(str(m) for m in self) return ", ".join(str(m) for m in self)
def parse_accept(accept: str | None) -> AcceptList: def parse_accept(accept: Optional[str]) -> AcceptList:
"""Parse an Accept header and order the acceptable media types according to RFC 7231, s. 5.3.2 """Parse an Accept header and order the acceptable media types according to RFC 7231, s. 5.3.2
https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2 https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
@ -308,7 +316,9 @@ def parse_accept(accept: str | None) -> AcceptList:
accept = "*/*" # No header means that all types are accepted accept = "*/*" # No header means that all types are accepted
try: try:
a = [ a = [
mt for mt in [MediaType._parse(mtype) for mtype in accept.split(",")] if mt mt
for mt in [MediaType._parse(mtype) for mtype in accept.split(",")]
if mt
] ]
if not a: if not a:
raise ValueError raise ValueError
@ -317,7 +327,7 @@ def parse_accept(accept: str | None) -> AcceptList:
raise InvalidHeader(f"Invalid header value in Accept: {accept}") raise InvalidHeader(f"Invalid header value in Accept: {accept}")
def parse_content_header(value: str) -> tuple[str, Options]: def parse_content_header(value: str) -> Tuple[str, Options]:
"""Parse content-type and content-disposition header values. """Parse content-type and content-disposition header values.
E.g. `form-data; name=upload; filename="file.txt"` to E.g. `form-data; name=upload; filename="file.txt"` to
@ -336,10 +346,11 @@ def parse_content_header(value: str) -> tuple[str, Options]:
""" """
pos = value.find(";") pos = value.find(";")
if pos == -1: if pos == -1:
options: dict[str, int | str] = {} options: Dict[str, Union[int, str]] = {}
else: else:
options = { options = {
m.group(1).lower(): (m.group(2) or m.group(3)) m.group(1)
.lower(): (m.group(2) or m.group(3))
.replace("%22", '"') .replace("%22", '"')
.replace("%0D%0A", "\n") .replace("%0D%0A", "\n")
for m in _param.finditer(value[pos:]) for m in _param.finditer(value[pos:])
@ -356,7 +367,7 @@ def parse_content_header(value: str) -> tuple[str, Options]:
_rparam = re.compile(f"(?:{_token}|{_quoted})={_token}\\s*($|[;,])", re.ASCII) _rparam = re.compile(f"(?:{_token}|{_quoted})={_token}\\s*($|[;,])", re.ASCII)
def parse_forwarded(headers, config) -> Options | None: def parse_forwarded(headers, config) -> Optional[Options]:
"""Parse RFC 7239 Forwarded headers. """Parse RFC 7239 Forwarded headers.
The value of `by` or `secret` must match `config.FORWARDED_SECRET` The value of `by` or `secret` must match `config.FORWARDED_SECRET`
:return: dict with keys and values, or None if nothing matched :return: dict with keys and values, or None if nothing matched
@ -370,7 +381,7 @@ def parse_forwarded(headers, config) -> Options | None:
return None return None
# Loop over <separator><key>=<value> elements from right to left # Loop over <separator><key>=<value> elements from right to left
sep = pos = None sep = pos = None
options: list[tuple[str, str]] = [] options: List[Tuple[str, str]] = []
found = False found = False
for m in _rparam.finditer(header[::-1]): for m in _rparam.finditer(header[::-1]):
# Start of new element? (on parser skips and non-semicolon right sep) # Start of new element? (on parser skips and non-semicolon right sep)
@ -394,7 +405,7 @@ def parse_forwarded(headers, config) -> Options | None:
return fwd_normalize(reversed(options)) if found else None return fwd_normalize(reversed(options)) if found else None
def parse_xforwarded(headers, config) -> Options | None: def parse_xforwarded(headers, config) -> Optional[Options]:
"""Parse traditional proxy headers.""" """Parse traditional proxy headers."""
real_ip_header = config.REAL_IP_HEADER real_ip_header = config.REAL_IP_HEADER
proxies_count = config.PROXIES_COUNT proxies_count = config.PROXIES_COUNT
@ -405,7 +416,11 @@ def parse_xforwarded(headers, config) -> Options | None:
# Combine, split and filter multiple headers' entries # Combine, split and filter multiple headers' entries
forwarded_for = headers.getall(config.FORWARDED_FOR_HEADER) forwarded_for = headers.getall(config.FORWARDED_FOR_HEADER)
proxies = [ proxies = [
p for p in (p.strip() for h in forwarded_for for p in h.split(",")) if p p
for p in (
p.strip() for h in forwarded_for for p in h.split(",")
)
if p
] ]
addr = proxies[-proxies_count] addr = proxies[-proxies_count]
except (KeyError, IndexError): except (KeyError, IndexError):
@ -437,7 +452,7 @@ def fwd_normalize(fwd: OptionsIterable) -> Options:
Returns: Returns:
Options: A dict of normalized key-value pairs. Options: A dict of normalized key-value pairs.
""" """
ret: dict[str, int | str] = {} ret: Dict[str, Union[int, str]] = {}
for key, val in fwd: for key, val in fwd:
if val is not None: if val is not None:
try: try:
@ -474,7 +489,7 @@ def fwd_normalize_address(addr: str) -> str:
return addr.lower() return addr.lower()
def parse_host(host: str) -> tuple[str | None, int | None]: def parse_host(host: str) -> Tuple[Optional[str], Optional[int]]:
"""Split host:port into hostname and port. """Split host:port into hostname and port.
Args: Args:
@ -516,9 +531,9 @@ def format_http1_response(status: int, headers: HeaderBytesIterable) -> bytes:
def parse_credentials( def parse_credentials(
header: str | None, header: Optional[str],
prefixes: list | tuple | set | None = None, prefixes: Optional[Union[List, Tuple, Set]] = None,
) -> tuple[str | None, str | None]: ) -> Tuple[Optional[str], Optional[str]]:
"""Parses any header with the aim to retrieve any credentials from it. """Parses any header with the aim to retrieve any credentials from it.
Args: Args:

View File

@ -1,10 +1,12 @@
"""Defines basics of HTTP standard.""" """Defines basics of HTTP standard."""
import sys import sys
from importlib import import_module from importlib import import_module
from inspect import ismodule from inspect import ismodule
from typing import Dict from typing import Dict
STATUS_CODES: Dict[int, bytes] = { STATUS_CODES: Dict[int, bytes] = {
100: b"Continue", 100: b"Continue",
101: b"Switching Protocols", 101: b"Switching Protocols",
@ -130,7 +132,7 @@ def remove_entity_headers(headers, allowed=("content-location", "expires")):
returns the headers without the entity headers returns the headers without the entity headers
""" """
allowed = {h.lower() for h in allowed} allowed = set([h.lower() for h in allowed])
headers = { headers = {
header: value header: value
for header, value in headers.items() for header, value in headers.items()

View File

@ -2,4 +2,5 @@ from .constants import Stage
from .http1 import Http from .http1 import Http
from .http3 import Http3 from .http3 import Http3
__all__ = ("Http", "Stage", "Http3") __all__ = ("Http", "Stage", "Http3")

View File

@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING: if TYPE_CHECKING:
from sanic.request import Request from sanic.request import Request
@ -24,6 +25,7 @@ from sanic.http.stream import Stream
from sanic.log import access_logger, error_logger, logger from sanic.log import access_logger, error_logger, logger
from sanic.touchup import TouchUpMeta from sanic.touchup import TouchUpMeta
HTTP_CONTINUE = b"HTTP/1.1 100 Continue\r\n\r\n" HTTP_CONTINUE = b"HTTP/1.1 100 Continue\r\n\r\n"
@ -361,20 +363,26 @@ class Http(Stream, metaclass=TouchUpMeta):
self.response_func = None self.response_func = None
self.stage = Stage.IDLE self.stage = Stage.IDLE
async def http1_response_chunked(self, data: bytes, end_stream: bool) -> None: async def http1_response_chunked(
self, data: bytes, end_stream: bool
) -> None:
"""Format a part of response body in chunked encoding.""" """Format a part of response body in chunked encoding."""
# Chunked encoding # Chunked encoding
size = len(data) size = len(data)
if end_stream: if end_stream:
await self._send( await self._send(
b"%x\r\n%b\r\n0\r\n\r\n" % (size, data) if size else b"0\r\n\r\n" b"%x\r\n%b\r\n0\r\n\r\n" % (size, data)
if size
else b"0\r\n\r\n"
) )
self.response_func = None self.response_func = None
self.stage = Stage.IDLE self.stage = Stage.IDLE
elif size: elif size:
await self._send(b"%x\r\n%b\r\n" % (size, data)) await self._send(b"%x\r\n%b\r\n" % (size, data))
async def http1_response_normal(self, data: bytes, end_stream: bool) -> None: async def http1_response_normal(
self, data: bytes, end_stream: bool
) -> None:
"""Format / keep track of non-chunked response.""" """Format / keep track of non-chunked response."""
bytes_left = self.response_bytes_left - len(data) bytes_left = self.response_bytes_left - len(data)
if bytes_left <= 0: if bytes_left <= 0:
@ -412,7 +420,9 @@ class Http(Stream, metaclass=TouchUpMeta):
exception, (ServiceUnavailable, RequestCancelled) exception, (ServiceUnavailable, RequestCancelled)
) )
try: try:
await app.handle_exception(self.request, exception, request_middleware) await app.handle_exception(
self.request, exception, request_middleware
)
except Exception as e: except Exception as e:
await app.handle_exception(self.request, e, False) await app.handle_exception(self.request, e, False)
@ -471,7 +481,7 @@ class Http(Stream, metaclass=TouchUpMeta):
if data: if data:
yield data yield data
async def read(self) -> bytes | None: # no cov async def read(self) -> Optional[bytes]: # no cov
"""Read some bytes of request body.""" """Read some bytes of request body."""
# Send a 100-continue if needed # Send a 100-continue if needed

View File

@ -1,12 +1,17 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from ssl import SSLContext from ssl import SSLContext
from typing import ( from typing import (
TYPE_CHECKING, TYPE_CHECKING,
Any, Any,
Callable, Callable,
Dict,
List,
Optional,
Tuple,
Union, Union,
cast, cast,
) )
@ -27,6 +32,7 @@ from sanic.log import Colors, logger
from sanic.models.protocol_types import TransportProtocol from sanic.models.protocol_types import TransportProtocol
from sanic.models.server_types import ConnInfo from sanic.models.server_types import ConnInfo
try: try:
from aioquic.h0.connection import H0_ALPN, H0Connection from aioquic.h0.connection import H0_ALPN, H0Connection
from aioquic.h3.connection import H3_ALPN, H3Connection from aioquic.h3.connection import H3_ALPN, H3Connection
@ -65,7 +71,10 @@ class HTTP3Transport(TransportProtocol):
return self._protocol return self._protocol
def get_extra_info(self, info: str, default: Any = None) -> Any: def get_extra_info(self, info: str, default: Any = None) -> Any:
if info in ("socket", "sockname", "peername") and self._protocol._transport: if (
info in ("socket", "sockname", "peername")
and self._protocol._transport
):
return self._protocol._transport.get_extra_info(info, default) return self._protocol._transport.get_extra_info(info, default)
elif info == "network_paths": elif info == "network_paths":
return self._protocol._quic._network_paths return self._protocol._quic._network_paths
@ -100,18 +109,19 @@ class HTTPReceiver(Receiver, Stream):
self.request_body = None self.request_body = None
self.stage = Stage.IDLE self.stage = Stage.IDLE
self.headers_sent = False self.headers_sent = False
self.response: BaseHTTPResponse | None = None self.response: Optional[BaseHTTPResponse] = None
self.request_max_size = self.protocol.request_max_size self.request_max_size = self.protocol.request_max_size
self.request_bytes = 0 self.request_bytes = 0
async def run(self, exception: Exception | None = None): async def run(self, exception: Optional[Exception] = None):
"""Handle the request and response cycle.""" """Handle the request and response cycle."""
self.stage = Stage.HANDLER self.stage = Stage.HANDLER
self.head_only = self.request.method.upper() == "HEAD" self.head_only = self.request.method.upper() == "HEAD"
if exception: if exception:
logger.info( # no cov logger.info( # no cov
f"{Colors.BLUE}[exception]: " f"{Colors.RED}{exception}{Colors.END}", f"{Colors.BLUE}[exception]: "
f"{Colors.RED}{exception}{Colors.END}",
exc_info=True, exc_info=True,
extra={"verbosity": 1}, extra={"verbosity": 1},
) )
@ -136,13 +146,17 @@ class HTTPReceiver(Receiver, Stream):
await app.handle_exception(self.request, exception) await app.handle_exception(self.request, exception)
def _prepare_headers(self, response: BaseHTTPResponse) -> list[tuple[bytes, bytes]]: def _prepare_headers(
self, response: BaseHTTPResponse
) -> List[Tuple[bytes, bytes]]:
size = len(response.body) if response.body else 0 size = len(response.body) if response.body else 0
headers = response.headers headers = response.headers
status = response.status status = response.status
if not has_message_body(status) and ( if not has_message_body(status) and (
size or "content-length" in headers or "transfer-encoding" in headers size
or "content-length" in headers
or "transfer-encoding" in headers
): ):
headers.pop("content-length", None) headers.pop("content-length", None)
headers.pop("transfer-encoding", None) headers.pop("transfer-encoding", None)
@ -235,7 +249,11 @@ class HTTPReceiver(Receiver, Stream):
): ):
size = len(data) size = len(data)
if end_stream: if end_stream:
data = b"%x\r\n%b\r\n0\r\n\r\n" % (size, data) if size else b"0\r\n\r\n" data = (
b"%x\r\n%b\r\n0\r\n\r\n" % (size, data)
if size
else b"0\r\n\r\n"
)
elif size: elif size:
data = b"%x\r\n%b\r\n" % (size, data) data = b"%x\r\n%b\r\n" % (size, data)
@ -286,7 +304,7 @@ class Http3:
) -> None: ) -> None:
self.protocol = protocol self.protocol = protocol
self.transmit = transmit self.transmit = transmit
self.receivers: dict[int, Receiver] = {} self.receivers: Dict[int, Receiver] = {}
def http_event_received(self, event: H3Event) -> None: def http_event_received(self, event: H3Event) -> None:
logger.debug( # no cov logger.debug( # no cov
@ -312,8 +330,11 @@ class Http3:
extra={"verbosity": 2}, extra={"verbosity": 2},
) )
def get_or_make_receiver(self, event: H3Event) -> tuple[Receiver, bool]: def get_or_make_receiver(self, event: H3Event) -> Tuple[Receiver, bool]:
if isinstance(event, HeadersReceived) and event.stream_id not in self.receivers: if (
isinstance(event, HeadersReceived)
and event.stream_id not in self.receivers
):
request = self._make_request(event) request = self._make_request(event)
receiver = HTTPReceiver(self.transmit, self.protocol, request) receiver = HTTPReceiver(self.transmit, self.protocol, request)
request.stream = receiver request.stream = receiver
@ -336,7 +357,9 @@ class Http3:
) )
) )
except UnicodeDecodeError: except UnicodeDecodeError:
raise BadRequest("Header names may only contain US-ASCII characters.") raise BadRequest(
"Header names may only contain US-ASCII characters."
)
method = headers[":method"] method = headers[":method"]
path = headers[":path"] path = headers[":path"]
scheme = headers.pop(":scheme", "") scheme = headers.pop(":scheme", "")
@ -373,16 +396,18 @@ class SessionTicketStore:
""" """
def __init__(self) -> None: def __init__(self) -> None:
self.tickets: dict[bytes, SessionTicket] = {} self.tickets: Dict[bytes, SessionTicket] = {}
def add(self, ticket: SessionTicket) -> None: def add(self, ticket: SessionTicket) -> None:
self.tickets[ticket.ticket] = ticket self.tickets[ticket.ticket] = ticket
def pop(self, label: bytes) -> SessionTicket | None: def pop(self, label: bytes) -> Optional[SessionTicket]:
return self.tickets.pop(label, None) return self.tickets.pop(label, None)
def get_config(app: Sanic, ssl: SanicSSLContext | CertSelector | SSLContext): def get_config(
app: Sanic, ssl: Union[SanicSSLContext, CertSelector, SSLContext]
):
# TODO: # TODO:
# - proper selection needed if service with multiple certs insted of # - proper selection needed if service with multiple certs insted of
# just taking the first # just taking the first
@ -405,6 +430,8 @@ def get_config(app: Sanic, ssl: SanicSSLContext | CertSelector | SSLContext):
) )
password = app.config.TLS_CERT_PASSWORD or None password = app.config.TLS_CERT_PASSWORD or None
config.load_cert_chain(ssl.sanic["cert"], ssl.sanic["key"], password=password) config.load_cert_chain(
ssl.sanic["cert"], ssl.sanic["key"], password=password
)
return config return config

View File

@ -1,9 +1,10 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Optional, Tuple, Union
from sanic.http.constants import Stage from sanic.http.constants import Stage
if TYPE_CHECKING: if TYPE_CHECKING:
from sanic.response import BaseHTTPResponse from sanic.response import BaseHTTPResponse
from sanic.server.protocols.http_protocol import HttpProtocol from sanic.server.protocols.http_protocol import HttpProtocol
@ -11,14 +12,16 @@ if TYPE_CHECKING:
class Stream: class Stream:
stage: Stage stage: Stage
response: BaseHTTPResponse | None response: Optional[BaseHTTPResponse]
protocol: HttpProtocol protocol: HttpProtocol
url: str | None url: Optional[str]
request_body: bytes | None request_body: Optional[bytes]
request_max_size: int | float request_max_size: Union[int, float]
__touchup__: tuple[str, ...] = () __touchup__: Tuple[str, ...] = tuple()
__slots__ = ("request_max_size",) __slots__ = ("request_max_size",)
def respond(self, response: BaseHTTPResponse) -> BaseHTTPResponse: # no cov def respond(
self, response: BaseHTTPResponse
) -> BaseHTTPResponse: # no cov
raise NotImplementedError("Not implemented") raise NotImplementedError("Not implemented")

View File

@ -1,4 +1,5 @@
from .context import process_to_context from .context import process_to_context
from .creators import get_ssl_context from .creators import get_ssl_context
__all__ = ("get_ssl_context", "process_to_context") __all__ = ("get_ssl_context", "process_to_context")

View File

@ -2,10 +2,12 @@ from __future__ import annotations
import os import os
import ssl import ssl
from typing import Any, Iterable
from typing import Any, Dict, Iterable, Optional, Union
from sanic.log import logger from sanic.log import logger
# Only allow secure ciphers, notably leaving out AES-CBC mode # Only allow secure ciphers, notably leaving out AES-CBC mode
# OpenSSL chooses ECDSA or RSA depending on the cert in use # OpenSSL chooses ECDSA or RSA depending on the cert in use
CIPHERS_TLS12 = [ CIPHERS_TLS12 = [
@ -17,14 +19,11 @@ CIPHERS_TLS12 = [
"ECDHE-RSA-AES128-GCM-SHA256", "ECDHE-RSA-AES128-GCM-SHA256",
] ]
TlsDef = None | ssl.SSLContext | dict[str, Any] | str
TlsDefs = TlsDef | list[TlsDef] | tuple[TlsDef, ...]
def create_context( def create_context(
certfile: str | None = None, certfile: Optional[str] = None,
keyfile: str | None = None, keyfile: Optional[str] = None,
password: str | None = None, password: Optional[str] = None,
purpose: ssl.Purpose = ssl.Purpose.CLIENT_AUTH, purpose: ssl.Purpose = ssl.Purpose.CLIENT_AUTH,
) -> ssl.SSLContext: ) -> ssl.SSLContext:
"""Create a context with secure crypto and HTTP/1.1 in protocols.""" """Create a context with secure crypto and HTTP/1.1 in protocols."""
@ -39,7 +38,9 @@ def create_context(
return context return context
def shorthand_to_ctx(ctxdef: TlsDef) -> ssl.SSLContext | None: def shorthand_to_ctx(
ctxdef: Union[None, ssl.SSLContext, dict, str]
) -> Optional[ssl.SSLContext]:
"""Convert an ssl argument shorthand to an SSLContext object.""" """Convert an ssl argument shorthand to an SSLContext object."""
if ctxdef is None or isinstance(ctxdef, ssl.SSLContext): if ctxdef is None or isinstance(ctxdef, ssl.SSLContext):
return ctxdef return ctxdef
@ -53,7 +54,9 @@ def shorthand_to_ctx(ctxdef: TlsDef) -> ssl.SSLContext | None:
) )
def process_to_context(ssldef: TlsDefs) -> ssl.SSLContext | None: def process_to_context(
ssldef: Union[None, ssl.SSLContext, dict, str, list, tuple]
) -> Optional[ssl.SSLContext]:
"""Process app.run ssl argument from easy formats to full SSLContext.""" """Process app.run ssl argument from easy formats to full SSLContext."""
return ( return (
CertSelector(map(shorthand_to_ctx, ssldef)) CertSelector(map(shorthand_to_ctx, ssldef))
@ -68,9 +71,13 @@ def load_cert_dir(p: str) -> ssl.SSLContext:
keyfile = os.path.join(p, "privkey.pem") keyfile = os.path.join(p, "privkey.pem")
certfile = os.path.join(p, "fullchain.pem") certfile = os.path.join(p, "fullchain.pem")
if not os.access(keyfile, os.R_OK): if not os.access(keyfile, os.R_OK):
raise ValueError(f"Certificate not found or permission denied {keyfile}") raise ValueError(
f"Certificate not found or permission denied {keyfile}"
)
if not os.access(certfile, os.R_OK): if not os.access(certfile, os.R_OK):
raise ValueError(f"Certificate not found or permission denied {certfile}") raise ValueError(
f"Certificate not found or permission denied {certfile}"
)
return CertSimple(certfile, keyfile) return CertSimple(certfile, keyfile)
@ -82,7 +89,9 @@ def find_cert(self: CertSelector, server_name: str):
if not server_name: if not server_name:
if self.sanic_fallback: if self.sanic_fallback:
return self.sanic_fallback return self.sanic_fallback
raise ValueError("The client provided no SNI to match for certificate.") raise ValueError(
"The client provided no SNI to match for certificate."
)
for ctx in self.sanic_select: for ctx in self.sanic_select:
if match_hostname(ctx, server_name): if match_hostname(ctx, server_name):
return ctx return ctx
@ -91,7 +100,9 @@ def find_cert(self: CertSelector, server_name: str):
raise ValueError(f"No certificate found matching hostname {server_name!r}") raise ValueError(f"No certificate found matching hostname {server_name!r}")
def match_hostname(ctx: ssl.SSLContext | CertSelector, hostname: str) -> bool: def match_hostname(
ctx: Union[ssl.SSLContext, CertSelector], hostname: str
) -> bool:
"""Match names from CertSelector against a received hostname.""" """Match names from CertSelector against a received hostname."""
# Local certs are considered trusted, so this can be less pedantic # Local certs are considered trusted, so this can be less pedantic
# and thus faster than the deprecated ssl.match_hostname function is. # and thus faster than the deprecated ssl.match_hostname function is.
@ -108,7 +119,7 @@ def match_hostname(ctx: ssl.SSLContext | CertSelector, hostname: str) -> bool:
def selector_sni_callback( def selector_sni_callback(
sslobj: ssl.SSLObject, server_name: str, ctx: CertSelector sslobj: ssl.SSLObject, server_name: str, ctx: CertSelector
) -> int | None: ) -> Optional[int]:
"""Select a certificate matching the SNI.""" """Select a certificate matching the SNI."""
# Call server_name_callback to store the SNI on sslobj # Call server_name_callback to store the SNI on sslobj
server_name_callback(sslobj, server_name, ctx) server_name_callback(sslobj, server_name, ctx)
@ -131,7 +142,7 @@ def server_name_callback(
class SanicSSLContext(ssl.SSLContext): class SanicSSLContext(ssl.SSLContext):
sanic: dict[str, os.PathLike] sanic: Dict[str, os.PathLike]
@classmethod @classmethod
def create_from_ssl_context(cls, context: ssl.SSLContext): def create_from_ssl_context(cls, context: ssl.SSLContext):
@ -142,7 +153,7 @@ class SanicSSLContext(ssl.SSLContext):
class CertSimple(SanicSSLContext): class CertSimple(SanicSSLContext):
"""A wrapper for creating SSLContext with a sanic attribute.""" """A wrapper for creating SSLContext with a sanic attribute."""
sanic: dict[str, Any] sanic: Dict[str, Any]
def __new__(cls, cert, key, **kw): def __new__(cls, cert, key, **kw):
# try common aliases, rename to cert/key # try common aliases, rename to cert/key
@ -155,7 +166,9 @@ class CertSimple(SanicSSLContext):
if "names" not in kw: if "names" not in kw:
cert = ssl._ssl._test_decode_cert(certfile) # type: ignore cert = ssl._ssl._test_decode_cert(certfile) # type: ignore
kw["names"] = [ kw["names"] = [
name for t, name in cert["subjectAltName"] if t in ["DNS", "IP Address"] name
for t, name in cert["subjectAltName"]
if t in ["DNS", "IP Address"]
] ]
subject = {k: v for item in cert["subject"] for k, v in item} subject = {k: v for item in cert["subject"] for k, v in item}
self = create_context(certfile, keyfile, password) self = create_context(certfile, keyfile, password)
@ -177,7 +190,7 @@ class CertSelector(ssl.SSLContext):
def __new__(cls, ctxs): def __new__(cls, ctxs):
return super().__new__(cls) return super().__new__(cls)
def __init__(self, ctxs: Iterable[ssl.SSLContext | None]): def __init__(self, ctxs: Iterable[Optional[ssl.SSLContext]]):
super().__init__() super().__init__()
self.sni_callback = selector_sni_callback # type: ignore self.sni_callback = selector_sni_callback # type: ignore
self.sanic_select = [] self.sanic_select = []
@ -192,5 +205,7 @@ class CertSelector(ssl.SSLContext):
if i == 0: if i == 0:
self.sanic_fallback = ctx self.sanic_fallback = ctx
if not all_names: if not all_names:
raise ValueError("No certificates with SubjectAlternativeNames found.") raise ValueError(
"No certificates with SubjectAlternativeNames found."
)
logger.info(f"Certificate vhosts: {', '.join(all_names)}") logger.info(f"Certificate vhosts: {', '.join(all_names)}")

View File

@ -3,12 +3,13 @@ from __future__ import annotations
import ssl import ssl
import subprocess import subprocess
import sys import sys
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from contextlib import suppress from contextlib import suppress
from pathlib import Path from pathlib import Path
from tempfile import mkdtemp from tempfile import mkdtemp
from types import ModuleType from types import ModuleType
from typing import TYPE_CHECKING, cast from typing import TYPE_CHECKING, Optional, Tuple, Type, Union, cast
from sanic.application.constants import Mode from sanic.application.constants import Mode
from sanic.application.spinner import loading from sanic.application.spinner import loading
@ -21,6 +22,7 @@ from sanic.exceptions import SanicException
from sanic.helpers import Default from sanic.helpers import Default
from sanic.http.tls.context import CertSimple, SanicSSLContext from sanic.http.tls.context import CertSimple, SanicSSLContext
try: try:
import trustme import trustme
@ -45,7 +47,7 @@ CIPHERS_TLS12 = [
] ]
def _make_path(maybe_path: Path | str, tmpdir: Path | None) -> Path: def _make_path(maybe_path: Union[Path, str], tmpdir: Optional[Path]) -> Path:
if isinstance(maybe_path, Path): if isinstance(maybe_path, Path):
return maybe_path return maybe_path
else: else:
@ -58,7 +60,9 @@ def _make_path(maybe_path: Path | str, tmpdir: Path | None) -> Path:
return path return path
def get_ssl_context(app: Sanic, ssl: ssl.SSLContext | None) -> ssl.SSLContext: def get_ssl_context(
app: Sanic, ssl: Optional[ssl.SSLContext]
) -> ssl.SSLContext:
if ssl: if ssl:
return ssl return ssl
@ -92,8 +96,16 @@ class CertCreator(ABC):
if isinstance(self.key, Default) or isinstance(self.cert, Default): if isinstance(self.key, Default) or isinstance(self.cert, Default):
self.tmpdir = Path(mkdtemp()) self.tmpdir = Path(mkdtemp())
key = DEFAULT_LOCAL_TLS_KEY if isinstance(self.key, Default) else self.key key = (
cert = DEFAULT_LOCAL_TLS_CERT if isinstance(self.cert, Default) else self.cert DEFAULT_LOCAL_TLS_KEY
if isinstance(self.key, Default)
else self.key
)
cert = (
DEFAULT_LOCAL_TLS_CERT
if isinstance(self.cert, Default)
else self.cert
)
self.key_path = _make_path(key, self.tmpdir) self.key_path = _make_path(key, self.tmpdir)
self.cert_path = _make_path(cert, self.tmpdir) self.cert_path = _make_path(cert, self.tmpdir)
@ -114,9 +126,11 @@ class CertCreator(ABC):
local_tls_key, local_tls_key,
local_tls_cert, local_tls_cert,
) -> CertCreator: ) -> CertCreator:
creator: CertCreator | None = None creator: Optional[CertCreator] = None
cert_creator_options: tuple[tuple[type[CertCreator], LocalCertCreator], ...] = ( cert_creator_options: Tuple[
Tuple[Type[CertCreator], LocalCertCreator], ...
] = (
(MkcertCreator, LocalCertCreator.MKCERT), (MkcertCreator, LocalCertCreator.MKCERT),
(TrustmeCreator, LocalCertCreator.TRUSTME), (TrustmeCreator, LocalCertCreator.TRUSTME),
) )
@ -146,8 +160,8 @@ class CertCreator(ABC):
@staticmethod @staticmethod
def _try_select( def _try_select(
app: Sanic, app: Sanic,
creator: CertCreator | None, creator: Optional[CertCreator],
creator_class: type[CertCreator], creator_class: Type[CertCreator],
creator_requirement: LocalCertCreator, creator_requirement: LocalCertCreator,
creator_requested: LocalCertCreator, creator_requested: LocalCertCreator,
local_tls_key, local_tls_key,

View File

@ -1,11 +1,13 @@
import logging import logging
import sys import sys
from enum import Enum from enum import Enum
from typing import TYPE_CHECKING, Any, Dict from typing import TYPE_CHECKING, Any, Dict
from warnings import warn from warnings import warn
from sanic.helpers import is_atty from sanic.helpers import is_atty
# Python 3.11 changed the way Enum formatting works for mixed-in types. # Python 3.11 changed the way Enum formatting works for mixed-in types.
if sys.version_info < (3, 11, 0): if sys.version_info < (3, 11, 0):
@ -17,10 +19,10 @@ else:
from enum import StrEnum from enum import StrEnum
LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = { # no cov LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict( # no cov
"version": 1, version=1,
"disable_existing_loggers": False, disable_existing_loggers=False,
"loggers": { loggers={
"sanic.root": {"level": "INFO", "handlers": ["console"]}, "sanic.root": {"level": "INFO", "handlers": ["console"]},
"sanic.error": { "sanic.error": {
"level": "INFO", "level": "INFO",
@ -41,7 +43,7 @@ LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = { # no cov
"qualname": "sanic.server", "qualname": "sanic.server",
}, },
}, },
"handlers": { handlers={
"console": { "console": {
"class": "logging.StreamHandler", "class": "logging.StreamHandler",
"formatter": "generic", "formatter": "generic",
@ -58,7 +60,7 @@ LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = { # no cov
"stream": sys.stdout, "stream": sys.stdout,
}, },
}, },
"formatters": { formatters={
"generic": { "generic": {
"format": "%(asctime)s [%(process)s] [%(levelname)s] %(message)s", "format": "%(asctime)s [%(process)s] [%(levelname)s] %(message)s",
"datefmt": "[%Y-%m-%d %H:%M:%S %z]", "datefmt": "[%Y-%m-%d %H:%M:%S %z]",
@ -66,12 +68,12 @@ LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = { # no cov
}, },
"access": { "access": {
"format": "%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: " "format": "%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: "
"%(request)s %(message)s %(status)s %(byte)s", + "%(request)s %(message)s %(status)s %(byte)s",
"datefmt": "[%Y-%m-%d %H:%M:%S %z]", "datefmt": "[%Y-%m-%d %H:%M:%S %z]",
"class": "logging.Formatter", "class": "logging.Formatter",
}, },
}, },
} )
""" """
Defult logging configuration Defult logging configuration
""" """

View File

@ -3,7 +3,7 @@ from __future__ import annotations
from collections import deque from collections import deque
from enum import IntEnum, auto from enum import IntEnum, auto
from itertools import count from itertools import count
from typing import Sequence from typing import Deque, Sequence, Union
from sanic.models.handler_types import MiddlewareType from sanic.models.handler_types import MiddlewareType
@ -69,9 +69,9 @@ class Middleware:
@classmethod @classmethod
def convert( def convert(
cls, cls,
*middleware_collections: Sequence[Middleware | MiddlewareType], *middleware_collections: Sequence[Union[Middleware, MiddlewareType]],
location: MiddlewareLocation, location: MiddlewareLocation,
) -> deque[Middleware]: ) -> Deque[Middleware]:
"""Convert middleware collections to a deque of Middleware objects. """Convert middleware collections to a deque of Middleware objects.
Args: Args:

View File

@ -14,7 +14,7 @@ class ExceptionMixin(metaclass=SanicMeta):
def exception( def exception(
self, self,
*exceptions: Union[Type[Exception], List[Type[Exception]]], *exceptions: Union[Type[Exception], List[Type[Exception]]],
apply: bool = True, apply: bool = True
) -> Callable: ) -> Callable:
"""Decorator used to register an exception handler for the current application or blueprint instance. """Decorator used to register an exception handler for the current application or blueprint instance.
@ -79,7 +79,9 @@ class ExceptionMixin(metaclass=SanicMeta):
return decorator return decorator
def all_exceptions(self, handler: Callable[..., Any]) -> Callable[..., Any]: def all_exceptions(
self, handler: Callable[..., Any]
) -> Callable[..., Any]:
"""Enables the process of creating a global exception handler as a convenience. """Enables the process of creating a global exception handler as a convenience.
This following two examples are equivalent: This following two examples are equivalent:

View File

@ -120,12 +120,16 @@ class ListenerMixin(metaclass=SanicMeta):
if callable(listener_or_event): if callable(listener_or_event):
if event_or_none is None: if event_or_none is None:
raise BadRequest("Invalid event registration: Missing event name.") raise BadRequest(
"Invalid event registration: Missing event name."
)
return register_listener(listener_or_event, event_or_none) return register_listener(listener_or_event, event_or_none)
else: else:
return partial(register_listener, event=listener_or_event) return partial(register_listener, event=listener_or_event)
def main_process_start(self, listener: ListenerType[Sanic]) -> ListenerType[Sanic]: def main_process_start(
self, listener: ListenerType[Sanic]
) -> ListenerType[Sanic]:
"""Decorator for registering a listener for the main_process_start event. """Decorator for registering a listener for the main_process_start event.
This event is fired only on the main process and **NOT** on any This event is fired only on the main process and **NOT** on any
@ -147,7 +151,9 @@ class ListenerMixin(metaclass=SanicMeta):
""" # noqa: E501 """ # noqa: E501
return self.listener(listener, "main_process_start") return self.listener(listener, "main_process_start")
def main_process_ready(self, listener: ListenerType[Sanic]) -> ListenerType[Sanic]: def main_process_ready(
self, listener: ListenerType[Sanic]
) -> ListenerType[Sanic]:
"""Decorator for registering a listener for the main_process_ready event. """Decorator for registering a listener for the main_process_ready event.
This event is fired only on the main process and **NOT** on any This event is fired only on the main process and **NOT** on any
@ -170,7 +176,9 @@ class ListenerMixin(metaclass=SanicMeta):
""" # noqa: E501 """ # noqa: E501
return self.listener(listener, "main_process_ready") return self.listener(listener, "main_process_ready")
def main_process_stop(self, listener: ListenerType[Sanic]) -> ListenerType[Sanic]: def main_process_stop(
self, listener: ListenerType[Sanic]
) -> ListenerType[Sanic]:
"""Decorator for registering a listener for the main_process_stop event. """Decorator for registering a listener for the main_process_stop event.
This event is fired only on the main process and **NOT** on any This event is fired only on the main process and **NOT** on any
@ -214,7 +222,9 @@ class ListenerMixin(metaclass=SanicMeta):
""" # noqa: E501 """ # noqa: E501
return self.listener(listener, "reload_process_start") return self.listener(listener, "reload_process_start")
def reload_process_stop(self, listener: ListenerType[Sanic]) -> ListenerType[Sanic]: def reload_process_stop(
self, listener: ListenerType[Sanic]
) -> ListenerType[Sanic]:
"""Decorator for registering a listener for the reload_process_stop event. """Decorator for registering a listener for the reload_process_stop event.
This event is fired only on the reload process and **NOT** on any This event is fired only on the reload process and **NOT** on any
@ -283,7 +293,9 @@ class ListenerMixin(metaclass=SanicMeta):
""" # noqa: E501 """ # noqa: E501
return self.listener(listener, "after_reload_trigger") return self.listener(listener, "after_reload_trigger")
def before_server_start(self, listener: ListenerType[Sanic]) -> ListenerType[Sanic]: def before_server_start(
self, listener: ListenerType[Sanic]
) -> ListenerType[Sanic]:
"""Decorator for registering a listener for the before_server_start event. """Decorator for registering a listener for the before_server_start event.
This event is fired on all worker processes. You should typically This event is fired on all worker processes. You should typically
@ -307,7 +319,9 @@ class ListenerMixin(metaclass=SanicMeta):
""" # noqa: E501 """ # noqa: E501
return self.listener(listener, "before_server_start") return self.listener(listener, "before_server_start")
def after_server_start(self, listener: ListenerType[Sanic]) -> ListenerType[Sanic]: def after_server_start(
self, listener: ListenerType[Sanic]
) -> ListenerType[Sanic]:
"""Decorator for registering a listener for the after_server_start event. """Decorator for registering a listener for the after_server_start event.
This event is fired on all worker processes. You should typically This event is fired on all worker processes. You should typically
@ -335,7 +349,9 @@ class ListenerMixin(metaclass=SanicMeta):
""" # noqa: E501 """ # noqa: E501
return self.listener(listener, "after_server_start") return self.listener(listener, "after_server_start")
def before_server_stop(self, listener: ListenerType[Sanic]) -> ListenerType[Sanic]: def before_server_stop(
self, listener: ListenerType[Sanic]
) -> ListenerType[Sanic]:
"""Decorator for registering a listener for the before_server_stop event. """Decorator for registering a listener for the before_server_stop event.
This event is fired on all worker processes. This event is fired This event is fired on all worker processes. This event is fired
@ -360,7 +376,9 @@ class ListenerMixin(metaclass=SanicMeta):
""" # noqa: E501 """ # noqa: E501
return self.listener(listener, "before_server_stop") return self.listener(listener, "before_server_stop")
def after_server_stop(self, listener: ListenerType[Sanic]) -> ListenerType[Sanic]: def after_server_stop(
self, listener: ListenerType[Sanic]
) -> ListenerType[Sanic]:
"""Decorator for registering a listener for the after_server_stop event. """Decorator for registering a listener for the after_server_stop event.
This event is fired on all worker processes. This event is fired This event is fired on all worker processes. This event is fired

View File

@ -25,7 +25,7 @@ class MiddlewareMixin(metaclass=SanicMeta):
attach_to: str = "request", attach_to: str = "request",
apply: bool = True, apply: bool = True,
*, *,
priority: int = 0, priority: int = 0
) -> MiddlewareType: ) -> MiddlewareType:
... ...
@ -36,7 +36,7 @@ class MiddlewareMixin(metaclass=SanicMeta):
attach_to: str = "request", attach_to: str = "request",
apply: bool = True, apply: bool = True,
*, *,
priority: int = 0, priority: int = 0
) -> Callable[[MiddlewareType], MiddlewareType]: ) -> Callable[[MiddlewareType], MiddlewareType]:
... ...
@ -46,7 +46,7 @@ class MiddlewareMixin(metaclass=SanicMeta):
attach_to: str = "request", attach_to: str = "request",
apply: bool = True, apply: bool = True,
*, *,
priority: int = 0, priority: int = 0
) -> Union[MiddlewareType, Callable[[MiddlewareType], MiddlewareType]]: ) -> Union[MiddlewareType, Callable[[MiddlewareType], MiddlewareType]]:
"""Decorator for registering middleware. """Decorator for registering middleware.
@ -99,9 +99,13 @@ class MiddlewareMixin(metaclass=SanicMeta):
# Detect which way this was called, @middleware or @middleware('AT') # Detect which way this was called, @middleware or @middleware('AT')
if callable(middleware_or_request): if callable(middleware_or_request):
return register_middleware(middleware_or_request, attach_to=attach_to) return register_middleware(
middleware_or_request, attach_to=attach_to
)
else: else:
return partial(register_middleware, attach_to=middleware_or_request) return partial(
register_middleware, attach_to=middleware_or_request
)
def on_request(self, middleware=None, *, priority=0) -> MiddlewareType: def on_request(self, middleware=None, *, priority=0) -> MiddlewareType:
"""Register a middleware to be called before a request is handled. """Register a middleware to be called before a request is handled.
@ -153,7 +157,9 @@ class MiddlewareMixin(metaclass=SanicMeta):
if callable(middleware): if callable(middleware):
return self.middleware(middleware, "response", priority=priority) return self.middleware(middleware, "response", priority=priority)
else: else:
return partial(self.middleware, attach_to="response", priority=priority) return partial(
self.middleware, attach_to="response", priority=priority
)
def finalize_middleware(self) -> None: def finalize_middleware(self) -> None:
"""Finalize the middleware configuration for the Sanic application. """Finalize the middleware configuration for the Sanic application.

View File

@ -25,7 +25,10 @@ from sanic.models.futures import FutureRoute, FutureStatic
from sanic.models.handler_types import RouteHandler from sanic.models.handler_types import RouteHandler
from sanic.types import HashableDict from sanic.types import HashableDict
RouteWrapper = Callable[[RouteHandler], Union[RouteHandler, Tuple[Route, RouteHandler]]]
RouteWrapper = Callable[
[RouteHandler], Union[RouteHandler, Tuple[Route, RouteHandler]]
]
class RouteMixin(BaseMixin, metaclass=SanicMeta): class RouteMixin(BaseMixin, metaclass=SanicMeta):
@ -812,5 +815,7 @@ class RouteMixin(BaseMixin, metaclass=SanicMeta):
} }
if raw: if raw:
unexpected_arguments = ", ".join(raw.keys()) unexpected_arguments = ", ".join(raw.keys())
raise TypeError(f"Unexpected keyword arguments: {unexpected_arguments}") raise TypeError(
f"Unexpected keyword arguments: {unexpected_arguments}"
)
return HashableDict(ctx_kwargs) return HashableDict(ctx_kwargs)

View File

@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
from enum import Enum from enum import Enum
from typing import Any, Callable, Coroutine from typing import Any, Callable, Coroutine, Dict, Optional, Set, Union
from sanic.base.meta import SanicMeta from sanic.base.meta import SanicMeta
from sanic.models.futures import FutureSignal from sanic.models.futures import FutureSignal
@ -12,17 +12,17 @@ from sanic.types import HashableDict
class SignalMixin(metaclass=SanicMeta): class SignalMixin(metaclass=SanicMeta):
def __init__(self, *args, **kwargs) -> None: def __init__(self, *args, **kwargs) -> None:
self._future_signals: set[FutureSignal] = set() self._future_signals: Set[FutureSignal] = set()
def _apply_signal(self, signal: FutureSignal) -> Signal: def _apply_signal(self, signal: FutureSignal) -> Signal:
raise NotImplementedError # noqa raise NotImplementedError # noqa
def signal( def signal(
self, self,
event: str | Enum, event: Union[str, Enum],
*, *,
apply: bool = True, apply: bool = True,
condition: dict[str, Any] | None = None, condition: Optional[Dict[str, Any]] = None,
exclusive: bool = True, exclusive: bool = True,
) -> Callable[[SignalHandler], SignalHandler]: ) -> Callable[[SignalHandler], SignalHandler]:
""" """
@ -64,9 +64,9 @@ class SignalMixin(metaclass=SanicMeta):
def add_signal( def add_signal(
self, self,
handler: Callable[..., Any] | None, handler: Optional[Callable[..., Any]],
event: str, event: str,
condition: dict[str, Any] | None = None, condition: Optional[Dict[str, Any]] = None,
exclusive: bool = True, exclusive: bool = True,
) -> Callable[..., Any]: ) -> Callable[..., Any]:
"""Registers a signal handler for a specific event. """Registers a signal handler for a specific event.
@ -92,7 +92,9 @@ class SignalMixin(metaclass=SanicMeta):
... ...
handler = noop handler = noop
self.signal(event=event, condition=condition, exclusive=exclusive)(handler) self.signal(event=event, condition=condition, exclusive=exclusive)(
handler
)
return handler return handler
def event(self, event: str): def event(self, event: str):

View File

@ -2,6 +2,8 @@ from __future__ import annotations
import os import os
import platform import platform
import sys
from asyncio import ( from asyncio import (
AbstractEventLoop, AbstractEventLoop,
CancelledError, CancelledError,
@ -30,7 +32,13 @@ from typing import (
Any, Any,
Callable, Callable,
ClassVar, ClassVar,
Dict,
List,
Mapping, Mapping,
Optional,
Set,
Tuple,
Type,
Union, Union,
cast, cast,
) )
@ -63,6 +71,7 @@ from sanic.worker.multiplexer import WorkerMultiplexer
from sanic.worker.reloader import Reloader from sanic.worker.reloader import Reloader
from sanic.worker.serve import worker_serve from sanic.worker.serve import worker_serve
if TYPE_CHECKING: if TYPE_CHECKING:
from sanic import Sanic from sanic import Sanic
from sanic.application.state import ApplicationState from sanic.application.state import ApplicationState
@ -70,17 +79,20 @@ if TYPE_CHECKING:
SANIC_PACKAGES = ("sanic-routing", "sanic-testing", "sanic-ext") SANIC_PACKAGES = ("sanic-routing", "sanic-testing", "sanic-ext")
from typing import Literal if sys.version_info < (3, 8): # no cov
HTTPVersion = Union[HTTP, int]
else: # no cov
from typing import Literal
HTTPVersion = Union[HTTP, Literal[1], Literal[3]] HTTPVersion = Union[HTTP, Literal[1], Literal[3]]
class StartupMixin(metaclass=SanicMeta): class StartupMixin(metaclass=SanicMeta):
_app_registry: ClassVar[dict[str, Sanic]] _app_registry: ClassVar[Dict[str, Sanic]]
asgi: bool asgi: bool
config: Config config: Config
listeners: dict[str, list[ListenerType[Any]]] listeners: Dict[str, List[ListenerType[Any]]]
state: ApplicationState state: ApplicationState
websocket_enabled: bool websocket_enabled: bool
multiplexer: WorkerMultiplexer multiplexer: WorkerMultiplexer
@ -100,7 +112,8 @@ class StartupMixin(metaclass=SanicMeta):
""" """
if not self.asgi: if not self.asgi:
if self.config.USE_UVLOOP is True or ( if self.config.USE_UVLOOP is True or (
isinstance(self.config.USE_UVLOOP, Default) and not OS_IS_WINDOWS isinstance(self.config.USE_UVLOOP, Default)
and not OS_IS_WINDOWS
): ):
try_use_uvloop() try_use_uvloop()
elif OS_IS_WINDOWS: elif OS_IS_WINDOWS:
@ -146,28 +159,28 @@ class StartupMixin(metaclass=SanicMeta):
def run( def run(
self, self,
host: str | None = None, host: Optional[str] = None,
port: int | None = None, port: Optional[int] = None,
*, *,
dev: bool = False, dev: bool = False,
debug: bool = False, debug: bool = False,
auto_reload: bool | None = None, auto_reload: Optional[bool] = None,
version: HTTPVersion = HTTP.VERSION_1, version: HTTPVersion = HTTP.VERSION_1,
ssl: None | SSLContext | dict | str | list | tuple = None, ssl: Union[None, SSLContext, dict, str, list, tuple] = None,
sock: socket | None = None, sock: Optional[socket] = None,
workers: int = 1, workers: int = 1,
protocol: type[Protocol] | None = None, protocol: Optional[Type[Protocol]] = None,
backlog: int = 100, backlog: int = 100,
register_sys_signals: bool = True, register_sys_signals: bool = True,
access_log: bool | None = None, access_log: Optional[bool] = None,
unix: str | None = None, unix: Optional[str] = None,
loop: AbstractEventLoop | None = None, loop: Optional[AbstractEventLoop] = None,
reload_dir: list[str] | str | None = None, reload_dir: Optional[Union[List[str], str]] = None,
noisy_exceptions: bool | None = None, noisy_exceptions: Optional[bool] = None,
motd: bool = True, motd: bool = True,
fast: bool = False, fast: bool = False,
verbosity: int = 0, verbosity: int = 0,
motd_display: dict[str, str] | None = None, motd_display: Optional[Dict[str, str]] = None,
auto_tls: bool = False, auto_tls: bool = False,
single_process: bool = False, single_process: bool = False,
) -> None: ) -> None:
@ -276,28 +289,28 @@ class StartupMixin(metaclass=SanicMeta):
def prepare( def prepare(
self, self,
host: str | None = None, host: Optional[str] = None,
port: int | None = None, port: Optional[int] = None,
*, *,
dev: bool = False, dev: bool = False,
debug: bool = False, debug: bool = False,
auto_reload: bool | None = None, auto_reload: Optional[bool] = None,
version: HTTPVersion = HTTP.VERSION_1, version: HTTPVersion = HTTP.VERSION_1,
ssl: None | SSLContext | dict | str | list | tuple = None, ssl: Union[None, SSLContext, dict, str, list, tuple] = None,
sock: socket | None = None, sock: Optional[socket] = None,
workers: int = 1, workers: int = 1,
protocol: type[Protocol] | None = None, protocol: Optional[Type[Protocol]] = None,
backlog: int = 100, backlog: int = 100,
register_sys_signals: bool = True, register_sys_signals: bool = True,
access_log: bool | None = None, access_log: Optional[bool] = None,
unix: str | None = None, unix: Optional[str] = None,
loop: AbstractEventLoop | None = None, loop: Optional[AbstractEventLoop] = None,
reload_dir: list[str] | str | None = None, reload_dir: Optional[Union[List[str], str]] = None,
noisy_exceptions: bool | None = None, noisy_exceptions: Optional[bool] = None,
motd: bool = True, motd: bool = True,
fast: bool = False, fast: bool = False,
verbosity: int = 0, verbosity: int = 0,
motd_display: dict[str, str] | None = None, motd_display: Optional[Dict[str, str]] = None,
coffee: bool = False, coffee: bool = False,
auto_tls: bool = False, auto_tls: bool = False,
single_process: bool = False, single_process: bool = False,
@ -372,7 +385,8 @@ class StartupMixin(metaclass=SanicMeta):
if single_process and (fast or (workers > 1) or auto_reload): if single_process and (fast or (workers > 1) or auto_reload):
raise RuntimeError( raise RuntimeError(
"Single process cannot be run with multiple workers " "or auto-reload" "Single process cannot be run with multiple workers "
"or auto-reload"
) )
if register_sys_signals is False and not single_process: if register_sys_signals is False and not single_process:
@ -391,7 +405,9 @@ class StartupMixin(metaclass=SanicMeta):
for directory in reload_dir: for directory in reload_dir:
direc = Path(directory) direc = Path(directory)
if not direc.is_dir(): if not direc.is_dir():
logger.warning(f"Directory {directory} could not be located") logger.warning(
f"Directory {directory} could not be located"
)
self.state.reload_dirs.add(Path(directory)) self.state.reload_dirs.add(Path(directory))
if loop is not None: if loop is not None:
@ -406,7 +422,9 @@ class StartupMixin(metaclass=SanicMeta):
host, port = self.get_address(host, port, version, auto_tls) host, port = self.get_address(host, port, version, auto_tls)
if protocol is None: if protocol is None:
protocol = WebSocketProtocol if self.websocket_enabled else HttpProtocol protocol = (
WebSocketProtocol if self.websocket_enabled else HttpProtocol
)
# Set explicitly passed configuration values # Set explicitly passed configuration values
for attribute, value in { for attribute, value in {
@ -442,7 +460,9 @@ class StartupMixin(metaclass=SanicMeta):
register_sys_signals=register_sys_signals, register_sys_signals=register_sys_signals,
auto_tls=auto_tls, auto_tls=auto_tls,
) )
self.state.server_info.append(ApplicationServerInfo(settings=server_settings)) self.state.server_info.append(
ApplicationServerInfo(settings=server_settings)
)
# if self.config.USE_UVLOOP is True or ( # if self.config.USE_UVLOOP is True or (
# self.config.USE_UVLOOP is _default and not OS_IS_WINDOWS # self.config.USE_UVLOOP is _default and not OS_IS_WINDOWS
@ -451,20 +471,20 @@ class StartupMixin(metaclass=SanicMeta):
async def create_server( async def create_server(
self, self,
host: str | None = None, host: Optional[str] = None,
port: int | None = None, port: Optional[int] = None,
*, *,
debug: bool = False, debug: bool = False,
ssl: None | SSLContext | dict | str | list | tuple = None, ssl: Union[None, SSLContext, dict, str, list, tuple] = None,
sock: socket | None = None, sock: Optional[socket] = None,
protocol: type[Protocol] | None = None, protocol: Optional[Type[Protocol]] = None,
backlog: int = 100, backlog: int = 100,
access_log: bool | None = None, access_log: Optional[bool] = None,
unix: str | None = None, unix: Optional[str] = None,
return_asyncio_server: bool = True, return_asyncio_server: bool = True,
asyncio_server_kwargs: dict[str, Any] | None = None, asyncio_server_kwargs: Optional[Dict[str, Any]] = None,
noisy_exceptions: bool | None = None, noisy_exceptions: Optional[bool] = None,
) -> AsyncioServer | None: ) -> Optional[AsyncioServer]:
""" """
Low level API for creating a Sanic Server instance. Low level API for creating a Sanic Server instance.
@ -538,7 +558,9 @@ class StartupMixin(metaclass=SanicMeta):
host, port = host, port = self.get_address(host, port) host, port = host, port = self.get_address(host, port)
if protocol is None: if protocol is None:
protocol = WebSocketProtocol if self.websocket_enabled else HttpProtocol protocol = (
WebSocketProtocol if self.websocket_enabled else HttpProtocol
)
# Set explicitly passed configuration values # Set explicitly passed configuration values
for attribute, value in { for attribute, value in {
@ -615,21 +637,21 @@ class StartupMixin(metaclass=SanicMeta):
def _helper( def _helper(
self, self,
host: str | None = None, host: Optional[str] = None,
port: int | None = None, port: Optional[int] = None,
debug: bool = False, debug: bool = False,
version: HTTPVersion = HTTP.VERSION_1, version: HTTPVersion = HTTP.VERSION_1,
ssl: None | SSLContext | dict | str | list | tuple = None, ssl: Union[None, SSLContext, dict, str, list, tuple] = None,
sock: socket | None = None, sock: Optional[socket] = None,
unix: str | None = None, unix: Optional[str] = None,
workers: int = 1, workers: int = 1,
loop: AbstractEventLoop | None = None, loop: Optional[AbstractEventLoop] = None,
protocol: type[Protocol] = HttpProtocol, protocol: Type[Protocol] = HttpProtocol,
backlog: int = 100, backlog: int = 100,
register_sys_signals: bool = True, register_sys_signals: bool = True,
run_async: bool = False, run_async: bool = False,
auto_tls: bool = False, auto_tls: bool = False,
) -> dict[str, Any]: ) -> Dict[str, Any]:
"""Helper function used by `run` and `create_server`.""" """Helper function used by `run` and `create_server`."""
if self.config.PROXIES_COUNT and self.config.PROXIES_COUNT < 0: if self.config.PROXIES_COUNT and self.config.PROXIES_COUNT < 0:
raise ValueError( raise ValueError(
@ -704,7 +726,7 @@ class StartupMixin(metaclass=SanicMeta):
def motd( def motd(
self, self,
server_settings: dict[str, Any] | None = None, server_settings: Optional[Dict[str, Any]] = None,
) -> None: ) -> None:
"""Outputs the message of the day (MOTD). """Outputs the message of the day (MOTD).
@ -733,8 +755,8 @@ class StartupMixin(metaclass=SanicMeta):
MOTD.output(logo, serve_location, display, extra) MOTD.output(logo, serve_location, display, extra)
def get_motd_data( def get_motd_data(
self, server_settings: dict[str, Any] | None = None self, server_settings: Optional[Dict[str, Any]] = None
) -> tuple[dict[str, Any], dict[str, Any]]: ) -> Tuple[Dict[str, Any], Dict[str, Any]]:
"""Retrieves the message of the day (MOTD) data. """Retrieves the message of the day (MOTD) data.
Args: Args:
@ -780,7 +802,10 @@ class StartupMixin(metaclass=SanicMeta):
reload_display += ", ".join( reload_display += ", ".join(
[ [
"", "",
*(str(path.absolute()) for path in self.state.reload_dirs), *(
str(path.absolute())
for path in self.state.reload_dirs
),
] ]
) )
display["auto-reload"] = reload_display display["auto-reload"] = reload_display
@ -819,7 +844,9 @@ class StartupMixin(metaclass=SanicMeta):
return f"http://<{location}>" return f"http://<{location}>"
@staticmethod @staticmethod
def get_server_location(server_settings: dict[str, Any] | None = None) -> str: def get_server_location(
server_settings: Optional[Dict[str, Any]] = None
) -> str:
"""Using the server settings, retrieve the server location. """Using the server settings, retrieve the server location.
Args: Args:
@ -853,11 +880,11 @@ class StartupMixin(metaclass=SanicMeta):
@staticmethod @staticmethod
def get_address( def get_address(
host: str | None, host: Optional[str],
port: int | None, port: Optional[int],
version: HTTPVersion = HTTP.VERSION_1, version: HTTPVersion = HTTP.VERSION_1,
auto_tls: bool = False, auto_tls: bool = False,
) -> tuple[str, int]: ) -> Tuple[str, int]:
"""Retrieve the host address and port, with default values based on the given parameters. """Retrieve the host address and port, with default values based on the given parameters.
Args: Args:
@ -886,7 +913,9 @@ class StartupMixin(metaclass=SanicMeta):
@classmethod @classmethod
def _get_startup_method(cls) -> str: def _get_startup_method(cls) -> str:
return ( return (
cls.start_method if not isinstance(cls.start_method, Default) else "spawn" cls.start_method
if not isinstance(cls.start_method, Default)
else "spawn"
) )
@classmethod @classmethod
@ -913,10 +942,10 @@ class StartupMixin(metaclass=SanicMeta):
@classmethod @classmethod
def serve( def serve(
cls, cls,
primary: Sanic | None = None, primary: Optional[Sanic] = None,
*, *,
app_loader: AppLoader | None = None, app_loader: Optional[AppLoader] = None,
factory: Callable[[], Sanic] | None = None, factory: Optional[Callable[[], Sanic]] = None,
) -> None: ) -> None:
"""Serve one or more Sanic applications. """Serve one or more Sanic applications.
@ -967,7 +996,9 @@ class StartupMixin(metaclass=SanicMeta):
try: try:
primary = apps[0] primary = apps[0]
except IndexError: except IndexError:
raise RuntimeError("Did not find any applications.") from None raise RuntimeError(
"Did not find any applications."
) from None
# This exists primarily for unit testing # This exists primarily for unit testing
if not primary.state.server_info: # no cov if not primary.state.server_info: # no cov
@ -1009,7 +1040,7 @@ class StartupMixin(metaclass=SanicMeta):
primary_server_info.settings["run_multiple"] = True primary_server_info.settings["run_multiple"] = True
monitor_sub, monitor_pub = Pipe(True) monitor_sub, monitor_pub = Pipe(True)
worker_state: Mapping[str, Any] = sync_manager.dict() worker_state: Mapping[str, Any] = sync_manager.dict()
kwargs: dict[str, Any] = { kwargs: Dict[str, Any] = {
**primary_server_info.settings, **primary_server_info.settings,
"monitor_publisher": monitor_pub, "monitor_publisher": monitor_pub,
"worker_state": worker_state, "worker_state": worker_state,
@ -1061,7 +1092,7 @@ class StartupMixin(metaclass=SanicMeta):
worker_state, worker_state,
) )
if cls.should_auto_reload(): if cls.should_auto_reload():
reload_dirs: set[Path] = primary.state.reload_dirs.union( reload_dirs: Set[Path] = primary.state.reload_dirs.union(
*(app.state.reload_dirs for app in apps) *(app.state.reload_dirs for app in apps)
) )
reloader = Reloader(monitor_pub, 0, reload_dirs, app_loader) reloader = Reloader(monitor_pub, 0, reload_dirs, app_loader)
@ -1070,7 +1101,9 @@ class StartupMixin(metaclass=SanicMeta):
inspector = None inspector = None
if primary.config.INSPECTOR: if primary.config.INSPECTOR:
display, extra = primary.get_motd_data() display, extra = primary.get_motd_data()
packages = [pkg.strip() for pkg in display["packages"].split(",")] packages = [
pkg.strip() for pkg in display["packages"].split(",")
]
module = import_module("sanic") module = import_module("sanic")
sanic_version = f"sanic=={module.__version__}" # type: ignore sanic_version = f"sanic=={module.__version__}" # type: ignore
app_info = { app_info = {
@ -1101,7 +1134,9 @@ class StartupMixin(metaclass=SanicMeta):
exit_code = 1 exit_code = 1
except BaseException: except BaseException:
kwargs = primary_server_info.settings kwargs = primary_server_info.settings
error_logger.exception("Experienced exception while trying to serve") error_logger.exception(
"Experienced exception while trying to serve"
)
raise raise
finally: finally:
logger.info("Server Stopped") logger.info("Server Stopped")
@ -1129,7 +1164,7 @@ class StartupMixin(metaclass=SanicMeta):
os._exit(exit_code) os._exit(exit_code)
@classmethod @classmethod
def serve_single(cls, primary: Sanic | None = None) -> None: def serve_single(cls, primary: Optional[Sanic] = None) -> None:
"""Serve a single process of a Sanic application. """Serve a single process of a Sanic application.
Similar to `serve`, but only serves a single process. When used, Similar to `serve`, but only serves a single process. When used,
@ -1207,7 +1242,9 @@ class StartupMixin(metaclass=SanicMeta):
try: try:
worker_serve(monitor_publisher=None, **kwargs) worker_serve(monitor_publisher=None, **kwargs)
except BaseException: except BaseException:
error_logger.exception("Experienced exception while trying to serve") error_logger.exception(
"Experienced exception while trying to serve"
)
raise raise
finally: finally:
logger.info("Server Stopped") logger.info("Server Stopped")
@ -1226,7 +1263,7 @@ class StartupMixin(metaclass=SanicMeta):
self, self,
primary: Sanic, primary: Sanic,
_, _,
apps: list[Sanic], apps: List[Sanic],
) -> None: ) -> None:
for app in apps: for app in apps:
if ( if (
@ -1271,7 +1308,7 @@ class StartupMixin(metaclass=SanicMeta):
if not server_info.settings["loop"]: if not server_info.settings["loop"]:
server_info.settings["loop"] = get_running_loop() server_info.settings["loop"] = get_running_loop()
serve_args: dict[str, Any] = { serve_args: Dict[str, Any] = {
**server_info.settings, **server_info.settings,
"run_async": True, "run_async": True,
"reuse_port": bool(primary.state.workers - 1), "reuse_port": bool(primary.state.workers - 1),

Some files were not shown because too many files have changed in this diff Show More