Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00493a6954 | ||
|
|
c8d0c1bf28 | ||
|
|
6ea5c44566 | ||
|
|
1afea39cb2 | ||
|
|
e4daf1ab21 | ||
|
|
469cb1663b | ||
|
|
cdc5dd6b75 | ||
|
|
6fac60c6fe | ||
|
|
53b7412c01 | ||
|
|
65ba1942cc | ||
|
|
9adb6e8ec0 |
@@ -1,5 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Sanic documentation build configuration file, created by
|
||||
# sphinx-quickstart on Sun Dec 25 18:07:21 2016.
|
||||
@@ -10,7 +9,6 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
# Add support for auto-doc
|
||||
|
||||
|
||||
@@ -21,7 +19,6 @@ sys.path.insert(0, root_directory)
|
||||
|
||||
import sanic # noqa: E402
|
||||
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
extensions = [
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import asyncio
|
||||
|
||||
from sanic import Sanic
|
||||
|
||||
|
||||
app = Sanic("Example")
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ from random import randint
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
|
||||
|
||||
app = Sanic("Example")
|
||||
|
||||
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from functools import wraps
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
|
||||
app = Sanic("Example")
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from sanic import Blueprint, Sanic
|
||||
from sanic.response import text
|
||||
|
||||
|
||||
"""
|
||||
Demonstrates that blueprint request middleware are executed in the order they
|
||||
are added. And blueprint response middleware are executed in _reverse_ order.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from sanic import Blueprint, Sanic
|
||||
from sanic.response import file, json
|
||||
|
||||
|
||||
app = Sanic("Example")
|
||||
blueprint = Blueprint("bp_example", url_prefix="/my_blueprint")
|
||||
blueprint2 = Blueprint("bp_example2", url_prefix="/my_blueprint2")
|
||||
|
||||
@@ -2,7 +2,6 @@ from asyncio import sleep
|
||||
|
||||
from sanic import Sanic, response
|
||||
|
||||
|
||||
app = Sanic("DelayedResponseApp", strict_slashes=True)
|
||||
app.config.AUTO_EXTEND = False
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ an external service.
|
||||
from sanic.exceptions import SanicException
|
||||
from sanic.handlers import ErrorHandler
|
||||
|
||||
|
||||
"""
|
||||
Imports and code relevant for our CustomHandler class
|
||||
(Ordinarily this would be in a separate file)
|
||||
@@ -39,7 +38,6 @@ server's error_handler to an instance of our CustomHandler
|
||||
|
||||
from sanic import Sanic
|
||||
|
||||
|
||||
handler = CustomHandler()
|
||||
app = Sanic("Example", error_handler=handler)
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from sanic import Sanic, response
|
||||
|
||||
|
||||
app = Sanic("Example")
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ from sanic import Sanic, response, text
|
||||
from sanic.handlers import ErrorHandler
|
||||
from sanic.server.async_server import AsyncioServer
|
||||
|
||||
|
||||
HTTP_PORT = 9999
|
||||
HTTPS_PORT = 8888
|
||||
|
||||
@@ -36,9 +35,7 @@ def proxy(request, path):
|
||||
|
||||
@https.main_process_start
|
||||
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.ctx.http_server = http_server
|
||||
app.ctx.http = http
|
||||
|
||||
@@ -5,7 +5,6 @@ import httpx
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
|
||||
app = Sanic("Example")
|
||||
|
||||
sem = None
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import logging
|
||||
|
||||
from contextvars import ContextVar
|
||||
|
||||
from sanic import Sanic, response
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import logging
|
||||
import socket
|
||||
|
||||
from os import getenv
|
||||
from platform import node
|
||||
from uuid import getnode as get_mac
|
||||
@@ -11,7 +10,6 @@ from sanic import Sanic
|
||||
from sanic.request import Request
|
||||
from sanic.response import json
|
||||
|
||||
|
||||
log = logging.getLogger("logdna")
|
||||
log.setLevel(logging.INFO)
|
||||
|
||||
@@ -35,9 +33,7 @@ logdna_options = {
|
||||
"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.setLevel(logging.INFO)
|
||||
@@ -48,7 +44,7 @@ app = Sanic("Example")
|
||||
|
||||
@app.middleware
|
||||
def log_request(request: Request):
|
||||
logdna.info("I was Here with a new Request to URL: {}".format(request.url))
|
||||
logdna.info(f"I was Here with a new Request to URL: {request.url}")
|
||||
|
||||
|
||||
@app.route("/")
|
||||
|
||||
@@ -4,7 +4,6 @@ Modify header or status in response
|
||||
|
||||
from sanic import Sanic, response
|
||||
|
||||
|
||||
app = Sanic("Example")
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import logging
|
||||
|
||||
from sanic import Sanic, text
|
||||
|
||||
|
||||
logging_format = "[%(asctime)s] %(process)d-%(levelname)s "
|
||||
logging_format += "%(module)s::%(funcName)s():l%(lineno)d: "
|
||||
logging_format += "%(message)s"
|
||||
|
||||
@@ -11,7 +11,6 @@ Run with xdist params:
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic_testing import SanicTestClient
|
||||
from sanic_testing.testing import PORT as PORT_BASE
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from sanic import Sanic, response
|
||||
|
||||
|
||||
app = Sanic("Example")
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import requests
|
||||
|
||||
|
||||
# Warning: This is a heavy process.
|
||||
|
||||
data = ""
|
||||
|
||||
@@ -4,7 +4,6 @@ from sanic.response import stream, text
|
||||
from sanic.views import HTTPMethodView
|
||||
from sanic.views import stream as stream_decorator
|
||||
|
||||
|
||||
bp = Blueprint("bp_example")
|
||||
app = Sanic("Example")
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ from sanic import Sanic, response
|
||||
from sanic.config import Config
|
||||
from sanic.exceptions import RequestTimeout
|
||||
|
||||
|
||||
Config.REQUEST_TIMEOUT = 1
|
||||
app = Sanic("Example")
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ from sanic import Sanic
|
||||
from sanic.exceptions import SanicException
|
||||
from sanic.handlers import ErrorHandler
|
||||
|
||||
|
||||
rollbar.init(getenv("ROLLBAR_API_KEY"))
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ from pathlib import Path
|
||||
|
||||
from sanic import Sanic, response
|
||||
|
||||
|
||||
app = Sanic("Example")
|
||||
|
||||
|
||||
@@ -43,9 +42,7 @@ async def handler_file(request):
|
||||
|
||||
@app.route("/file_stream")
|
||||
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)
|
||||
|
||||
@@ -4,7 +4,6 @@ import uvloop
|
||||
|
||||
from sanic import Sanic, response
|
||||
|
||||
|
||||
app = Sanic("Example")
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import asyncio
|
||||
|
||||
from signal import SIGINT, signal
|
||||
|
||||
import uvloop
|
||||
@@ -7,7 +6,6 @@ import uvloop
|
||||
from sanic import Sanic, response
|
||||
from sanic.server import AsyncioServer
|
||||
|
||||
|
||||
app = Sanic("Example")
|
||||
|
||||
|
||||
@@ -38,9 +36,7 @@ async def test(request):
|
||||
|
||||
if __name__ == "__main__":
|
||||
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()
|
||||
serv_task = asyncio.ensure_future(serv_coro, loop=loop)
|
||||
signal(SIGINT, lambda s, f: loop.stop())
|
||||
|
||||
@@ -6,7 +6,6 @@ from sentry_sdk.integrations.sanic import SanicIntegration
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
|
||||
sentry_init(
|
||||
dsn=getenv("SENTRY_DSN"),
|
||||
integrations=[SanicIntegration()],
|
||||
|
||||
@@ -2,7 +2,6 @@ from sanic import Sanic
|
||||
from sanic.response import text
|
||||
from sanic.views import HTTPMethodView
|
||||
|
||||
|
||||
app = Sanic("some_name")
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from sanic import Sanic
|
||||
|
||||
|
||||
app = Sanic("Example")
|
||||
|
||||
app.static("/", "./static")
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from sanic import Sanic
|
||||
from sanic import response as res
|
||||
|
||||
|
||||
app = Sanic("Example")
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ from sanic import Sanic, response
|
||||
from sanic.exceptions import ServerError
|
||||
from sanic.log import logger as log
|
||||
|
||||
|
||||
app = Sanic("Example")
|
||||
|
||||
|
||||
@@ -20,7 +19,7 @@ def test_sync(request):
|
||||
|
||||
@app.route("/dynamic/<name>/<i:int>")
|
||||
def test_params(request, name, i):
|
||||
return response.text("yeehaww {} {}".format(name, i))
|
||||
return response.text(f"yeehaww {name} {i}")
|
||||
|
||||
|
||||
@app.route("/exception")
|
||||
@@ -43,9 +42,7 @@ async def test_file(request):
|
||||
|
||||
@app.route("/file_stream")
|
||||
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)
|
||||
|
||||
|
||||
# ----------------------------------------------- #
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from sanic import Sanic, response
|
||||
|
||||
|
||||
app = Sanic("Example")
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from sanic import Sanic, response
|
||||
|
||||
|
||||
app = Sanic("Example")
|
||||
|
||||
|
||||
@@ -14,7 +13,7 @@ async def index(request):
|
||||
|
||||
@app.route("/posts/<post_id>")
|
||||
async def post_handler(request, post_id):
|
||||
return response.text("Post - {}".format(post_id))
|
||||
return response.text(f"Post - {post_id}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -2,7 +2,6 @@ from sanic import Sanic
|
||||
from sanic.blueprints import Blueprint
|
||||
from sanic.response import json
|
||||
|
||||
|
||||
app = Sanic(name="blue-print-group-version-example")
|
||||
|
||||
bp1 = Blueprint(name="ultron", url_prefix="/ultron")
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from sanic import Sanic, response
|
||||
from sanic.blueprints import Blueprint
|
||||
|
||||
|
||||
# Usage
|
||||
# curl -H "Host: example.com" localhost:8000
|
||||
# curl -H "Host: sub.example.com" localhost:8000
|
||||
@@ -12,9 +11,7 @@ app = Sanic("Example")
|
||||
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):
|
||||
return response.text("Some defaults")
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from sanic import Sanic
|
||||
from sanic.response import redirect
|
||||
|
||||
|
||||
app = Sanic("Example")
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import re
|
||||
|
||||
from pathlib import Path
|
||||
from textwrap import indent
|
||||
|
||||
@@ -8,14 +7,10 @@ from emoji import EMOJI
|
||||
COLUMN_PATTERN = re.compile(r"---:1\s*(.*?)\s*:--:1\s*(.*?)\s*:---", re.DOTALL)
|
||||
PYTHON_HIGHLIGHT_PATTERN = re.compile(r"```python\{+.*?\}", 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+):")
|
||||
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):
|
||||
|
||||
@@ -3,5 +3,3 @@ extend = "../pyproject.toml"
|
||||
|
||||
[tool.ruff.isort]
|
||||
known-first-party = ["webapp"]
|
||||
lines-after-imports = 1
|
||||
lines-between-types = 1
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from html5tagger import Builder, Document # type: ignore
|
||||
|
||||
|
||||
class BaseRenderer:
|
||||
def __init__(self, base_title: str):
|
||||
self.base_title = base_title
|
||||
|
||||
@@ -7,6 +7,7 @@ from pygments.token import ( # Error,; Generic,; Number,; Operator,
|
||||
Token,
|
||||
)
|
||||
|
||||
|
||||
class SanicCodeStyle(Style):
|
||||
styles = {
|
||||
Token: "#777",
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Generator
|
||||
from html5tagger import Builder
|
||||
from sanic import Request
|
||||
|
||||
|
||||
class BaseLayout:
|
||||
def __init__(self, builder: Builder):
|
||||
self.builder = builder
|
||||
|
||||
@@ -3,6 +3,7 @@ from datetime import datetime
|
||||
from html5tagger import Builder, E # type: ignore
|
||||
from sanic import Request
|
||||
|
||||
|
||||
def do_footer(builder: Builder, request: Request) -> None:
|
||||
builder.footer(
|
||||
_pagination(request),
|
||||
@@ -12,9 +13,7 @@ def do_footer(builder: Builder, request: Request) -> None:
|
||||
|
||||
|
||||
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:
|
||||
@@ -63,9 +62,7 @@ def _content() -> Builder:
|
||||
href="https://github.com/sanic-org/sanic/blob/master/LICENSE",
|
||||
target="_blank",
|
||||
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(
|
||||
inner,
|
||||
|
||||
@@ -3,14 +3,12 @@ from sanic import Request
|
||||
|
||||
from webapp.display.layouts.models import MenuItem
|
||||
|
||||
|
||||
def do_navbar(builder: Builder, request: Request) -> None:
|
||||
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(
|
||||
E.div(container, class_="navbar-menu"),
|
||||
@@ -45,10 +43,7 @@ def _render_navbar_item(item: MenuItem, request: Request) -> Builder:
|
||||
return E.div(
|
||||
E.a(item.label, class_="navbar-link"),
|
||||
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-item has-dropdown is-hoverable",
|
||||
|
||||
@@ -4,6 +4,7 @@ from sanic import Request
|
||||
from webapp.display.layouts.models import MenuItem
|
||||
from webapp.display.text import slugify
|
||||
|
||||
|
||||
def do_sidebar(builder: Builder, request: Request) -> None:
|
||||
builder.a(class_="burger")(E.span().span().span().span())
|
||||
builder.aside(*_menu_items(request), class_="menu")
|
||||
@@ -14,9 +15,7 @@ def _menu_items(request: Request) -> list[Builder]:
|
||||
_sanic_logo(request),
|
||||
*_sidebar_items(request),
|
||||
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.p("Want more? ").a(
|
||||
"sanicbook.com", href="https://sanicbook.com", target="_blank"
|
||||
@@ -72,9 +71,7 @@ def _single_sidebar_item(item: MenuItem, request: Request) -> Builder:
|
||||
kwargs = {}
|
||||
classes: list[str] = []
|
||||
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:
|
||||
classes.append("is-active")
|
||||
if item.href:
|
||||
|
||||
@@ -8,6 +8,7 @@ from sanic import Request
|
||||
|
||||
from .base import BaseLayout
|
||||
|
||||
|
||||
class HomeLayout(BaseLayout):
|
||||
@contextmanager
|
||||
def layout(
|
||||
|
||||
@@ -9,6 +9,7 @@ from webapp.display.layouts.elements.sidebar import do_sidebar
|
||||
|
||||
from .base import BaseLayout
|
||||
|
||||
|
||||
class MainLayout(BaseLayout):
|
||||
@contextmanager
|
||||
def layout(
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from msgspec import Struct, field
|
||||
|
||||
|
||||
class MenuItem(Struct, kw_only=False, omit_defaults=True):
|
||||
label: str
|
||||
path: str | None = None
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import re
|
||||
|
||||
from textwrap import dedent
|
||||
|
||||
from html5tagger import HTML, Builder, E # type: ignore
|
||||
@@ -20,6 +19,7 @@ from .plugins.span import span
|
||||
from .plugins.tabs import Tabs
|
||||
from .text import slugify
|
||||
|
||||
|
||||
class DocsRenderer(HTMLRenderer):
|
||||
def block_code(self, code: str, info: str | None = None):
|
||||
builder = Builder("Block")
|
||||
@@ -36,9 +36,9 @@ class DocsRenderer(HTMLRenderer):
|
||||
class_="code-block__copy",
|
||||
onclick="copyCode(this)",
|
||||
):
|
||||
builder.div(
|
||||
class_="code-block__rectangle code-block__filled"
|
||||
).div(class_="code-block__rectangle code-block__outlined")
|
||||
builder.div(class_="code-block__rectangle code-block__filled").div(
|
||||
class_="code-block__rectangle code-block__outlined"
|
||||
)
|
||||
else:
|
||||
builder.pre(E.code(escape(code)))
|
||||
return str(builder)
|
||||
@@ -46,9 +46,7 @@ class DocsRenderer(HTMLRenderer):
|
||||
def heading(self, text: str, level: int, **attrs) -> str:
|
||||
ident = slugify(text)
|
||||
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(
|
||||
f"h{level}", {"id": ident, "class": f"is-size-{level}"}, text
|
||||
)
|
||||
@@ -92,9 +90,7 @@ class DocsRenderer(HTMLRenderer):
|
||||
def _make_tag(
|
||||
self, tag: str, attributes: dict[str, str], text: str | None = None
|
||||
) -> 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:
|
||||
return f"<{tag} {attrs} />"
|
||||
return f"<{tag} {attrs}>{text}</{tag}>"
|
||||
|
||||
@@ -3,7 +3,6 @@ from __future__ import annotations
|
||||
import importlib
|
||||
import inspect
|
||||
import pkgutil
|
||||
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
from html import escape
|
||||
@@ -15,6 +14,7 @@ from html5tagger import HTML, Builder, E # type: ignore
|
||||
|
||||
from ..markdown import render_markdown, slugify
|
||||
|
||||
|
||||
@dataclass
|
||||
class DocObject:
|
||||
name: str
|
||||
@@ -119,9 +119,7 @@ def _extract_docobjects(package_name: str) -> dict[str, DocObject]:
|
||||
docstrings = {}
|
||||
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)
|
||||
for obj_name, obj in inspect.getmembers(module):
|
||||
if (
|
||||
@@ -155,9 +153,7 @@ def _docobject_to_html(
|
||||
) -> None:
|
||||
anchor_id = slugify(docobject.full_name.replace(".", "-"))
|
||||
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):
|
||||
builder(heading)
|
||||
@@ -211,9 +207,7 @@ def _docobject_to_html(
|
||||
|
||||
if docobject.docstring.params:
|
||||
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)
|
||||
|
||||
if docobject.docstring.returns:
|
||||
@@ -238,9 +232,7 @@ def _signature_to_html(
|
||||
parts = []
|
||||
parts.append("<span class='function-signature'>")
|
||||
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(
|
||||
f"<span class='is-italic'>{object_type}</span> "
|
||||
f"<span class='has-text-weight-bold'>{name}</span>("
|
||||
@@ -254,9 +246,7 @@ def _signature_to_html(
|
||||
annotation = ""
|
||||
if param.annotation != inspect.Parameter.empty:
|
||||
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:
|
||||
default = escape(str(param.default))
|
||||
if annotation == "str":
|
||||
@@ -267,9 +257,7 @@ def _signature_to_html(
|
||||
parts.append(")")
|
||||
if signature.return_annotation != inspect.Signature.empty:
|
||||
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>")
|
||||
return "".join(parts)
|
||||
|
||||
@@ -317,10 +305,7 @@ def _render_params(builder: Builder, params: list[DocstringParam]) -> None:
|
||||
builder.dd(
|
||||
HTML(
|
||||
render_markdown(
|
||||
param.description
|
||||
or param.arg_name
|
||||
or param.type_name
|
||||
or ""
|
||||
param.description or param.arg_name or param.type_name or ""
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -333,11 +318,7 @@ def _render_raises(builder: Builder, raises: list[DocstringRaises]) -> None:
|
||||
with builder.dl(class_="mt-2"):
|
||||
builder.dt(raise_.type_name, class_="is-family-monospace")
|
||||
builder.dd(
|
||||
HTML(
|
||||
render_markdown(
|
||||
raise_.description or raise_.type_name or ""
|
||||
)
|
||||
)
|
||||
HTML(render_markdown(raise_.description or raise_.type_name or ""))
|
||||
)
|
||||
|
||||
|
||||
@@ -353,11 +334,7 @@ def _render_returns(builder: Builder, docobject: DocObject) -> None:
|
||||
if not return_type or return_type == inspect.Signature.empty:
|
||||
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")
|
||||
with builder.dl(class_="mt-2"):
|
||||
builder.dt(return_type, class_="is-family-monospace")
|
||||
@@ -372,17 +349,11 @@ 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"):
|
||||
builder.h5("Examples", class_="is-size-5 has-text-weight-bold")
|
||||
for example in examples:
|
||||
with builder.div(class_="mt-2"):
|
||||
builder(
|
||||
HTML(
|
||||
render_markdown(
|
||||
example.description or example.snippet or ""
|
||||
)
|
||||
)
|
||||
HTML(render_markdown(example.description or example.snippet or ""))
|
||||
)
|
||||
|
||||
@@ -2,7 +2,6 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Type
|
||||
|
||||
from frontmatter import parse
|
||||
|
||||
@@ -12,10 +11,8 @@ from ..layouts.main import MainLayout
|
||||
from ..markdown import render_markdown
|
||||
from .docobject import organize_docobjects
|
||||
|
||||
_PAGE_CACHE: dict[
|
||||
str, dict[str, tuple[Page | None, Page | None, Page | None]]
|
||||
] = {}
|
||||
_LAYOUTS_CACHE: dict[str, Type[BaseLayout]] = {
|
||||
_PAGE_CACHE: dict[str, dict[str, tuple[Page | None, Page | None, Page | None]]] = {}
|
||||
_LAYOUTS_CACHE: dict[str, type[BaseLayout]] = {
|
||||
"home": HomeLayout,
|
||||
"main": MainLayout,
|
||||
}
|
||||
@@ -43,7 +40,7 @@ class Page:
|
||||
|
||||
DEFAULT_LANGUAGE = _DEFAULT
|
||||
|
||||
def get_layout(self) -> Type[BaseLayout]:
|
||||
def get_layout(self) -> type[BaseLayout]:
|
||||
return _LAYOUTS_CACHE[self.meta.layout]
|
||||
|
||||
@property
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
from typing import Type
|
||||
|
||||
from html5tagger import HTML, Builder # type: ignore
|
||||
from sanic import Request
|
||||
@@ -11,6 +10,7 @@ from webapp.display.base import BaseRenderer
|
||||
from ..layouts.base import BaseLayout
|
||||
from .page import Page
|
||||
|
||||
|
||||
class PageRenderer(BaseRenderer):
|
||||
def render(self, request: Request, language: str, path: str) -> Builder:
|
||||
builder = self.get_builder(
|
||||
@@ -20,13 +20,9 @@ class PageRenderer(BaseRenderer):
|
||||
self._body(request, builder, language, path)
|
||||
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)
|
||||
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.previous_page = prev_page
|
||||
request.ctx.next_page = next_page
|
||||
@@ -38,9 +34,7 @@ class PageRenderer(BaseRenderer):
|
||||
|
||||
@contextmanager
|
||||
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)
|
||||
with layout(request, builder.full):
|
||||
yield
|
||||
|
||||
@@ -7,6 +7,7 @@ from mistune.block_parser import BlockParser
|
||||
from mistune.core import BlockState
|
||||
from mistune.directives import DirectivePlugin
|
||||
|
||||
|
||||
class Attributes(DirectivePlugin):
|
||||
def __call__(self, directive, md):
|
||||
directive.register("attrs", self.parse)
|
||||
@@ -14,9 +15,7 @@ class Attributes(DirectivePlugin):
|
||||
if md.renderer.NAME == "html":
|
||||
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()
|
||||
options = dict(self.parse_options(m))
|
||||
new_state = block.state_cls()
|
||||
|
||||
@@ -8,10 +8,9 @@ from mistune.core import BlockState
|
||||
from mistune.directives import DirectivePlugin, RSTDirective
|
||||
from mistune.markdown import Markdown
|
||||
|
||||
|
||||
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()
|
||||
|
||||
new_state = block.state_cls()
|
||||
@@ -35,9 +34,7 @@ class Column(DirectivePlugin):
|
||||
|
||||
def _render_column(self, renderer: HTMLRenderer, text: str, **attrs):
|
||||
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 ""
|
||||
col = f'<div class="column is-half">{text}</div>\n'
|
||||
|
||||
@@ -2,6 +2,7 @@ from mistune.core import BlockState
|
||||
from mistune.directives import DirectivePlugin, RSTDirective
|
||||
from mistune.markdown import Markdown
|
||||
|
||||
|
||||
class Hook(DirectivePlugin):
|
||||
def __call__( # type: ignore
|
||||
self, directive: RSTDirective, md: Markdown
|
||||
@@ -15,16 +16,12 @@ class Hook(DirectivePlugin):
|
||||
for type_ in ("column", "tab"):
|
||||
if token["type"] == type_:
|
||||
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", {})
|
||||
if prev and prev["type"] != type_:
|
||||
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
|
||||
|
||||
prev = token
|
||||
|
||||
@@ -10,10 +10,9 @@ from mistune.core import BlockState
|
||||
from mistune.directives import DirectivePlugin, RSTDirective
|
||||
from mistune.markdown import Markdown
|
||||
|
||||
|
||||
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()
|
||||
|
||||
new_state = block.state_cls()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from html5tagger import HTML, E
|
||||
from mistune.directives import Admonition
|
||||
|
||||
|
||||
class Notification(Admonition):
|
||||
SUPPORTED_NAMES = {
|
||||
"success",
|
||||
@@ -18,12 +19,8 @@ class Notification(Admonition):
|
||||
|
||||
if md.renderer.NAME == "html":
|
||||
md.renderer.register("admonition", self._render_admonition)
|
||||
md.renderer.register(
|
||||
"admonition_title", self._render_admonition_title
|
||||
)
|
||||
md.renderer.register(
|
||||
"admonition_content", self._render_admonition_content
|
||||
)
|
||||
md.renderer.register("admonition_title", self._render_admonition_title)
|
||||
md.renderer.register("admonition_content", self._render_admonition_content)
|
||||
|
||||
def _render_admonition(self, _, text, name, **attrs) -> str:
|
||||
return str(
|
||||
|
||||
@@ -2,6 +2,7 @@ import re
|
||||
|
||||
from mistune.markdown import Markdown
|
||||
|
||||
|
||||
def parse_inline_span(inline, m: re.Match, state):
|
||||
state.append_token(
|
||||
{
|
||||
|
||||
@@ -8,10 +8,9 @@ from mistune.core import BlockState
|
||||
from mistune.directives import DirectivePlugin, RSTDirective
|
||||
from mistune.markdown import Markdown
|
||||
|
||||
|
||||
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()
|
||||
|
||||
new_state = block.state_cls()
|
||||
@@ -40,9 +39,7 @@ class Tabs(DirectivePlugin):
|
||||
def _render_tab(self, renderer: HTMLRenderer, text: str, **attrs):
|
||||
start = '<div class="tabs mt-6"><ul>\n' if attrs.get("first") else ""
|
||||
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'
|
||||
tab = f'<li><a>{attrs["title"]}</a>{content}</li>\n'
|
||||
|
||||
@@ -9,6 +9,7 @@ from webapp.display.search.search import Searcher
|
||||
from ..base import BaseRenderer
|
||||
from ..layouts.main import MainLayout
|
||||
|
||||
|
||||
class SearchRenderer(BaseRenderer):
|
||||
def render(
|
||||
self, request: Request, language: str, searcher: Searcher, full: bool
|
||||
|
||||
@@ -8,6 +8,7 @@ from msgspec import Struct
|
||||
|
||||
from webapp.display.page import Page
|
||||
|
||||
|
||||
class Stemmer:
|
||||
STOP_WORDS: ClassVar[set[str]] = set(
|
||||
"a about above after again against all am an and any are aren't as at be because been before being below between both but by can't cannot could couldn't did didn't do does doesn't doing don't down during each few for from further had hadn't has hasn't have haven't having he he'd he'll he's her here here's hers herself him himself his how how's i i'd i'll i'm i've if in into is isn't it it's its itself let's me more most mustn't my myself no nor not of off on once only or other ought our ours ourselves out over own same shan't she she'd she'll she's should shouldn't so some such than that that's the their theirs them themselves then there there's these they they'd they'll they're they've this those through to too under until up very was wasn't we we'd we'll we're we've were weren't what what's when when's where where's which while who who's whom why why's with won't would wouldn't you you'd you'll you're you've your yours yourself yourselves".split() # noqa: E501
|
||||
@@ -91,9 +92,7 @@ def _inverse_document_frequency(docs: list[Document]) -> dict[str, float]:
|
||||
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."""
|
||||
return {
|
||||
word: tf * idf[word]
|
||||
@@ -102,9 +101,7 @@ def _tf_idf_vector(
|
||||
}
|
||||
|
||||
|
||||
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."""
|
||||
if not vec1 or not vec2:
|
||||
return 0.0
|
||||
@@ -126,9 +123,7 @@ def _search(
|
||||
tf_idf_query = _tf_idf_vector(
|
||||
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 [
|
||||
(similarity, document)
|
||||
for similarity, document in sorted(
|
||||
@@ -155,16 +150,13 @@ class Searcher:
|
||||
}
|
||||
self._vectors = {
|
||||
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()
|
||||
}
|
||||
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(
|
||||
query,
|
||||
language,
|
||||
|
||||
@@ -4,6 +4,7 @@ from msgspec import yaml
|
||||
|
||||
from webapp.display.layouts.models import GeneralConfig, MenuItem
|
||||
|
||||
|
||||
def load_menu(path: Path) -> list[MenuItem]:
|
||||
loaded = yaml.decode(path.read_bytes(), type=dict[str, list[MenuItem]])
|
||||
return loaded["root"]
|
||||
|
||||
@@ -9,6 +9,7 @@ from webapp.worker.config import load_config, load_menu
|
||||
from webapp.worker.reload import setup_livereload
|
||||
from webapp.worker.style import setup_style
|
||||
|
||||
|
||||
def _compile_sidebar_order(items: list[MenuItem]) -> list[str]:
|
||||
order = []
|
||||
for item in items:
|
||||
@@ -27,13 +28,9 @@ def create_app(root: Path) -> Sanic:
|
||||
app.config.STYLE_DIR = root / "style"
|
||||
app.config.NODE_MODULES_DIR = root / "node_modules"
|
||||
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.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_style(app)
|
||||
@@ -65,8 +62,6 @@ def create_app(root: Path) -> Sanic:
|
||||
|
||||
@app.on_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
|
||||
|
||||
@@ -5,9 +5,9 @@ from queue import Empty, Queue
|
||||
from typing import Any
|
||||
|
||||
import ujson
|
||||
|
||||
from sanic import Request, Sanic, Websocket
|
||||
|
||||
|
||||
def setup_livereload(app: Sanic) -> None:
|
||||
@app.main_process_start
|
||||
async def main_process_start(app: Sanic):
|
||||
@@ -53,16 +53,12 @@ class Livereload:
|
||||
"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.app = Sanic(self.SERVER_NAME)
|
||||
self.debug = debug
|
||||
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.livereload_handler, "/livereload", name="livereload"
|
||||
)
|
||||
@@ -108,7 +104,5 @@ class Livereload:
|
||||
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()
|
||||
|
||||
@@ -6,6 +6,7 @@ from sass import compile as compile_scss
|
||||
|
||||
from webapp.display.code_style import SanicCodeStyle
|
||||
|
||||
|
||||
def setup_style(app: Sanic) -> None:
|
||||
index = app.config.STYLE_DIR / "index.scss"
|
||||
style_output = app.config.PUBLIC_DIR / "assets" / "style.css"
|
||||
|
||||
@@ -3,32 +3,27 @@ requires = ["setuptools", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.ruff]
|
||||
extend-select = ["I"]
|
||||
ignore = ["D100", "D101", "D102", "D103", "E402", "E741", "F811", "F821"]
|
||||
line-length = 79
|
||||
extend-select = ["I", "W", "UP", "C4", "ISC"]
|
||||
# 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]
|
||||
known-first-party = ["sanic"]
|
||||
known-third-party = ["pytest"]
|
||||
lines-after-imports = 2
|
||||
lines-between-types = 1
|
||||
|
||||
[tool.black]
|
||||
line-length = 79
|
||||
|
||||
[tool.isort]
|
||||
atomic = true
|
||||
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]]
|
||||
module = [
|
||||
|
||||
@@ -36,7 +36,6 @@ from sanic.response import (
|
||||
)
|
||||
from sanic.server.websockets.impl import WebsocketImplProtocol as Websocket
|
||||
|
||||
|
||||
DefaultSanic: TypeAlias = "Sanic[Config, SimpleNamespace]"
|
||||
"""
|
||||
A type alias for a Sanic app with a default config and namespace.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from sanic.cli.app import SanicCLI
|
||||
from sanic.compat import OS_IS_WINDOWS, enable_windows_color_support
|
||||
|
||||
|
||||
if OS_IS_WINDOWS:
|
||||
enable_windows_color_support()
|
||||
|
||||
|
||||
379
sanic/app.py
379
sanic/app.py
@@ -5,7 +5,6 @@ import logging
|
||||
import logging.config
|
||||
import re
|
||||
import sys
|
||||
|
||||
from asyncio import (
|
||||
AbstractEventLoop,
|
||||
CancelledError,
|
||||
@@ -32,19 +31,12 @@ from typing import (
|
||||
Callable,
|
||||
ClassVar,
|
||||
Coroutine,
|
||||
Deque,
|
||||
Dict,
|
||||
Generic,
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
Literal,
|
||||
Optional,
|
||||
Set,
|
||||
Tuple,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
cast,
|
||||
overload,
|
||||
)
|
||||
@@ -96,7 +88,6 @@ from sanic.worker.inspector import Inspector
|
||||
from sanic.worker.loader import CertLoader
|
||||
from sanic.worker.manager import WorkerManager
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
try:
|
||||
from sanic_ext import Extend # type: ignore
|
||||
@@ -173,7 +164,7 @@ class Sanic(
|
||||
"websocket_tasks",
|
||||
)
|
||||
|
||||
_app_registry: ClassVar[Dict[str, "Sanic"]] = {}
|
||||
_app_registry: ClassVar[dict[str, Sanic]] = {}
|
||||
test_mode: ClassVar[bool] = False
|
||||
|
||||
@overload
|
||||
@@ -182,19 +173,19 @@ class Sanic(
|
||||
name: str,
|
||||
config: None = None,
|
||||
ctx: None = None,
|
||||
router: Optional[Router] = None,
|
||||
signal_router: Optional[SignalRouter] = None,
|
||||
error_handler: Optional[ErrorHandler] = None,
|
||||
env_prefix: Optional[str] = SANIC_PREFIX,
|
||||
request_class: Optional[Type[Request]] = None,
|
||||
router: Router | None = None,
|
||||
signal_router: SignalRouter | None = None,
|
||||
error_handler: ErrorHandler | None = None,
|
||||
env_prefix: str | None = SANIC_PREFIX,
|
||||
request_class: type[Request] | None = None,
|
||||
strict_slashes: bool = False,
|
||||
log_config: Optional[Dict[str, Any]] = None,
|
||||
log_config: dict[str, Any] | None = None,
|
||||
configure_logging: bool = True,
|
||||
dumps: Optional[Callable[..., AnyStr]] = None,
|
||||
loads: Optional[Callable[..., Any]] = None,
|
||||
dumps: Callable[..., AnyStr] | None = None,
|
||||
loads: Callable[..., Any] | None = None,
|
||||
inspector: bool = False,
|
||||
inspector_class: Optional[Type[Inspector]] = None,
|
||||
certloader_class: Optional[Type[CertLoader]] = None,
|
||||
inspector_class: type[Inspector] | None = None,
|
||||
certloader_class: type[CertLoader] | None = None,
|
||||
) -> None:
|
||||
...
|
||||
|
||||
@@ -202,21 +193,21 @@ class Sanic(
|
||||
def __init__(
|
||||
self: Sanic[config_type, SimpleNamespace],
|
||||
name: str,
|
||||
config: Optional[config_type] = None,
|
||||
config: config_type | None = None,
|
||||
ctx: None = None,
|
||||
router: Optional[Router] = None,
|
||||
signal_router: Optional[SignalRouter] = None,
|
||||
error_handler: Optional[ErrorHandler] = None,
|
||||
env_prefix: Optional[str] = SANIC_PREFIX,
|
||||
request_class: Optional[Type[Request]] = None,
|
||||
router: Router | None = None,
|
||||
signal_router: SignalRouter | None = None,
|
||||
error_handler: ErrorHandler | None = None,
|
||||
env_prefix: str | None = SANIC_PREFIX,
|
||||
request_class: type[Request] | None = None,
|
||||
strict_slashes: bool = False,
|
||||
log_config: Optional[Dict[str, Any]] = None,
|
||||
log_config: dict[str, Any] | None = None,
|
||||
configure_logging: bool = True,
|
||||
dumps: Optional[Callable[..., AnyStr]] = None,
|
||||
loads: Optional[Callable[..., Any]] = None,
|
||||
dumps: Callable[..., AnyStr] | None = None,
|
||||
loads: Callable[..., Any] | None = None,
|
||||
inspector: bool = False,
|
||||
inspector_class: Optional[Type[Inspector]] = None,
|
||||
certloader_class: Optional[Type[CertLoader]] = None,
|
||||
inspector_class: type[Inspector] | None = None,
|
||||
certloader_class: type[CertLoader] | None = None,
|
||||
) -> None:
|
||||
...
|
||||
|
||||
@@ -225,20 +216,20 @@ class Sanic(
|
||||
self: Sanic[Config, ctx_type],
|
||||
name: str,
|
||||
config: None = None,
|
||||
ctx: Optional[ctx_type] = None,
|
||||
router: Optional[Router] = None,
|
||||
signal_router: Optional[SignalRouter] = None,
|
||||
error_handler: Optional[ErrorHandler] = None,
|
||||
env_prefix: Optional[str] = SANIC_PREFIX,
|
||||
request_class: Optional[Type[Request]] = None,
|
||||
ctx: ctx_type | None = None,
|
||||
router: Router | None = None,
|
||||
signal_router: SignalRouter | None = None,
|
||||
error_handler: ErrorHandler | None = None,
|
||||
env_prefix: str | None = SANIC_PREFIX,
|
||||
request_class: type[Request] | None = None,
|
||||
strict_slashes: bool = False,
|
||||
log_config: Optional[Dict[str, Any]] = None,
|
||||
log_config: dict[str, Any] | None = None,
|
||||
configure_logging: bool = True,
|
||||
dumps: Optional[Callable[..., AnyStr]] = None,
|
||||
loads: Optional[Callable[..., Any]] = None,
|
||||
dumps: Callable[..., AnyStr] | None = None,
|
||||
loads: Callable[..., Any] | None = None,
|
||||
inspector: bool = False,
|
||||
inspector_class: Optional[Type[Inspector]] = None,
|
||||
certloader_class: Optional[Type[CertLoader]] = None,
|
||||
inspector_class: type[Inspector] | None = None,
|
||||
certloader_class: type[CertLoader] | None = None,
|
||||
) -> None:
|
||||
...
|
||||
|
||||
@@ -246,42 +237,42 @@ class Sanic(
|
||||
def __init__(
|
||||
self: Sanic[config_type, ctx_type],
|
||||
name: str,
|
||||
config: Optional[config_type] = None,
|
||||
ctx: Optional[ctx_type] = None,
|
||||
router: Optional[Router] = None,
|
||||
signal_router: Optional[SignalRouter] = None,
|
||||
error_handler: Optional[ErrorHandler] = None,
|
||||
env_prefix: Optional[str] = SANIC_PREFIX,
|
||||
request_class: Optional[Type[Request]] = None,
|
||||
config: config_type | None = None,
|
||||
ctx: ctx_type | None = None,
|
||||
router: Router | None = None,
|
||||
signal_router: SignalRouter | None = None,
|
||||
error_handler: ErrorHandler | None = None,
|
||||
env_prefix: str | None = SANIC_PREFIX,
|
||||
request_class: type[Request] | None = None,
|
||||
strict_slashes: bool = False,
|
||||
log_config: Optional[Dict[str, Any]] = None,
|
||||
log_config: dict[str, Any] | None = None,
|
||||
configure_logging: bool = True,
|
||||
dumps: Optional[Callable[..., AnyStr]] = None,
|
||||
loads: Optional[Callable[..., Any]] = None,
|
||||
dumps: Callable[..., AnyStr] | None = None,
|
||||
loads: Callable[..., Any] | None = None,
|
||||
inspector: bool = False,
|
||||
inspector_class: Optional[Type[Inspector]] = None,
|
||||
certloader_class: Optional[Type[CertLoader]] = None,
|
||||
inspector_class: type[Inspector] | None = None,
|
||||
certloader_class: type[CertLoader] | None = None,
|
||||
) -> None:
|
||||
...
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
config: Optional[config_type] = None,
|
||||
ctx: Optional[ctx_type] = None,
|
||||
router: Optional[Router] = None,
|
||||
signal_router: Optional[SignalRouter] = None,
|
||||
error_handler: Optional[ErrorHandler] = None,
|
||||
env_prefix: Optional[str] = SANIC_PREFIX,
|
||||
request_class: Optional[Type[Request]] = None,
|
||||
config: config_type | None = None,
|
||||
ctx: ctx_type | None = None,
|
||||
router: Router | None = None,
|
||||
signal_router: SignalRouter | None = None,
|
||||
error_handler: ErrorHandler | None = None,
|
||||
env_prefix: str | None = SANIC_PREFIX,
|
||||
request_class: type[Request] | None = None,
|
||||
strict_slashes: bool = False,
|
||||
log_config: Optional[Dict[str, Any]] = None,
|
||||
log_config: dict[str, Any] | None = None,
|
||||
configure_logging: bool = True,
|
||||
dumps: Optional[Callable[..., AnyStr]] = None,
|
||||
loads: Optional[Callable[..., Any]] = None,
|
||||
dumps: Callable[..., AnyStr] | None = None,
|
||||
loads: Callable[..., Any] | None = None,
|
||||
inspector: bool = False,
|
||||
inspector_class: Optional[Type[Inspector]] = None,
|
||||
certloader_class: Optional[Type[CertLoader]] = None,
|
||||
inspector_class: type[Inspector] | None = None,
|
||||
certloader_class: type[CertLoader] | None = None,
|
||||
) -> None:
|
||||
super().__init__(name=name)
|
||||
# logging
|
||||
@@ -303,41 +294,39 @@ class Sanic(
|
||||
self.config.INSPECTOR = inspector
|
||||
|
||||
# Then we can do the rest
|
||||
self._asgi_app: Optional[ASGIApp] = None
|
||||
self._asgi_lifespan: Optional[Lifespan] = None
|
||||
self._asgi_app: ASGIApp | None = None
|
||||
self._asgi_lifespan: Lifespan | None = None
|
||||
self._asgi_client: Any = None
|
||||
self._blueprint_order: List[Blueprint] = []
|
||||
self._delayed_tasks: List[str] = []
|
||||
self._blueprint_order: list[Blueprint] = []
|
||||
self._delayed_tasks: list[str] = []
|
||||
self._future_registry: FutureRegistry = FutureRegistry()
|
||||
self._inspector: Optional[Inspector] = None
|
||||
self._manager: Optional[WorkerManager] = None
|
||||
self._inspector: Inspector | None = None
|
||||
self._manager: WorkerManager | None = None
|
||||
self._state: ApplicationState = ApplicationState(app=self)
|
||||
self._task_registry: Dict[str, Union[Task, None]] = {}
|
||||
self._task_registry: dict[str, Task | None] = {}
|
||||
self._test_client: Any = None
|
||||
self._test_manager: Any = None
|
||||
self.asgi = False
|
||||
self.auto_reload = False
|
||||
self.blueprints: Dict[str, Blueprint] = {}
|
||||
self.certloader_class: Type[CertLoader] = (
|
||||
certloader_class or CertLoader
|
||||
)
|
||||
self.blueprints: dict[str, Blueprint] = {}
|
||||
self.certloader_class: type[CertLoader] = certloader_class or CertLoader
|
||||
self.configure_logging: bool = configure_logging
|
||||
self.ctx: ctx_type = cast(ctx_type, ctx or SimpleNamespace())
|
||||
self.error_handler: ErrorHandler = error_handler or ErrorHandler()
|
||||
self.inspector_class: Type[Inspector] = inspector_class or Inspector
|
||||
self.listeners: Dict[str, List[ListenerType[Any]]] = defaultdict(list)
|
||||
self.named_request_middleware: Dict[str, Deque[Middleware]] = {}
|
||||
self.named_response_middleware: Dict[str, Deque[Middleware]] = {}
|
||||
self.request_class: Type[Request] = request_class or Request
|
||||
self.request_middleware: Deque[Middleware] = deque()
|
||||
self.response_middleware: Deque[Middleware] = deque()
|
||||
self.inspector_class: type[Inspector] = inspector_class or Inspector
|
||||
self.listeners: dict[str, list[ListenerType[Any]]] = defaultdict(list)
|
||||
self.named_request_middleware: dict[str, deque[Middleware]] = {}
|
||||
self.named_response_middleware: dict[str, deque[Middleware]] = {}
|
||||
self.request_class: type[Request] = request_class or Request
|
||||
self.request_middleware: deque[Middleware] = deque()
|
||||
self.response_middleware: deque[Middleware] = deque()
|
||||
self.router: Router = router or Router()
|
||||
self.shared_ctx: SharedContext = SharedContext()
|
||||
self.signal_router: SignalRouter = signal_router or SignalRouter()
|
||||
self.sock: Optional[socket] = None
|
||||
self.sock: socket | None = None
|
||||
self.strict_slashes: bool = strict_slashes
|
||||
self.websocket_enabled: bool = False
|
||||
self.websocket_tasks: Set[Future[Any]] = set()
|
||||
self.websocket_tasks: set[Future[Any]] = set()
|
||||
|
||||
# Register alternative method names
|
||||
self.go_fast = self.run
|
||||
@@ -396,15 +385,11 @@ class Sanic(
|
||||
try:
|
||||
_event = ListenerEvent[event.upper()]
|
||||
except (ValueError, AttributeError):
|
||||
valid = ", ".join(
|
||||
map(lambda x: x.lower(), ListenerEvent.__members__.keys())
|
||||
)
|
||||
valid = ", ".join(x.lower() for x in ListenerEvent.__members__.keys())
|
||||
raise BadRequest(f"Invalid event: {event}. Use one of: {valid}")
|
||||
|
||||
if "." in _event:
|
||||
self.signal(_event.value)(
|
||||
partial(self._listener, listener=listener)
|
||||
)
|
||||
self.signal(_event.value)(partial(self._listener, listener=listener))
|
||||
else:
|
||||
self.listeners[_event.value].append(listener)
|
||||
|
||||
@@ -412,11 +397,11 @@ class Sanic(
|
||||
|
||||
def register_middleware(
|
||||
self,
|
||||
middleware: Union[MiddlewareType, Middleware],
|
||||
middleware: MiddlewareType | Middleware,
|
||||
attach_to: str = "request",
|
||||
*,
|
||||
priority: Union[Default, int] = _default,
|
||||
) -> Union[MiddlewareType, Middleware]:
|
||||
priority: Default | int = _default,
|
||||
) -> MiddlewareType | Middleware:
|
||||
"""Register a middleware to be called before a request is handled.
|
||||
|
||||
Args:
|
||||
@@ -461,7 +446,7 @@ class Sanic(
|
||||
route_names: Iterable[str],
|
||||
attach_to: str = "request",
|
||||
*,
|
||||
priority: Union[Default, int] = _default,
|
||||
priority: Default | int = _default,
|
||||
):
|
||||
"""Used to register named middleqare (middleware typically on blueprints)
|
||||
|
||||
@@ -512,7 +497,7 @@ class Sanic(
|
||||
def _apply_exception_handler(
|
||||
self,
|
||||
handler: FutureException,
|
||||
route_names: Optional[List[str]] = None,
|
||||
route_names: list[str] | None = None,
|
||||
):
|
||||
"""Decorate a function to be registered as a handler for exceptions
|
||||
|
||||
@@ -531,9 +516,7 @@ class Sanic(
|
||||
def _apply_listener(self, listener: FutureListener):
|
||||
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["overwrite"] = overwrite
|
||||
websocket = params.pop("websocket", False)
|
||||
@@ -567,7 +550,7 @@ class Sanic(
|
||||
def _apply_middleware(
|
||||
self,
|
||||
middleware: FutureMiddleware,
|
||||
route_names: Optional[List[str]] = None,
|
||||
route_names: list[str] | None = None,
|
||||
):
|
||||
with self.amend():
|
||||
if route_names:
|
||||
@@ -588,8 +571,8 @@ class Sanic(
|
||||
self,
|
||||
event: str,
|
||||
*,
|
||||
condition: Optional[Dict[str, str]] = None,
|
||||
context: Optional[Dict[str, Any]] = None,
|
||||
condition: dict[str, str] | None = None,
|
||||
context: dict[str, Any] | None = None,
|
||||
fail_not_found: bool = True,
|
||||
inline: Literal[True],
|
||||
reverse: bool = False,
|
||||
@@ -601,8 +584,8 @@ class Sanic(
|
||||
self,
|
||||
event: str,
|
||||
*,
|
||||
condition: Optional[Dict[str, str]] = None,
|
||||
context: Optional[Dict[str, Any]] = None,
|
||||
condition: dict[str, str] | None = None,
|
||||
context: dict[str, Any] | None = None,
|
||||
fail_not_found: bool = True,
|
||||
inline: Literal[False] = False,
|
||||
reverse: bool = False,
|
||||
@@ -613,12 +596,12 @@ class Sanic(
|
||||
self,
|
||||
event: str,
|
||||
*,
|
||||
condition: Optional[Dict[str, str]] = None,
|
||||
context: Optional[Dict[str, Any]] = None,
|
||||
condition: dict[str, str] | None = None,
|
||||
context: dict[str, Any] | None = None,
|
||||
fail_not_found: bool = True,
|
||||
inline: bool = False,
|
||||
reverse: bool = False,
|
||||
) -> Coroutine[Any, Any, Awaitable[Union[Task, Any]]]:
|
||||
) -> Coroutine[Any, Any, Awaitable[Task | Any]]:
|
||||
"""Dispatches an event to the signal router.
|
||||
|
||||
Args:
|
||||
@@ -662,9 +645,7 @@ class Sanic(
|
||||
fail_not_found=fail_not_found,
|
||||
)
|
||||
|
||||
async def event(
|
||||
self, event: str, timeout: Optional[Union[int, float]] = None
|
||||
) -> None:
|
||||
async def event(self, event: str, timeout: int | float | None = None) -> None:
|
||||
"""Wait for a specific event to be triggered.
|
||||
|
||||
This method waits for a named event to be triggered and can be used
|
||||
@@ -749,9 +730,7 @@ class Sanic(
|
||||
async def report(exception: Exception) -> None:
|
||||
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
|
||||
|
||||
@@ -780,13 +759,13 @@ class Sanic(
|
||||
|
||||
def blueprint(
|
||||
self,
|
||||
blueprint: Union[Blueprint, Iterable[Blueprint], BlueprintGroup],
|
||||
blueprint: Blueprint | (Iterable[Blueprint] | BlueprintGroup),
|
||||
*,
|
||||
url_prefix: Optional[str] = None,
|
||||
version: Optional[Union[int, float, str]] = None,
|
||||
strict_slashes: Optional[bool] = None,
|
||||
version_prefix: Optional[str] = None,
|
||||
name_prefix: Optional[str] = None,
|
||||
url_prefix: str | None = None,
|
||||
version: int | float | str | None = None,
|
||||
strict_slashes: bool | None = None,
|
||||
version_prefix: str | None = None,
|
||||
name_prefix: str | None = None,
|
||||
) -> None:
|
||||
"""Register a blueprint on the application.
|
||||
|
||||
@@ -812,7 +791,7 @@ class Sanic(
|
||||
app.blueprint(bp, url_prefix='/blueprint')
|
||||
```
|
||||
""" # noqa: E501
|
||||
options: Dict[str, Any] = {}
|
||||
options: dict[str, Any] = {}
|
||||
if url_prefix is not None:
|
||||
options["url_prefix"] = url_prefix
|
||||
if version is not None:
|
||||
@@ -825,7 +804,7 @@ class Sanic(
|
||||
options["name_prefix"] = name_prefix
|
||||
if isinstance(blueprint, (Iterable, BlueprintGroup)):
|
||||
for item in blueprint:
|
||||
params: Dict[str, Any] = {**options}
|
||||
params: dict[str, Any] = {**options}
|
||||
if isinstance(blueprint, BlueprintGroup):
|
||||
merge_from = [
|
||||
options.get("url_prefix", ""),
|
||||
@@ -840,14 +819,12 @@ class Sanic(
|
||||
|
||||
for _attr in ["version", "strict_slashes"]:
|
||||
if getattr(item, _attr) is None:
|
||||
params[_attr] = getattr(
|
||||
blueprint, _attr
|
||||
) or options.get(_attr)
|
||||
params[_attr] = getattr(blueprint, _attr) or options.get(
|
||||
_attr
|
||||
)
|
||||
if item.version_prefix == "/v":
|
||||
if blueprint.version_prefix == "/v":
|
||||
params["version_prefix"] = options.get(
|
||||
"version_prefix"
|
||||
)
|
||||
params["version_prefix"] = options.get("version_prefix")
|
||||
else:
|
||||
params["version_prefix"] = blueprint.version_prefix
|
||||
name_prefix = getattr(blueprint, "name_prefix", None)
|
||||
@@ -857,17 +834,14 @@ class Sanic(
|
||||
return
|
||||
if blueprint.name in self.blueprints:
|
||||
assert self.blueprints[blueprint.name] is blueprint, (
|
||||
'A blueprint with the name "%s" is already registered. '
|
||||
"Blueprint names must be unique." % (blueprint.name,)
|
||||
f'A blueprint with the name "{blueprint.name}" is already registered. '
|
||||
"Blueprint names must be unique."
|
||||
)
|
||||
else:
|
||||
self.blueprints[blueprint.name] = 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.register(self, options)
|
||||
|
||||
@@ -923,7 +897,7 @@ class Sanic(
|
||||
# http://subdomain.example.com/view-name
|
||||
""" # noqa: E501
|
||||
# find the route by the supplied view name
|
||||
kw: Dict[str, str] = {}
|
||||
kw: dict[str, str] = {}
|
||||
# special static files url_for
|
||||
|
||||
if "." not in view_name:
|
||||
@@ -937,9 +911,7 @@ class Sanic(
|
||||
|
||||
route = self.router.find_route_by_view_name(view_name, **kw)
|
||||
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
|
||||
|
||||
@@ -978,9 +950,7 @@ class Sanic(
|
||||
scheme = kwargs.pop("_scheme", "")
|
||||
if route.extra.hosts and external:
|
||||
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:
|
||||
raise ValueError(
|
||||
f"Requested host ({host}) is not available for this "
|
||||
@@ -1096,10 +1066,7 @@ class Sanic(
|
||||
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)
|
||||
logger.error(
|
||||
"The error response will not be sent to the client for "
|
||||
@@ -1146,10 +1113,7 @@ class Sanic(
|
||||
response = self.error_handler.default(request, e)
|
||||
elif self.debug:
|
||||
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,
|
||||
)
|
||||
else:
|
||||
@@ -1194,9 +1158,7 @@ class Sanic(
|
||||
)
|
||||
await response.eof()
|
||||
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
|
||||
"""Handles a request by dispatching it to the appropriate handler.
|
||||
@@ -1221,13 +1183,11 @@ class Sanic(
|
||||
|
||||
# Define `response` var here to remove warnings about
|
||||
# allocation before assignment below.
|
||||
response: Optional[
|
||||
Union[
|
||||
BaseHTTPResponse,
|
||||
Coroutine[Any, Any, Optional[BaseHTTPResponse]],
|
||||
ResponseStream,
|
||||
]
|
||||
] = None
|
||||
response: (
|
||||
BaseHTTPResponse
|
||||
| (Coroutine[Any, Any, BaseHTTPResponse | None] | ResponseStream)
|
||||
| None
|
||||
) = None
|
||||
run_middleware = True
|
||||
try:
|
||||
await self.dispatch(
|
||||
@@ -1285,10 +1245,8 @@ class Sanic(
|
||||
|
||||
if handler is None:
|
||||
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
|
||||
@@ -1347,17 +1305,14 @@ class Sanic(
|
||||
else:
|
||||
if not hasattr(handler, "is_websocket"):
|
||||
raise ServerError(
|
||||
f"Invalid response type {response!r} "
|
||||
"(need HTTPResponse)"
|
||||
f"Invalid response type {response!r} " "(need HTTPResponse)"
|
||||
)
|
||||
|
||||
except CancelledError: # type: ignore
|
||||
raise
|
||||
except Exception as e:
|
||||
# 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(
|
||||
self, handler, request, *args, subprotocols=None, **kwargs
|
||||
@@ -1436,9 +1391,7 @@ class Sanic(
|
||||
# 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
|
||||
|
||||
for middleware in middleware_collection:
|
||||
@@ -1515,9 +1468,7 @@ class Sanic(
|
||||
task.cancel()
|
||||
|
||||
@staticmethod
|
||||
async def _listener(
|
||||
app: Sanic, loop: AbstractEventLoop, listener: ListenerType
|
||||
):
|
||||
async def _listener(app: Sanic, loop: AbstractEventLoop, listener: ListenerType):
|
||||
try:
|
||||
maybe_coro = listener(app) # type: ignore
|
||||
except TypeError:
|
||||
@@ -1546,9 +1497,7 @@ class Sanic(
|
||||
if isawaitable(task):
|
||||
await task
|
||||
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
|
||||
except Exception as e:
|
||||
await app.dispatch(
|
||||
@@ -1566,7 +1515,7 @@ class Sanic(
|
||||
app,
|
||||
loop,
|
||||
*,
|
||||
name: Optional[str] = None,
|
||||
name: str | None = None,
|
||||
register: bool = True,
|
||||
) -> Task:
|
||||
if not isinstance(task, Future):
|
||||
@@ -1628,11 +1577,11 @@ class Sanic(
|
||||
|
||||
def add_task(
|
||||
self,
|
||||
task: Union[Future[Any], Coroutine[Any, Any, Any], Awaitable[Any]],
|
||||
task: Future[Any] | (Coroutine[Any, Any, Any] | Awaitable[Any]),
|
||||
*,
|
||||
name: Optional[str] = None,
|
||||
name: str | None = None,
|
||||
register: bool = True,
|
||||
) -> Optional[Task[Any]]:
|
||||
) -> Task[Any] | None:
|
||||
"""Schedule a task to run later, after the loop has started.
|
||||
|
||||
While this is somewhat similar to `asyncio.create_task`, it can be
|
||||
@@ -1657,18 +1606,14 @@ class Sanic(
|
||||
""" # noqa: E501
|
||||
try:
|
||||
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:
|
||||
task_name = f"sanic.delayed_task.{hash(task)}"
|
||||
if not self._delayed_tasks:
|
||||
self.after_server_start(partial(self.dispatch_delayed_tasks))
|
||||
|
||||
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._delayed_tasks.append(task_name)
|
||||
@@ -1679,18 +1624,14 @@ class Sanic(
|
||||
...
|
||||
|
||||
@overload
|
||||
def get_task(
|
||||
self, name: str, *, raise_exception: Literal[False]
|
||||
) -> Optional[Task]:
|
||||
def get_task(self, name: str, *, raise_exception: Literal[False]) -> Task | None:
|
||||
...
|
||||
|
||||
@overload
|
||||
def get_task(self, name: str, *, raise_exception: bool) -> Optional[Task]:
|
||||
def get_task(self, name: str, *, raise_exception: bool) -> Task | None:
|
||||
...
|
||||
|
||||
def get_task(
|
||||
self, name: str, *, raise_exception: bool = True
|
||||
) -> Optional[Task]:
|
||||
def get_task(self, name: str, *, raise_exception: bool = True) -> Task | None:
|
||||
"""Get a named task.
|
||||
|
||||
This method is used to get a task by its name. Optionally, you can
|
||||
@@ -1708,15 +1649,13 @@ class Sanic(
|
||||
return self._task_registry[name]
|
||||
except KeyError:
|
||||
if raise_exception:
|
||||
raise SanicException(
|
||||
f'Registered task named "{name}" not found.'
|
||||
)
|
||||
raise SanicException(f'Registered task named "{name}" not found.')
|
||||
return None
|
||||
|
||||
async def cancel_task(
|
||||
self,
|
||||
name: str,
|
||||
msg: Optional[str] = None,
|
||||
msg: str | None = None,
|
||||
*,
|
||||
raise_exception: bool = True,
|
||||
) -> None:
|
||||
@@ -1751,7 +1690,7 @@ class Sanic(
|
||||
""" # noqa: E501
|
||||
task = self.get_task(name, raise_exception=raise_exception)
|
||||
if task and not task.cancelled():
|
||||
args: Tuple[str, ...] = ()
|
||||
args: tuple[str, ...] = ()
|
||||
if msg:
|
||||
if sys.version_info >= (3, 9):
|
||||
args = (msg,)
|
||||
@@ -1784,7 +1723,7 @@ class Sanic(
|
||||
}
|
||||
|
||||
def shutdown_tasks(
|
||||
self, timeout: Optional[float] = None, increment: float = 0.1
|
||||
self, timeout: float | None = None, increment: float = 0.1
|
||||
) -> None:
|
||||
"""Cancel all tasks except the server task.
|
||||
|
||||
@@ -1822,11 +1761,7 @@ class Sanic(
|
||||
Iterable[Task[Any]]: The tasks that are currently registered with
|
||||
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
|
||||
@@ -1853,7 +1788,7 @@ class Sanic(
|
||||
# Configuration
|
||||
# -------------------------------------------------------------------- #
|
||||
|
||||
def update_config(self, config: Union[bytes, str, dict, Any]) -> None:
|
||||
def update_config(self, config: Any) -> None:
|
||||
"""Update the application configuration.
|
||||
|
||||
This method is used to update the application configuration. It can
|
||||
@@ -1863,7 +1798,7 @@ class Sanic(
|
||||
See [Configuration](/en/guide/deployment/configuration) for details.
|
||||
|
||||
Args:
|
||||
config (Union[bytes, str, dict, Any]): The configuration object,
|
||||
config (bytes | str | dict | Any): The configuration object,
|
||||
dictionary, or path to a configuration file.
|
||||
"""
|
||||
|
||||
@@ -1903,7 +1838,7 @@ class Sanic(
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def reload_dirs(self) -> Set[Path]:
|
||||
def reload_dirs(self) -> set[Path]:
|
||||
"""The directories that are monitored for auto-reload.
|
||||
|
||||
Returns:
|
||||
@@ -1948,9 +1883,9 @@ class Sanic(
|
||||
def extend(
|
||||
self,
|
||||
*,
|
||||
extensions: Optional[List[Type[Extension]]] = None,
|
||||
extensions: list[type[Extension]] | None = None,
|
||||
built_in_extensions: bool = True,
|
||||
config: Optional[Union[Config, Dict[str, Any]]] = None,
|
||||
config: Config | dict[str, Any] | None = None,
|
||||
**kwargs,
|
||||
) -> Extend:
|
||||
"""Extend Sanic with additional functionality using Sanic Extensions.
|
||||
@@ -2068,9 +2003,7 @@ class Sanic(
|
||||
del cls._app_registry[name]
|
||||
|
||||
@classmethod
|
||||
def get_app(
|
||||
cls, name: Optional[str] = None, *, force_create: bool = False
|
||||
) -> Sanic:
|
||||
def get_app(cls, name: str | None = None, *, force_create: bool = False) -> Sanic:
|
||||
"""Retrieve an instantiated Sanic instance by name.
|
||||
|
||||
This method is best used when needing to get access to an already
|
||||
@@ -2277,9 +2210,7 @@ class Sanic(
|
||||
self.finalize()
|
||||
|
||||
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:
|
||||
names = ", ".join(duplicates)
|
||||
message = (
|
||||
@@ -2316,7 +2247,7 @@ class Sanic(
|
||||
self,
|
||||
concern: str,
|
||||
action: str,
|
||||
loop: Optional[AbstractEventLoop] = None,
|
||||
loop: AbstractEventLoop | None = None,
|
||||
) -> None:
|
||||
event = f"server.{concern}.{action}"
|
||||
if action not in ("before", "after") or concern not in (
|
||||
@@ -2324,9 +2255,7 @@ class Sanic(
|
||||
"shutdown",
|
||||
):
|
||||
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"
|
||||
if loop is None:
|
||||
loop = self.loop
|
||||
@@ -2347,7 +2276,7 @@ class Sanic(
|
||||
|
||||
def refresh(
|
||||
self,
|
||||
passthru: Optional[Dict[str, Any]] = None,
|
||||
passthru: dict[str, Any] | None = None,
|
||||
) -> Sanic:
|
||||
"""Refresh the application instance. **This is used internally by Sanic**.
|
||||
|
||||
@@ -2392,9 +2321,7 @@ class Sanic(
|
||||
Inspector: An instance of 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
|
||||
|
||||
@property
|
||||
@@ -2427,7 +2354,5 @@ class Sanic(
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
@@ -4,7 +4,6 @@ from contextlib import suppress
|
||||
from importlib import import_module
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sanic import Sanic
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import re
|
||||
import sys
|
||||
|
||||
from os import environ
|
||||
|
||||
from sanic.helpers import is_atty
|
||||
|
||||
|
||||
BASE_LOGO = """
|
||||
|
||||
Sanic
|
||||
@@ -63,10 +61,7 @@ def get_logo(full: bool = False, coffee: bool = False) -> str:
|
||||
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)
|
||||
|
||||
return logo
|
||||
|
||||
@@ -79,9 +79,7 @@ class MOTDTTY(MOTD):
|
||||
def set_variables(self): # no cov
|
||||
"""Set the variables used for display."""
|
||||
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.key_width = 4
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
from contextlib import contextmanager
|
||||
from queue import Queue
|
||||
from threading import Thread
|
||||
|
||||
|
||||
if os.name == "nt": # noqa
|
||||
import ctypes # noqa
|
||||
|
||||
@@ -47,21 +45,16 @@ class Spinner: # noqa
|
||||
@staticmethod
|
||||
def cursor():
|
||||
while True:
|
||||
for cursor in "|/-\\":
|
||||
yield cursor
|
||||
yield from "|/-\\"
|
||||
|
||||
@staticmethod
|
||||
def hide():
|
||||
if os.name == "nt":
|
||||
ci = _CursorInfo()
|
||||
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
|
||||
ctypes.windll.kernel32.SetConsoleCursorInfo(
|
||||
handle, ctypes.byref(ci)
|
||||
)
|
||||
ctypes.windll.kernel32.SetConsoleCursorInfo(handle, ctypes.byref(ci))
|
||||
elif os.name == "posix":
|
||||
sys.stdout.write("\033[?25l")
|
||||
sys.stdout.flush()
|
||||
@@ -71,13 +64,9 @@ class Spinner: # noqa
|
||||
if os.name == "nt":
|
||||
ci = _CursorInfo()
|
||||
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
|
||||
ctypes.windll.kernel32.SetConsoleCursorInfo(
|
||||
handle, ctypes.byref(ci)
|
||||
)
|
||||
ctypes.windll.kernel32.SetConsoleCursorInfo(handle, ctypes.byref(ci))
|
||||
elif os.name == "posix":
|
||||
sys.stdout.write("\033[?25h")
|
||||
sys.stdout.flush()
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from socket import socket
|
||||
from ssl import SSLContext
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Union
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from sanic.application.constants import Mode, Server, ServerStage
|
||||
from sanic.log import VerbosityFilter, logger
|
||||
from sanic.server.async_server import AsyncioServer
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sanic import Sanic
|
||||
|
||||
@@ -21,9 +19,9 @@ if TYPE_CHECKING:
|
||||
class ApplicationServerInfo:
|
||||
"""Information about a server instance."""
|
||||
|
||||
settings: Dict[str, Any]
|
||||
settings: dict[str, Any]
|
||||
stage: ServerStage = field(default=ServerStage.STOPPED)
|
||||
server: Optional[AsyncioServer] = field(default=None)
|
||||
server: AsyncioServer | None = field(default=None)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -40,11 +38,11 @@ class ApplicationState:
|
||||
fast: bool = field(default=False)
|
||||
host: str = field(default="")
|
||||
port: int = field(default=0)
|
||||
ssl: Optional[SSLContext] = field(default=None)
|
||||
sock: Optional[socket] = field(default=None)
|
||||
unix: Optional[str] = field(default=None)
|
||||
ssl: SSLContext | None = field(default=None)
|
||||
sock: socket | None = field(default=None)
|
||||
unix: str | None = field(default=None)
|
||||
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)
|
||||
server: Server = field(default=Server.SANIC)
|
||||
is_running: bool = field(default=False)
|
||||
@@ -53,7 +51,7 @@ class ApplicationState:
|
||||
verbosity: int = field(default=0)
|
||||
workers: int = field(default=0)
|
||||
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
|
||||
# not be changed except in the __post_init__ method
|
||||
@@ -64,14 +62,12 @@ class ApplicationState:
|
||||
|
||||
def __setattr__(self, name: str, value: Any) -> None:
|
||||
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)
|
||||
if self._init and hasattr(self, f"set_{name}"):
|
||||
getattr(self, f"set_{name}")(value)
|
||||
|
||||
def set_mode(self, value: Union[str, Mode]):
|
||||
def set_mode(self, value: str | Mode):
|
||||
if hasattr(self.app, "error_handler"):
|
||||
self.app.error_handler.debug = self.app.debug
|
||||
if getattr(self.app, "configure_logging", False) and self.app.debug:
|
||||
@@ -107,9 +103,7 @@ class ApplicationState:
|
||||
|
||||
if all(info.stage is ServerStage.SERVING for info in self.server_info):
|
||||
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.STOPPED
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sanic.compat import Header
|
||||
from sanic.exceptions import BadRequest, ServerError
|
||||
@@ -15,7 +14,6 @@ from sanic.response import BaseHTTPResponse
|
||||
from sanic.server import ConnInfo
|
||||
from sanic.server.websockets.connection import WebSocketConnection
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sanic import Sanic
|
||||
|
||||
@@ -109,9 +107,9 @@ class ASGIApp:
|
||||
request: Request
|
||||
transport: MockTransport
|
||||
lifespan: Lifespan
|
||||
ws: Optional[WebSocketConnection]
|
||||
ws: WebSocketConnection | None
|
||||
stage: Stage
|
||||
response: Optional[BaseHTTPResponse]
|
||||
response: BaseHTTPResponse | None
|
||||
|
||||
@classmethod
|
||||
async def create(
|
||||
@@ -142,9 +140,7 @@ class ASGIApp:
|
||||
]
|
||||
)
|
||||
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":
|
||||
version = scope["http_version"]
|
||||
@@ -153,9 +149,7 @@ class ASGIApp:
|
||||
version = "1.1"
|
||||
method = "GET"
|
||||
|
||||
instance.ws = instance.transport.create_websocket_connection(
|
||||
send, receive
|
||||
)
|
||||
instance.ws = instance.transport.create_websocket_connection(send, receive)
|
||||
else:
|
||||
raise ServerError("Received unknown ASGI scope")
|
||||
|
||||
@@ -189,7 +183,7 @@ class ASGIApp:
|
||||
|
||||
return instance
|
||||
|
||||
async def read(self) -> Optional[bytes]:
|
||||
async def read(self) -> bytes | None:
|
||||
"""
|
||||
Read and stream the body in chunks from an incoming ASGI message.
|
||||
"""
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import re
|
||||
|
||||
from typing import Any, Optional
|
||||
|
||||
from sanic.base.meta import SanicMeta
|
||||
@@ -11,7 +10,6 @@ from sanic.mixins.routes import RouteMixin
|
||||
from sanic.mixins.signals import SignalMixin
|
||||
from sanic.mixins.static import StaticMixin
|
||||
|
||||
|
||||
VALID_NAME = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_\-]*$")
|
||||
|
||||
|
||||
@@ -26,9 +24,7 @@ class BaseSanic(
|
||||
):
|
||||
__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__
|
||||
|
||||
if name is None:
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from .blueprints import BlueprintGroup
|
||||
|
||||
|
||||
__all__ = ["BlueprintGroup"] # noqa: F405
|
||||
|
||||
@@ -2,7 +2,6 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
|
||||
from collections import defaultdict
|
||||
from collections.abc import MutableSequence
|
||||
from copy import deepcopy
|
||||
@@ -14,15 +13,9 @@ from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
Sequence,
|
||||
Set,
|
||||
Tuple,
|
||||
Union,
|
||||
overload,
|
||||
)
|
||||
|
||||
@@ -39,7 +32,6 @@ from sanic.models.handler_types import (
|
||||
RouteHandler,
|
||||
)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sanic import Sanic
|
||||
|
||||
@@ -122,10 +114,10 @@ class Blueprint(BaseSanic):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
url_prefix: Optional[str] = None,
|
||||
host: Optional[Union[List[str], str]] = None,
|
||||
version: Optional[Union[int, str, float]] = None,
|
||||
strict_slashes: Optional[bool] = None,
|
||||
url_prefix: str | None = None,
|
||||
host: list[str] | str | None = None,
|
||||
version: int | str | float | None = None,
|
||||
strict_slashes: bool | None = None,
|
||||
version_prefix: str = "/v",
|
||||
):
|
||||
super().__init__(name=name)
|
||||
@@ -136,9 +128,7 @@ class Blueprint(BaseSanic):
|
||||
self.host = host
|
||||
self.strict_slashes = strict_slashes
|
||||
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_prefix = version_prefix
|
||||
@@ -161,7 +151,7 @@ class Blueprint(BaseSanic):
|
||||
return f"Blueprint({args})"
|
||||
|
||||
@property
|
||||
def apps(self) -> Set[Sanic]:
|
||||
def apps(self) -> set[Sanic]:
|
||||
"""Get the set of apps that this blueprint is registered to.
|
||||
|
||||
Returns:
|
||||
@@ -172,9 +162,7 @@ class Blueprint(BaseSanic):
|
||||
an app.
|
||||
"""
|
||||
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
|
||||
|
||||
@property
|
||||
@@ -196,23 +184,23 @@ class Blueprint(BaseSanic):
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Reset the blueprint to its initial state."""
|
||||
self._apps: Set[Sanic] = set()
|
||||
self._apps: set[Sanic] = set()
|
||||
self._allow_route_overwrite = False
|
||||
self.exceptions: List[RouteHandler] = []
|
||||
self.listeners: Dict[str, List[ListenerType[Any]]] = {}
|
||||
self.middlewares: List[MiddlewareType] = []
|
||||
self.routes: List[Route] = []
|
||||
self.statics: List[RouteHandler] = []
|
||||
self.websocket_routes: List[Route] = []
|
||||
self.exceptions: list[RouteHandler] = []
|
||||
self.listeners: dict[str, list[ListenerType[Any]]] = {}
|
||||
self.middlewares: list[MiddlewareType] = []
|
||||
self.routes: list[Route] = []
|
||||
self.statics: list[RouteHandler] = []
|
||||
self.websocket_routes: list[Route] = []
|
||||
|
||||
def copy(
|
||||
self,
|
||||
name: str,
|
||||
url_prefix: Optional[Union[str, Default]] = _default,
|
||||
version: Optional[Union[int, str, float, Default]] = _default,
|
||||
version_prefix: Union[str, Default] = _default,
|
||||
allow_route_overwrite: Union[bool, Default] = _default,
|
||||
strict_slashes: Optional[Union[bool, Default]] = _default,
|
||||
url_prefix: str | Default | None = _default,
|
||||
version: int | str | float | Default | None = _default,
|
||||
version_prefix: str | Default = _default,
|
||||
allow_route_overwrite: bool | Default = _default,
|
||||
strict_slashes: bool | Default | None = _default,
|
||||
with_registration: bool = True,
|
||||
with_ctx: bool = False,
|
||||
):
|
||||
@@ -277,12 +265,12 @@ class Blueprint(BaseSanic):
|
||||
|
||||
@staticmethod
|
||||
def group(
|
||||
*blueprints: Union[Blueprint, BlueprintGroup],
|
||||
url_prefix: Optional[str] = None,
|
||||
version: Optional[Union[int, str, float]] = None,
|
||||
strict_slashes: Optional[bool] = None,
|
||||
*blueprints: Blueprint | BlueprintGroup,
|
||||
url_prefix: str | None = None,
|
||||
version: int | str | float | None = None,
|
||||
strict_slashes: bool | None = None,
|
||||
version_prefix: str = "/v",
|
||||
name_prefix: Optional[str] = "",
|
||||
name_prefix: str | None = "",
|
||||
) -> BlueprintGroup:
|
||||
"""Group multiple blueprints (or other blueprint groups) together.
|
||||
|
||||
@@ -353,9 +341,7 @@ class Blueprint(BaseSanic):
|
||||
opt_strict_slashes = options.get("strict_slashes", None)
|
||||
opt_version_prefix = options.get("version_prefix", self.version_prefix)
|
||||
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 = []
|
||||
middleware = []
|
||||
@@ -381,9 +367,7 @@ class Blueprint(BaseSanic):
|
||||
version_prefix = prefix
|
||||
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(
|
||||
future.strict_slashes, opt_strict_slashes, self.strict_slashes
|
||||
)
|
||||
@@ -419,22 +403,16 @@ class Blueprint(BaseSanic):
|
||||
continue
|
||||
|
||||
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
|
||||
# matchup with the new BP name
|
||||
if self.copied_from:
|
||||
for r in route:
|
||||
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)
|
||||
|
||||
# Static Files
|
||||
@@ -479,7 +457,7 @@ class Blueprint(BaseSanic):
|
||||
continue
|
||||
future.condition.update({"__blueprint__": self.name})
|
||||
# Force exclusive to be False
|
||||
app._apply_signal(tuple((*future[:-1], False)))
|
||||
app._apply_signal((*future[:-1], False))
|
||||
|
||||
self.routes += [route for route in routes if isinstance(route, Route)]
|
||||
self.websocket_routes += [
|
||||
@@ -512,11 +490,9 @@ class Blueprint(BaseSanic):
|
||||
condition = kwargs.pop("condition", {})
|
||||
condition.update({"__blueprint__": self.name})
|
||||
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: Optional[Union[int, float]] = None):
|
||||
def event(self, event: str, timeout: int | float | None = None):
|
||||
"""Wait for a signal event to be dispatched.
|
||||
|
||||
Args:
|
||||
@@ -550,7 +526,7 @@ class Blueprint(BaseSanic):
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
def _setup_uri(base: str, prefix: Optional[str]):
|
||||
def _setup_uri(base: str, prefix: str | None):
|
||||
uri = base
|
||||
if prefix:
|
||||
uri = prefix
|
||||
@@ -563,7 +539,7 @@ class Blueprint(BaseSanic):
|
||||
|
||||
@staticmethod
|
||||
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.
|
||||
|
||||
@@ -575,7 +551,7 @@ class Blueprint(BaseSanic):
|
||||
"""
|
||||
|
||||
for app in apps:
|
||||
app._future_registry.update(set((bp, item) for item in futures))
|
||||
app._future_registry.update({(bp, item) for item in futures})
|
||||
|
||||
|
||||
if sys.version_info < (3, 9):
|
||||
@@ -667,13 +643,13 @@ class BlueprintGroup(bpg_base):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
url_prefix: Optional[str] = None,
|
||||
version: Optional[Union[int, str, float]] = None,
|
||||
strict_slashes: Optional[bool] = None,
|
||||
url_prefix: str | None = None,
|
||||
version: int | str | float | None = None,
|
||||
strict_slashes: bool | None = None,
|
||||
version_prefix: str = "/v",
|
||||
name_prefix: Optional[str] = "",
|
||||
name_prefix: str | None = "",
|
||||
):
|
||||
self._blueprints: List[Blueprint] = []
|
||||
self._blueprints: list[Blueprint] = []
|
||||
self._url_prefix = url_prefix
|
||||
self._version = version
|
||||
self._version_prefix = version_prefix
|
||||
@@ -681,7 +657,7 @@ class BlueprintGroup(bpg_base):
|
||||
self._name_prefix = name_prefix
|
||||
|
||||
@property
|
||||
def url_prefix(self) -> Optional[Union[int, str, float]]:
|
||||
def url_prefix(self) -> int | str | float | None:
|
||||
"""The URL prefix for the Blueprint Group.
|
||||
|
||||
Returns:
|
||||
@@ -691,7 +667,7 @@ class BlueprintGroup(bpg_base):
|
||||
return self._url_prefix
|
||||
|
||||
@property
|
||||
def blueprints(self) -> List[Blueprint]:
|
||||
def blueprints(self) -> list[Blueprint]:
|
||||
"""A list of all the available blueprints under this group.
|
||||
|
||||
Returns:
|
||||
@@ -701,7 +677,7 @@ class BlueprintGroup(bpg_base):
|
||||
return self._blueprints
|
||||
|
||||
@property
|
||||
def version(self) -> Optional[Union[str, int, float]]:
|
||||
def version(self) -> str | int | float | None:
|
||||
"""API Version for the Blueprint Group, if any.
|
||||
|
||||
Returns:
|
||||
@@ -710,7 +686,7 @@ class BlueprintGroup(bpg_base):
|
||||
return self._version
|
||||
|
||||
@property
|
||||
def strict_slashes(self) -> Optional[bool]:
|
||||
def strict_slashes(self) -> bool | None:
|
||||
"""Whether to enforce strict slashes for the Blueprint Group.
|
||||
|
||||
Returns:
|
||||
@@ -728,7 +704,7 @@ class BlueprintGroup(bpg_base):
|
||||
return self._version_prefix
|
||||
|
||||
@property
|
||||
def name_prefix(self) -> Optional[str]:
|
||||
def name_prefix(self) -> str | None:
|
||||
"""Name prefix for the Blueprint Group.
|
||||
|
||||
This is mainly needed when blueprints are copied in order to
|
||||
@@ -755,9 +731,7 @@ class BlueprintGroup(bpg_base):
|
||||
def __getitem__(self, item: slice) -> MutableSequence[Blueprint]:
|
||||
...
|
||||
|
||||
def __getitem__(
|
||||
self, item: Union[int, slice]
|
||||
) -> Union[Blueprint, MutableSequence[Blueprint]]:
|
||||
def __getitem__(self, item: int | slice) -> Blueprint | MutableSequence[Blueprint]:
|
||||
"""Get the Blueprint object at the specified index.
|
||||
|
||||
This method returns a blueprint inside the group specified by
|
||||
@@ -785,8 +759,8 @@ class BlueprintGroup(bpg_base):
|
||||
|
||||
def __setitem__(
|
||||
self,
|
||||
index: Union[int, slice],
|
||||
item: Union[Blueprint, Iterable[Blueprint]],
|
||||
index: int | slice,
|
||||
item: Blueprint | Iterable[Blueprint],
|
||||
) -> None:
|
||||
"""Set the Blueprint object at the specified index.
|
||||
|
||||
@@ -824,7 +798,7 @@ class BlueprintGroup(bpg_base):
|
||||
def __delitem__(self, index: slice) -> None:
|
||||
...
|
||||
|
||||
def __delitem__(self, index: Union[int, slice]) -> None:
|
||||
def __delitem__(self, index: int | slice) -> None:
|
||||
"""Delete the Blueprint object at the specified index.
|
||||
|
||||
Abstract method implemented to turn the `BlueprintGroup` class
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
from argparse import Namespace
|
||||
from functools import partial
|
||||
from textwrap import indent
|
||||
@@ -57,9 +56,7 @@ Or, a path to a directory to run as a simple HTTP server:
|
||||
)
|
||||
self.parser._positionals.title = "Required\n========\n Positional"
|
||||
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.groups: List[Group] = []
|
||||
self.inspecting = False
|
||||
@@ -127,11 +124,7 @@ Or, a path to a directory to run as a simple HTTP server:
|
||||
key = key.lstrip("-")
|
||||
except ValueError:
|
||||
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)
|
||||
|
||||
kwargs = {**self.args.__dict__}
|
||||
@@ -181,8 +174,7 @@ Or, a path to a directory to run as a simple HTTP server:
|
||||
" Example Module: project.sanic_server.app"
|
||||
)
|
||||
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)
|
||||
else:
|
||||
@@ -196,7 +188,7 @@ Or, a path to a directory to run as a simple HTTP server:
|
||||
if self.args.tlshost:
|
||||
ssl.append(None)
|
||||
if self.args.cert is not None or self.args.key is not None:
|
||||
ssl.append(dict(cert=self.args.cert, key=self.args.key))
|
||||
ssl.append({"cert": self.args.cert, "key": self.args.key})
|
||||
if self.args.tls:
|
||||
ssl += self.args.tls
|
||||
if not ssl:
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from argparse import ArgumentParser, _ArgumentGroup
|
||||
from typing import List, Optional, Type, Union
|
||||
|
||||
from sanic_routing import __version__ as __routing_version__
|
||||
|
||||
@@ -10,14 +9,14 @@ from sanic.http.constants import HTTP
|
||||
|
||||
|
||||
class Group:
|
||||
name: Optional[str]
|
||||
container: Union[ArgumentParser, _ArgumentGroup]
|
||||
_registry: List[Type[Group]] = []
|
||||
name: str | None
|
||||
container: ArgumentParser | _ArgumentGroup
|
||||
_registry: list[type[Group]] = []
|
||||
|
||||
def __init_subclass__(cls) -> None:
|
||||
Group._registry.append(cls)
|
||||
|
||||
def __init__(self, parser: ArgumentParser, title: Optional[str]):
|
||||
def __init__(self, parser: ArgumentParser, title: str | None):
|
||||
self.parser = parser
|
||||
|
||||
if title:
|
||||
@@ -245,10 +244,7 @@ class DevelopmentGroup(Group):
|
||||
"--auto-reload",
|
||||
dest="auto_reload",
|
||||
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(
|
||||
"-R",
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
from http.client import RemoteDisconnected
|
||||
from textwrap import indent
|
||||
from typing import Any, Dict, Optional
|
||||
from typing import Any
|
||||
from urllib.error import URLError
|
||||
from urllib.request import Request as URequest
|
||||
from urllib.request import urlopen
|
||||
@@ -13,7 +12,6 @@ from sanic.application.logo import get_logo
|
||||
from sanic.application.motd import MOTDTTY
|
||||
from sanic.log import Colors
|
||||
|
||||
|
||||
try: # no cov
|
||||
from ujson import dumps, loads
|
||||
except ModuleNotFoundError: # no cov
|
||||
@@ -27,7 +25,7 @@ class InspectorClient:
|
||||
port: int,
|
||||
secure: bool,
|
||||
raw: bool,
|
||||
api_key: Optional[str],
|
||||
api_key: str | None,
|
||||
) -> None:
|
||||
self.scheme = "https" if secure else "http"
|
||||
self.host = host
|
||||
@@ -47,11 +45,7 @@ class InspectorClient:
|
||||
return
|
||||
result = self.request(action, **kwargs).get("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")
|
||||
|
||||
def info(self) -> None:
|
||||
@@ -89,7 +83,7 @@ class InspectorClient:
|
||||
|
||||
def request(self, action: str, method: str = "POST", **kwargs: Any) -> Any:
|
||||
url = f"{self.base_url}/{action}"
|
||||
params: Dict[str, Any] = {"method": method, "headers": {}}
|
||||
params: dict[str, Any] = {"method": method, "headers": {}}
|
||||
if kwargs:
|
||||
params["data"] = dumps(kwargs).encode()
|
||||
params["headers"]["content-type"] = "application/json"
|
||||
|
||||
@@ -3,25 +3,16 @@ import os
|
||||
import platform
|
||||
import signal
|
||||
import sys
|
||||
|
||||
from contextlib import contextmanager
|
||||
from enum import Enum
|
||||
from typing import Awaitable, Union
|
||||
from typing import Awaitable, Literal, Union
|
||||
|
||||
from multidict import CIMultiDict # type: ignore
|
||||
|
||||
from sanic.helpers import Default
|
||||
from sanic.log import error_logger
|
||||
|
||||
|
||||
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"]
|
||||
]
|
||||
StartMethod = Union[Default, Literal["fork"], Literal["forkserver"], Literal["spawn"]]
|
||||
|
||||
OS_IS_WINDOWS = os.name == "nt"
|
||||
PYPY_IMPLEMENTATION = platform.python_implementation() == "PyPy"
|
||||
@@ -142,7 +133,10 @@ if use_trio: # pragma: no cover
|
||||
return trio.Path(path).stat()
|
||||
|
||||
open_async = trio.open_file
|
||||
CancelledErrors = tuple([asyncio.CancelledError, trio.Cancelled])
|
||||
CancelledErrors: tuple[type[BaseException], ...] = (
|
||||
asyncio.CancelledError,
|
||||
trio.Cancelled,
|
||||
)
|
||||
else:
|
||||
if PYPY_IMPLEMENTATION:
|
||||
pypy_os_module_patch()
|
||||
@@ -156,7 +150,7 @@ else:
|
||||
async def open_async(file, mode="r", **kwargs):
|
||||
return aio_open(file, mode, **kwargs)
|
||||
|
||||
CancelledErrors = tuple([asyncio.CancelledError])
|
||||
CancelledErrors = (asyncio.CancelledError,)
|
||||
|
||||
|
||||
def ctrlc_workaround_for_windows(app):
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
from abc import ABCMeta
|
||||
from inspect import getmembers, isclass, isdatadescriptor
|
||||
from os import environ
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Dict, Optional, Sequence, Union
|
||||
from typing import Any, Callable, Literal, Sequence, Union
|
||||
from warnings import filterwarnings
|
||||
|
||||
from sanic.constants import LocalCertCreator
|
||||
@@ -16,20 +14,14 @@ from sanic.http import Http
|
||||
from sanic.log import error_logger
|
||||
from sanic.utils import load_module_from_file_location, str_to_bool
|
||||
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
from typing import Literal
|
||||
|
||||
FilterWarningType = Union[
|
||||
Literal["default"],
|
||||
Literal["error"],
|
||||
Literal["ignore"],
|
||||
Literal["always"],
|
||||
Literal["module"],
|
||||
Literal["once"],
|
||||
]
|
||||
else:
|
||||
FilterWarningType = str
|
||||
FilterWarningType = Union[
|
||||
Literal["default"],
|
||||
Literal["error"],
|
||||
Literal["ignore"],
|
||||
Literal["always"],
|
||||
Literal["module"],
|
||||
Literal["once"],
|
||||
]
|
||||
|
||||
SANIC_PREFIX = "SANIC_"
|
||||
|
||||
@@ -100,25 +92,25 @@ class Config(dict, metaclass=DescriptorMeta):
|
||||
EVENT_AUTOREGISTER: bool
|
||||
DEPRECATION_FILTER: FilterWarningType
|
||||
FORWARDED_FOR_HEADER: str
|
||||
FORWARDED_SECRET: Optional[str]
|
||||
FORWARDED_SECRET: str | None
|
||||
GRACEFUL_SHUTDOWN_TIMEOUT: float
|
||||
INSPECTOR: bool
|
||||
INSPECTOR_HOST: str
|
||||
INSPECTOR_PORT: int
|
||||
INSPECTOR_TLS_KEY: Union[Path, str, Default]
|
||||
INSPECTOR_TLS_CERT: Union[Path, str, Default]
|
||||
INSPECTOR_TLS_KEY: Path | str | Default
|
||||
INSPECTOR_TLS_CERT: Path | str | Default
|
||||
INSPECTOR_API_KEY: str
|
||||
KEEP_ALIVE_TIMEOUT: int
|
||||
KEEP_ALIVE: bool
|
||||
LOCAL_CERT_CREATOR: Union[str, LocalCertCreator]
|
||||
LOCAL_TLS_KEY: Union[Path, str, Default]
|
||||
LOCAL_TLS_CERT: Union[Path, str, Default]
|
||||
LOCAL_CERT_CREATOR: str | LocalCertCreator
|
||||
LOCAL_TLS_KEY: Path | str | Default
|
||||
LOCAL_TLS_CERT: Path | str | Default
|
||||
LOCALHOST: str
|
||||
MOTD: bool
|
||||
MOTD_DISPLAY: Dict[str, str]
|
||||
MOTD_DISPLAY: dict[str, str]
|
||||
NOISY_EXCEPTIONS: bool
|
||||
PROXIES_COUNT: Optional[int]
|
||||
REAL_IP_HEADER: Optional[str]
|
||||
PROXIES_COUNT: int | None
|
||||
REAL_IP_HEADER: str | None
|
||||
REQUEST_BUFFER_SIZE: int
|
||||
REQUEST_MAX_HEADER_SIZE: int
|
||||
REQUEST_ID_HEADER: str
|
||||
@@ -127,21 +119,19 @@ class Config(dict, metaclass=DescriptorMeta):
|
||||
RESPONSE_TIMEOUT: int
|
||||
SERVER_NAME: str
|
||||
TLS_CERT_PASSWORD: str
|
||||
TOUCHUP: Union[Default, bool]
|
||||
USE_UVLOOP: Union[Default, bool]
|
||||
TOUCHUP: Default | bool
|
||||
USE_UVLOOP: Default | bool
|
||||
WEBSOCKET_MAX_SIZE: int
|
||||
WEBSOCKET_PING_INTERVAL: int
|
||||
WEBSOCKET_PING_TIMEOUT: int
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
defaults: Optional[
|
||||
Dict[str, Union[str, bool, int, float, None]]
|
||||
] = None,
|
||||
env_prefix: Optional[str] = SANIC_PREFIX,
|
||||
keep_alive: Optional[bool] = None,
|
||||
defaults: dict[str, str | bool | int | float | None] | None = None,
|
||||
env_prefix: str | None = SANIC_PREFIX,
|
||||
keep_alive: bool | None = None,
|
||||
*,
|
||||
converters: Optional[Sequence[Callable[[str], Any]]] = None,
|
||||
converters: Sequence[Callable[[str], Any]] | None = None,
|
||||
):
|
||||
defaults = defaults or {}
|
||||
super().__init__({**DEFAULT_CONFIG, **defaults})
|
||||
@@ -209,7 +199,7 @@ class Config(dict, metaclass=DescriptorMeta):
|
||||
```
|
||||
"""
|
||||
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)
|
||||
for k in {**kwargs}.keys()
|
||||
if k in self.__class__.__setters__
|
||||
@@ -237,9 +227,7 @@ class Config(dict, metaclass=DescriptorMeta):
|
||||
if attr == "LOCAL_CERT_CREATOR" and not isinstance(
|
||||
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":
|
||||
self._configure_warnings()
|
||||
|
||||
@@ -276,7 +264,7 @@ class Config(dict, metaclass=DescriptorMeta):
|
||||
module=r"sanic.*",
|
||||
)
|
||||
|
||||
def _check_error_format(self, format: Optional[str] = None):
|
||||
def _check_error_format(self, format: str | None = None):
|
||||
check_error_format(format or self.FALLBACK_ERROR_FORMAT)
|
||||
|
||||
def load_environment_vars(self, prefix=SANIC_PREFIX):
|
||||
@@ -332,7 +320,7 @@ class Config(dict, metaclass=DescriptorMeta):
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def update_config(self, config: Union[bytes, str, dict, Any]):
|
||||
def update_config(self, config: bytes | str | dict | Any):
|
||||
"""Update app.config.
|
||||
|
||||
.. note::
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from .response import Cookie, CookieJar
|
||||
|
||||
|
||||
__all__ = ("Cookie", "CookieJar")
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import re
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from sanic.cookies.response import Cookie
|
||||
from sanic.log import deprecation
|
||||
from sanic.request.parameters import RequestParameters
|
||||
|
||||
|
||||
COOKIE_NAME_RESERVED_CHARS = re.compile(
|
||||
'[\x00-\x1F\x7F-\xFF()<>@,;:\\\\"/[\\]?={} \x09]'
|
||||
)
|
||||
@@ -149,9 +147,7 @@ class CookieRequestParameters(RequestParameters):
|
||||
except KeyError:
|
||||
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:
|
||||
return self._get_prefixed_cookie(name)
|
||||
except KeyError:
|
||||
|
||||
@@ -2,31 +2,25 @@ from __future__ import annotations
|
||||
|
||||
import re
|
||||
import string
|
||||
import sys
|
||||
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
|
||||
from typing import TYPE_CHECKING, Any, Union
|
||||
|
||||
from sanic.exceptions import ServerError
|
||||
from sanic.log import deprecation
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sanic.compat import Header
|
||||
|
||||
if sys.version_info < (3, 8): # no cov
|
||||
SameSite = str
|
||||
else: # no cov
|
||||
from typing import Literal
|
||||
from typing import Literal
|
||||
|
||||
SameSite = Union[
|
||||
Literal["Strict"],
|
||||
Literal["Lax"],
|
||||
Literal["None"],
|
||||
Literal["strict"],
|
||||
Literal["lax"],
|
||||
Literal["none"],
|
||||
]
|
||||
SameSite = Union[
|
||||
Literal["Strict"],
|
||||
Literal["Lax"],
|
||||
Literal["None"],
|
||||
Literal["strict"],
|
||||
Literal["lax"],
|
||||
Literal["none"],
|
||||
]
|
||||
|
||||
DEFAULT_MAX_AGE = 0
|
||||
SAMESITE_VALUES = ("strict", "lax", "none")
|
||||
@@ -180,7 +174,7 @@ class CookieJar(dict):
|
||||
return CookieJar.HEADER_KEY
|
||||
|
||||
@property
|
||||
def cookie_headers(self) -> Dict[str, str]: # no cov
|
||||
def cookie_headers(self) -> dict[str, str]: # no cov
|
||||
"""Deprecated in v24.3"""
|
||||
deprecation(
|
||||
"The CookieJar.coookie_headers property has been deprecated "
|
||||
@@ -191,7 +185,7 @@ class CookieJar(dict):
|
||||
return {key: self.header_key for key in self}
|
||||
|
||||
@property
|
||||
def cookies(self) -> List[Cookie]:
|
||||
def cookies(self) -> list[Cookie]:
|
||||
"""A list of cookies in the CookieJar.
|
||||
|
||||
Returns:
|
||||
@@ -203,10 +197,10 @@ class CookieJar(dict):
|
||||
self,
|
||||
key: str,
|
||||
path: str = "/",
|
||||
domain: Optional[str] = None,
|
||||
domain: str | None = None,
|
||||
host_prefix: bool = False,
|
||||
secure_prefix: bool = False,
|
||||
) -> Optional[Cookie]:
|
||||
) -> Cookie | None:
|
||||
"""Fetch a cookie from the CookieJar.
|
||||
|
||||
Args:
|
||||
@@ -236,7 +230,7 @@ class CookieJar(dict):
|
||||
self,
|
||||
key: str,
|
||||
path: str = "/",
|
||||
domain: Optional[str] = None,
|
||||
domain: str | None = None,
|
||||
host_prefix: bool = False,
|
||||
secure_prefix: bool = False,
|
||||
) -> bool:
|
||||
@@ -271,14 +265,14 @@ class CookieJar(dict):
|
||||
value: str,
|
||||
*,
|
||||
path: str = "/",
|
||||
domain: Optional[str] = None,
|
||||
domain: str | None = None,
|
||||
secure: bool = True,
|
||||
max_age: Optional[int] = None,
|
||||
expires: Optional[datetime] = None,
|
||||
max_age: int | None = None,
|
||||
expires: datetime | None = None,
|
||||
httponly: bool = False,
|
||||
samesite: Optional[SameSite] = "Lax",
|
||||
samesite: SameSite | None = "Lax",
|
||||
partitioned: bool = False,
|
||||
comment: Optional[str] = None,
|
||||
comment: str | None = None,
|
||||
host_prefix: bool = False,
|
||||
secure_prefix: bool = False,
|
||||
) -> Cookie:
|
||||
@@ -362,7 +356,7 @@ class CookieJar(dict):
|
||||
key: str,
|
||||
*,
|
||||
path: str = "/",
|
||||
domain: Optional[str] = None,
|
||||
domain: str | None = None,
|
||||
host_prefix: bool = False,
|
||||
secure_prefix: bool = False,
|
||||
) -> None:
|
||||
@@ -390,7 +384,7 @@ class CookieJar(dict):
|
||||
:type secure_prefix: bool
|
||||
"""
|
||||
# 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:
|
||||
if (
|
||||
cookie.key != Cookie.make_key(key, host_prefix, secure_prefix)
|
||||
@@ -481,14 +475,14 @@ class Cookie(dict):
|
||||
value: str,
|
||||
*,
|
||||
path: str = "/",
|
||||
domain: Optional[str] = None,
|
||||
domain: str | None = None,
|
||||
secure: bool = True,
|
||||
max_age: Optional[int] = None,
|
||||
expires: Optional[datetime] = None,
|
||||
max_age: int | None = None,
|
||||
expires: datetime | None = None,
|
||||
httponly: bool = False,
|
||||
samesite: Optional[SameSite] = "Lax",
|
||||
samesite: SameSite | None = "Lax",
|
||||
partitioned: bool = False,
|
||||
comment: Optional[str] = None,
|
||||
comment: str | None = None,
|
||||
host_prefix: bool = False,
|
||||
secure_prefix: bool = False,
|
||||
):
|
||||
@@ -502,9 +496,7 @@ class Cookie(dict):
|
||||
"Cannot set host_prefix on a cookie without secure=True"
|
||||
)
|
||||
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:
|
||||
raise ServerError(
|
||||
"Cannot set host_prefix on a cookie with a defined domain"
|
||||
@@ -561,7 +553,7 @@ class Cookie(dict):
|
||||
# in v24.3 when this is no longer a dict
|
||||
def _set_value(self, key: str, value: Any) -> None:
|
||||
if key not in self._keys:
|
||||
raise KeyError("Unknown cookie property: %s=%s" % (key, value))
|
||||
raise KeyError(f"Unknown cookie property: {key}={value}")
|
||||
|
||||
if value is not None:
|
||||
if key.lower() == "max-age" and not str(value).isdigit():
|
||||
@@ -604,21 +596,18 @@ class Cookie(dict):
|
||||
|
||||
def __str__(self):
|
||||
"""Format as a Set-Cookie header value."""
|
||||
output = ["%s=%s" % (self.key, _quote(self.value))]
|
||||
output = [f"{self.key}={_quote(self.value)}"]
|
||||
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 key == "max-age":
|
||||
try:
|
||||
output.append("%s=%d" % (self._keys[key], value))
|
||||
except TypeError:
|
||||
output.append("%s=%s" % (self._keys[key], value))
|
||||
output.append(f"{self._keys[key]}={value}")
|
||||
elif key == "expires":
|
||||
output.append(
|
||||
"%s=%s"
|
||||
% (
|
||||
"{}={}".format(
|
||||
self._keys[key],
|
||||
value.strftime("%a, %d-%b-%Y %T GMT"),
|
||||
)
|
||||
@@ -626,7 +615,7 @@ class Cookie(dict):
|
||||
elif key in self._flags:
|
||||
output.append(self._keys[key])
|
||||
else:
|
||||
output.append("%s=%s" % (self._keys[key], value))
|
||||
output.append(f"{self._keys[key]}={value}")
|
||||
|
||||
return "; ".join(output)
|
||||
|
||||
@@ -640,7 +629,7 @@ class Cookie(dict):
|
||||
self._set_value("path", value)
|
||||
|
||||
@property
|
||||
def expires(self) -> Optional[datetime]: # no cov
|
||||
def expires(self) -> datetime | None: # no cov
|
||||
"""The expiration date of the cookie. Defaults to `None`."""
|
||||
return self.get("expires")
|
||||
|
||||
@@ -649,7 +638,7 @@ class Cookie(dict):
|
||||
self._set_value("expires", value)
|
||||
|
||||
@property
|
||||
def comment(self) -> Optional[str]: # no cov
|
||||
def comment(self) -> str | None: # no cov
|
||||
"""A comment for the cookie. Defaults to `None`."""
|
||||
return self.get("comment")
|
||||
|
||||
@@ -658,7 +647,7 @@ class Cookie(dict):
|
||||
self._set_value("comment", value)
|
||||
|
||||
@property
|
||||
def domain(self) -> Optional[str]: # no cov
|
||||
def domain(self) -> str | None: # no cov
|
||||
"""The domain of the cookie. Defaults to `None`."""
|
||||
return self.get("domain")
|
||||
|
||||
@@ -667,7 +656,7 @@ class Cookie(dict):
|
||||
self._set_value("domain", value)
|
||||
|
||||
@property
|
||||
def max_age(self) -> Optional[int]: # no cov
|
||||
def max_age(self) -> int | None: # no cov
|
||||
"""The maximum age of the cookie in seconds. Defaults to `None`."""
|
||||
return self.get("max-age")
|
||||
|
||||
@@ -694,7 +683,7 @@ class Cookie(dict):
|
||||
self._set_value("httponly", value)
|
||||
|
||||
@property
|
||||
def samesite(self) -> Optional[SameSite]: # no cov
|
||||
def samesite(self) -> SameSite | None: # no cov
|
||||
"""The SameSite attribute for the cookie. Defaults to `"Lax"`."""
|
||||
return self.get("samesite")
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import typing as t
|
||||
|
||||
from functools import partial
|
||||
from traceback import extract_tb
|
||||
|
||||
@@ -26,7 +25,6 @@ from sanic.log import deprecation, logger
|
||||
from sanic.pages.error import ErrorPage
|
||||
from sanic.response import html, json, text
|
||||
|
||||
|
||||
dumps: t.Callable[..., str]
|
||||
try:
|
||||
from ujson import dumps
|
||||
@@ -73,7 +71,7 @@ class BaseRenderer:
|
||||
self.debug = debug
|
||||
|
||||
@property
|
||||
def headers(self) -> t.Dict[str, str]:
|
||||
def headers(self) -> dict[str, str]:
|
||||
"""The headers to be used for the response."""
|
||||
if isinstance(self.exception, SanicException):
|
||||
return getattr(self.exception, "headers", {})
|
||||
@@ -192,8 +190,7 @@ class TextRenderer(BaseRenderer):
|
||||
lines += [
|
||||
f"{self.exception.__class__.__name__}: {self.exception} while "
|
||||
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:
|
||||
@@ -326,8 +323,8 @@ def exception_response(
|
||||
exception: Exception,
|
||||
debug: bool,
|
||||
fallback: str,
|
||||
base: t.Type[BaseRenderer],
|
||||
renderer: t.Optional[t.Type[BaseRenderer]] = None,
|
||||
base: type[BaseRenderer],
|
||||
renderer: type[BaseRenderer] | None = None,
|
||||
) -> HTTPResponse:
|
||||
"""Render a response for the default FALLBACK exception handler."""
|
||||
if not renderer:
|
||||
@@ -390,9 +387,7 @@ def guess_mime(req: Request, fallback: str) -> str:
|
||||
if m:
|
||||
format = CONFIG_BY_MIME[m.mime]
|
||||
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:
|
||||
logger.debug(f"No format found, the client accepts {req.accept!r}")
|
||||
return m.mime
|
||||
|
||||
@@ -69,9 +69,7 @@ class SanicException(Exception):
|
||||
) -> None:
|
||||
self.context = context
|
||||
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)
|
||||
headers = headers or getattr(self.__class__, "headers", {})
|
||||
if message is None:
|
||||
@@ -621,9 +619,7 @@ class Unauthorized(HTTPException):
|
||||
|
||||
# if auth-scheme is specified, set "WWW-Authenticate" header
|
||||
if scheme is not None:
|
||||
values = [
|
||||
'{!s}="{!s}"'.format(k, v) for k, v in challenges.items()
|
||||
]
|
||||
values = [f'{k!s}="{v!s}"' for k, v in challenges.items()]
|
||||
challenge = ", ".join(values)
|
||||
|
||||
self.headers = {
|
||||
|
||||
@@ -2,7 +2,6 @@ from .content_range import ContentRangeHandler
|
||||
from .directory import DirectoryHandler
|
||||
from .error import ErrorHandler
|
||||
|
||||
|
||||
__all__ = (
|
||||
"ContentRangeHandler",
|
||||
"DirectoryHandler",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sanic.exceptions import (
|
||||
@@ -11,7 +10,6 @@ from sanic.exceptions import (
|
||||
)
|
||||
from sanic.models.protocol_types import Range
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sanic import Request
|
||||
|
||||
@@ -33,27 +31,19 @@ class ContentRangeHandler(Range):
|
||||
raise HeaderNotFound("Range Header Not Found")
|
||||
unit, _, value = tuple(map(str.strip, _range.partition("=")))
|
||||
if unit != "bytes":
|
||||
raise InvalidRangeType(
|
||||
"%s is not a valid Range Type" % (unit,), self
|
||||
)
|
||||
raise InvalidRangeType(f"{unit} is not a valid Range Type", self)
|
||||
start_b, _, end_b = tuple(map(str.strip, value.partition("-")))
|
||||
try:
|
||||
self.start = int(start_b) if start_b else None
|
||||
except ValueError:
|
||||
raise RangeNotSatisfiable(
|
||||
"'%s' is invalid for Content Range" % (start_b,), self
|
||||
)
|
||||
raise RangeNotSatisfiable(f"'{start_b}' is invalid for Content Range", self)
|
||||
try:
|
||||
self.end = int(end_b) if end_b else None
|
||||
except ValueError:
|
||||
raise RangeNotSatisfiable(
|
||||
"'%s' is invalid for Content Range" % (end_b,), self
|
||||
)
|
||||
raise RangeNotSatisfiable(f"'{end_b}' is invalid for Content Range", self)
|
||||
if self.end is None:
|
||||
if self.start is None:
|
||||
raise RangeNotSatisfiable(
|
||||
"Invalid for Content Range parameters", self
|
||||
)
|
||||
raise RangeNotSatisfiable("Invalid for Content Range parameters", self)
|
||||
else:
|
||||
# this case represents `Content-Range: bytes 5-`
|
||||
self.end = self.total - 1
|
||||
@@ -63,14 +53,9 @@ class ContentRangeHandler(Range):
|
||||
self.start = self.total - self.end
|
||||
self.end = self.total - 1
|
||||
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.headers = {
|
||||
"Content-Range": "bytes %s-%s/%s"
|
||||
% (self.start, self.end, self.total)
|
||||
}
|
||||
self.headers = {"Content-Range": f"bytes {self.start}-{self.end}/{self.total}"}
|
||||
|
||||
def __bool__(self):
|
||||
return hasattr(self, "size") and self.size > 0
|
||||
|
||||
@@ -4,7 +4,7 @@ from datetime import datetime
|
||||
from operator import itemgetter
|
||||
from pathlib import Path
|
||||
from stat import S_ISDIR
|
||||
from typing import Dict, Iterable, Optional, Sequence, Union, cast
|
||||
from typing import Iterable, Sequence, cast
|
||||
|
||||
from sanic.exceptions import NotFound
|
||||
from sanic.pages.directory_page import DirectoryPage, FileInfo
|
||||
@@ -28,7 +28,7 @@ class DirectoryHandler:
|
||||
uri: str,
|
||||
directory: Path,
|
||||
directory_view: bool = False,
|
||||
index: Optional[Union[str, Sequence[str]]] = None,
|
||||
index: str | Sequence[str] | None = None,
|
||||
) -> None:
|
||||
if isinstance(index, str):
|
||||
index = [index]
|
||||
@@ -60,9 +60,7 @@ class DirectoryHandler:
|
||||
return await file(index_file)
|
||||
|
||||
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:
|
||||
raise NotFound("File not found")
|
||||
@@ -72,20 +70,16 @@ class DirectoryHandler:
|
||||
def _index(self, location: Path, path: str, debug: bool):
|
||||
# Remove empty path elements, append slash
|
||||
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
|
||||
page = DirectoryPage(self._iter_files(location), path, debug)
|
||||
return html(page.render())
|
||||
|
||||
def _prepare_file(self, path: Path) -> Dict[str, Union[int, str]]:
|
||||
def _prepare_file(self, path: Path) -> dict[str, int | str]:
|
||||
stat = path.stat()
|
||||
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)
|
||||
icon = "📁" if is_dir else "📄"
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict, List, Optional, Tuple, Type
|
||||
|
||||
from sanic.errorpages import BaseRenderer, TextRenderer, exception_response
|
||||
from sanic.exceptions import ServerError
|
||||
from sanic.log import error_logger
|
||||
@@ -25,20 +23,20 @@ class ErrorHandler:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
base: Type[BaseRenderer] = TextRenderer,
|
||||
base: type[BaseRenderer] = TextRenderer,
|
||||
):
|
||||
self.cached_handlers: Dict[
|
||||
Tuple[Type[BaseException], Optional[str]], Optional[RouteHandler]
|
||||
self.cached_handlers: dict[
|
||||
tuple[type[BaseException], str | None], RouteHandler | None
|
||||
] = {}
|
||||
self.debug = False
|
||||
self.base = base
|
||||
|
||||
def _full_lookup(self, exception, route_name: Optional[str] = None):
|
||||
def _full_lookup(self, exception, route_name: str | None = None):
|
||||
return self.lookup(exception, route_name)
|
||||
|
||||
def _add(
|
||||
self,
|
||||
key: Tuple[Type[BaseException], Optional[str]],
|
||||
key: tuple[type[BaseException], str | None],
|
||||
handler: RouteHandler,
|
||||
) -> None:
|
||||
if key in self.cached_handlers:
|
||||
@@ -53,7 +51,7 @@ class ErrorHandler:
|
||||
raise ServerError(message)
|
||||
self.cached_handlers[key] = handler
|
||||
|
||||
def add(self, exception, handler, route_names: Optional[List[str]] = None):
|
||||
def add(self, exception, handler, route_names: list[str] | None = None):
|
||||
"""Add a new exception handler to an already existing handler object.
|
||||
|
||||
Args:
|
||||
@@ -72,7 +70,7 @@ class ErrorHandler:
|
||||
else:
|
||||
self._add((exception, None), handler)
|
||||
|
||||
def lookup(self, exception, route_name: Optional[str] = None):
|
||||
def lookup(self, exception, route_name: str | None = None):
|
||||
"""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.
|
||||
@@ -98,9 +96,7 @@ class ErrorHandler:
|
||||
exception_key = (ancestor, name)
|
||||
if exception_key in self.cached_handlers:
|
||||
handler = self.cached_handlers[exception_key]
|
||||
self.cached_handlers[
|
||||
(exception_class, route_name)
|
||||
] = handler
|
||||
self.cached_handlers[(exception_class, route_name)] = handler
|
||||
return handler
|
||||
|
||||
if ancestor is BaseException:
|
||||
@@ -135,13 +131,11 @@ class ErrorHandler:
|
||||
url = repr(request.url)
|
||||
except AttributeError: # no cov
|
||||
url = "unknown"
|
||||
response_message = (
|
||||
"Exception raised in exception handler " '"%s" for uri: %s'
|
||||
)
|
||||
error_logger.exception(response_message, handler.__name__, url)
|
||||
response_message = f'Exception raised in exception handler "{handler.__name__}" for uri: {url}'
|
||||
error_logger.exception(response_message)
|
||||
|
||||
if self.debug:
|
||||
return text(response_message % (handler.__name__, url), 500)
|
||||
return text(response_message, 500)
|
||||
else:
|
||||
return text("An error occurred while handling an error", 500)
|
||||
return response
|
||||
@@ -200,6 +194,4 @@ class ErrorHandler:
|
||||
except AttributeError: # no cov
|
||||
url = "unknown"
|
||||
|
||||
error_logger.exception(
|
||||
"Exception occurred while handling uri: %s", url
|
||||
)
|
||||
error_logger.exception("Exception occurred while handling uri: %s", url)
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Union
|
||||
from typing import Any, Dict, Iterable, Tuple, Union
|
||||
from urllib.parse import unquote
|
||||
|
||||
from sanic.exceptions import InvalidHeader
|
||||
from sanic.helpers import STATUS_CODES
|
||||
|
||||
|
||||
# TODO:
|
||||
# - the Options object should be a typed object to allow for less casting
|
||||
# across the application (in request.py for example)
|
||||
@@ -21,9 +19,7 @@ _token, _quoted = r"([\w!#$%&'*+\-.^_`|~]+)", r'"([^"]*)"'
|
||||
_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_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
|
||||
# curl all have different escaping, that we try to handle as well as possible,
|
||||
@@ -85,8 +81,8 @@ class MediaType:
|
||||
|
||||
def match(
|
||||
self,
|
||||
mime_with_params: Union[str, MediaType],
|
||||
) -> Optional[MediaType]:
|
||||
mime_with_params: str | MediaType,
|
||||
) -> MediaType | None:
|
||||
"""Match this media type against another media type.
|
||||
|
||||
Check if this media type matches the given mime type/subtype.
|
||||
@@ -124,9 +120,7 @@ class MediaType:
|
||||
or mt.subtype == "*"
|
||||
)
|
||||
# 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
|
||||
)
|
||||
@@ -141,7 +135,7 @@ class MediaType:
|
||||
return any(part == "*" for part in (self.subtype, self.type))
|
||||
|
||||
@classmethod
|
||||
def _parse(cls, mime_with_params: str) -> Optional[MediaType]:
|
||||
def _parse(cls, mime_with_params: str) -> MediaType | None:
|
||||
mtype = mime_with_params.strip()
|
||||
if "/" not in mime_with_params:
|
||||
return None
|
||||
@@ -151,12 +145,10 @@ class MediaType:
|
||||
if not type_ or not subtype:
|
||||
raise ValueError(f"Invalid media type: {mtype}")
|
||||
|
||||
params = dict(
|
||||
[
|
||||
(key.strip(), value.strip())
|
||||
for key, value in (param.split("=", 1) for param in raw_params)
|
||||
]
|
||||
)
|
||||
params = {
|
||||
key.strip(): value.strip()
|
||||
for key, value in (param.split("=", 1) for param in raw_params)
|
||||
}
|
||||
|
||||
return cls(type_.lstrip(), subtype.rstrip(), **params)
|
||||
|
||||
@@ -173,7 +165,7 @@ class Matched:
|
||||
header (MediaType): The header to match against, if any.
|
||||
"""
|
||||
|
||||
def __init__(self, mime: str, header: Optional[MediaType]):
|
||||
def __init__(self, mime: str, header: MediaType | None):
|
||||
self.mime = mime
|
||||
self.header = header
|
||||
|
||||
@@ -200,7 +192,7 @@ class Matched:
|
||||
)
|
||||
)
|
||||
|
||||
def _compare(self, other) -> Tuple[bool, Matched]:
|
||||
def _compare(self, other) -> tuple[bool, Matched]:
|
||||
if isinstance(other, str):
|
||||
parsed = Matched.parse(other)
|
||||
if self.mime == other:
|
||||
@@ -215,7 +207,7 @@ class Matched:
|
||||
f"mime types of '{self.mime}' and '{other}'"
|
||||
)
|
||||
|
||||
def match(self, other: Union[str, Matched]) -> Optional[Matched]:
|
||||
def match(self, other: str | Matched) -> Matched | None:
|
||||
"""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.
|
||||
@@ -296,7 +288,7 @@ class AcceptList(list):
|
||||
return ", ".join(str(m) for m in self)
|
||||
|
||||
|
||||
def parse_accept(accept: Optional[str]) -> AcceptList:
|
||||
def parse_accept(accept: str | None) -> AcceptList:
|
||||
"""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
|
||||
@@ -316,9 +308,7 @@ def parse_accept(accept: Optional[str]) -> AcceptList:
|
||||
accept = "*/*" # No header means that all types are accepted
|
||||
try:
|
||||
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:
|
||||
raise ValueError
|
||||
@@ -327,7 +317,7 @@ def parse_accept(accept: Optional[str]) -> AcceptList:
|
||||
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.
|
||||
|
||||
E.g. `form-data; name=upload; filename="file.txt"` to
|
||||
@@ -346,7 +336,7 @@ def parse_content_header(value: str) -> Tuple[str, Options]:
|
||||
"""
|
||||
pos = value.find(";")
|
||||
if pos == -1:
|
||||
options: Dict[str, Union[int, str]] = {}
|
||||
options: dict[str, int | str] = {}
|
||||
else:
|
||||
options = {
|
||||
m.group(1).lower(): (m.group(2) or m.group(3))
|
||||
@@ -366,7 +356,7 @@ def parse_content_header(value: str) -> Tuple[str, Options]:
|
||||
_rparam = re.compile(f"(?:{_token}|{_quoted})={_token}\\s*($|[;,])", re.ASCII)
|
||||
|
||||
|
||||
def parse_forwarded(headers, config) -> Optional[Options]:
|
||||
def parse_forwarded(headers, config) -> Options | None:
|
||||
"""Parse RFC 7239 Forwarded headers.
|
||||
The value of `by` or `secret` must match `config.FORWARDED_SECRET`
|
||||
:return: dict with keys and values, or None if nothing matched
|
||||
@@ -380,7 +370,7 @@ def parse_forwarded(headers, config) -> Optional[Options]:
|
||||
return None
|
||||
# Loop over <separator><key>=<value> elements from right to left
|
||||
sep = pos = None
|
||||
options: List[Tuple[str, str]] = []
|
||||
options: list[tuple[str, str]] = []
|
||||
found = False
|
||||
for m in _rparam.finditer(header[::-1]):
|
||||
# Start of new element? (on parser skips and non-semicolon right sep)
|
||||
@@ -404,7 +394,7 @@ def parse_forwarded(headers, config) -> Optional[Options]:
|
||||
return fwd_normalize(reversed(options)) if found else None
|
||||
|
||||
|
||||
def parse_xforwarded(headers, config) -> Optional[Options]:
|
||||
def parse_xforwarded(headers, config) -> Options | None:
|
||||
"""Parse traditional proxy headers."""
|
||||
real_ip_header = config.REAL_IP_HEADER
|
||||
proxies_count = config.PROXIES_COUNT
|
||||
@@ -415,11 +405,7 @@ def parse_xforwarded(headers, config) -> Optional[Options]:
|
||||
# Combine, split and filter multiple headers' entries
|
||||
forwarded_for = headers.getall(config.FORWARDED_FOR_HEADER)
|
||||
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]
|
||||
except (KeyError, IndexError):
|
||||
@@ -451,7 +437,7 @@ def fwd_normalize(fwd: OptionsIterable) -> Options:
|
||||
Returns:
|
||||
Options: A dict of normalized key-value pairs.
|
||||
"""
|
||||
ret: Dict[str, Union[int, str]] = {}
|
||||
ret: dict[str, int | str] = {}
|
||||
for key, val in fwd:
|
||||
if val is not None:
|
||||
try:
|
||||
@@ -488,7 +474,7 @@ def fwd_normalize_address(addr: str) -> str:
|
||||
return addr.lower()
|
||||
|
||||
|
||||
def parse_host(host: str) -> Tuple[Optional[str], Optional[int]]:
|
||||
def parse_host(host: str) -> tuple[str | None, int | None]:
|
||||
"""Split host:port into hostname and port.
|
||||
|
||||
Args:
|
||||
@@ -530,9 +516,9 @@ def format_http1_response(status: int, headers: HeaderBytesIterable) -> bytes:
|
||||
|
||||
|
||||
def parse_credentials(
|
||||
header: Optional[str],
|
||||
prefixes: Optional[Union[List, Tuple, Set]] = None,
|
||||
) -> Tuple[Optional[str], Optional[str]]:
|
||||
header: str | None,
|
||||
prefixes: list | tuple | set | None = None,
|
||||
) -> tuple[str | None, str | None]:
|
||||
"""Parses any header with the aim to retrieve any credentials from it.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
"""Defines basics of HTTP standard."""
|
||||
|
||||
import sys
|
||||
|
||||
from importlib import import_module
|
||||
from inspect import ismodule
|
||||
from typing import Dict
|
||||
|
||||
|
||||
STATUS_CODES: Dict[int, bytes] = {
|
||||
100: b"Continue",
|
||||
101: b"Switching Protocols",
|
||||
@@ -132,7 +130,7 @@ def remove_entity_headers(headers, allowed=("content-location", "expires")):
|
||||
|
||||
returns the headers without the entity headers
|
||||
"""
|
||||
allowed = set([h.lower() for h in allowed])
|
||||
allowed = {h.lower() for h in allowed}
|
||||
headers = {
|
||||
header: value
|
||||
for header, value in headers.items()
|
||||
|
||||
@@ -2,5 +2,4 @@ from .constants import Stage
|
||||
from .http1 import Http
|
||||
from .http3 import Http3
|
||||
|
||||
|
||||
__all__ = ("Http", "Stage", "Http3")
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sanic.request import Request
|
||||
@@ -25,7 +24,6 @@ from sanic.http.stream import Stream
|
||||
from sanic.log import access_logger, error_logger, logger
|
||||
from sanic.touchup import TouchUpMeta
|
||||
|
||||
|
||||
HTTP_CONTINUE = b"HTTP/1.1 100 Continue\r\n\r\n"
|
||||
|
||||
|
||||
@@ -363,26 +361,20 @@ class Http(Stream, metaclass=TouchUpMeta):
|
||||
self.response_func = None
|
||||
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."""
|
||||
# Chunked encoding
|
||||
size = len(data)
|
||||
if end_stream:
|
||||
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.stage = Stage.IDLE
|
||||
elif size:
|
||||
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."""
|
||||
bytes_left = self.response_bytes_left - len(data)
|
||||
if bytes_left <= 0:
|
||||
@@ -420,9 +412,7 @@ class Http(Stream, metaclass=TouchUpMeta):
|
||||
exception, (ServiceUnavailable, RequestCancelled)
|
||||
)
|
||||
try:
|
||||
await app.handle_exception(
|
||||
self.request, exception, request_middleware
|
||||
)
|
||||
await app.handle_exception(self.request, exception, request_middleware)
|
||||
except Exception as e:
|
||||
await app.handle_exception(self.request, e, False)
|
||||
|
||||
@@ -481,7 +471,7 @@ class Http(Stream, metaclass=TouchUpMeta):
|
||||
if data:
|
||||
yield data
|
||||
|
||||
async def read(self) -> Optional[bytes]: # no cov
|
||||
async def read(self) -> bytes | None: # no cov
|
||||
"""Read some bytes of request body."""
|
||||
|
||||
# Send a 100-continue if needed
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from ssl import SSLContext
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
List,
|
||||
Optional,
|
||||
Tuple,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
@@ -32,7 +27,6 @@ from sanic.log import Colors, logger
|
||||
from sanic.models.protocol_types import TransportProtocol
|
||||
from sanic.models.server_types import ConnInfo
|
||||
|
||||
|
||||
try:
|
||||
from aioquic.h0.connection import H0_ALPN, H0Connection
|
||||
from aioquic.h3.connection import H3_ALPN, H3Connection
|
||||
@@ -71,10 +65,7 @@ class HTTP3Transport(TransportProtocol):
|
||||
return self._protocol
|
||||
|
||||
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)
|
||||
elif info == "network_paths":
|
||||
return self._protocol._quic._network_paths
|
||||
@@ -109,19 +100,18 @@ class HTTPReceiver(Receiver, Stream):
|
||||
self.request_body = None
|
||||
self.stage = Stage.IDLE
|
||||
self.headers_sent = False
|
||||
self.response: Optional[BaseHTTPResponse] = None
|
||||
self.response: BaseHTTPResponse | None = None
|
||||
self.request_max_size = self.protocol.request_max_size
|
||||
self.request_bytes = 0
|
||||
|
||||
async def run(self, exception: Optional[Exception] = None):
|
||||
async def run(self, exception: Exception | None = None):
|
||||
"""Handle the request and response cycle."""
|
||||
self.stage = Stage.HANDLER
|
||||
self.head_only = self.request.method.upper() == "HEAD"
|
||||
|
||||
if exception:
|
||||
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,
|
||||
extra={"verbosity": 1},
|
||||
)
|
||||
@@ -146,17 +136,13 @@ class HTTPReceiver(Receiver, Stream):
|
||||
|
||||
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
|
||||
headers = response.headers
|
||||
status = response.status
|
||||
|
||||
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("transfer-encoding", None)
|
||||
@@ -249,11 +235,7 @@ class HTTPReceiver(Receiver, Stream):
|
||||
):
|
||||
size = len(data)
|
||||
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:
|
||||
data = b"%x\r\n%b\r\n" % (size, data)
|
||||
|
||||
@@ -304,7 +286,7 @@ class Http3:
|
||||
) -> None:
|
||||
self.protocol = protocol
|
||||
self.transmit = transmit
|
||||
self.receivers: Dict[int, Receiver] = {}
|
||||
self.receivers: dict[int, Receiver] = {}
|
||||
|
||||
def http_event_received(self, event: H3Event) -> None:
|
||||
logger.debug( # no cov
|
||||
@@ -330,11 +312,8 @@ class Http3:
|
||||
extra={"verbosity": 2},
|
||||
)
|
||||
|
||||
def get_or_make_receiver(self, event: H3Event) -> Tuple[Receiver, bool]:
|
||||
if (
|
||||
isinstance(event, HeadersReceived)
|
||||
and event.stream_id not in self.receivers
|
||||
):
|
||||
def get_or_make_receiver(self, event: H3Event) -> tuple[Receiver, bool]:
|
||||
if isinstance(event, HeadersReceived) and event.stream_id not in self.receivers:
|
||||
request = self._make_request(event)
|
||||
receiver = HTTPReceiver(self.transmit, self.protocol, request)
|
||||
request.stream = receiver
|
||||
@@ -357,9 +336,7 @@ class Http3:
|
||||
)
|
||||
)
|
||||
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"]
|
||||
path = headers[":path"]
|
||||
scheme = headers.pop(":scheme", "")
|
||||
@@ -396,18 +373,16 @@ class SessionTicketStore:
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.tickets: Dict[bytes, SessionTicket] = {}
|
||||
self.tickets: dict[bytes, SessionTicket] = {}
|
||||
|
||||
def add(self, ticket: SessionTicket) -> None:
|
||||
self.tickets[ticket.ticket] = ticket
|
||||
|
||||
def pop(self, label: bytes) -> Optional[SessionTicket]:
|
||||
def pop(self, label: bytes) -> SessionTicket | None:
|
||||
return self.tickets.pop(label, None)
|
||||
|
||||
|
||||
def get_config(
|
||||
app: Sanic, ssl: Union[SanicSSLContext, CertSelector, SSLContext]
|
||||
):
|
||||
def get_config(app: Sanic, ssl: SanicSSLContext | CertSelector | SSLContext):
|
||||
# TODO:
|
||||
# - proper selection needed if service with multiple certs insted of
|
||||
# just taking the first
|
||||
@@ -430,8 +405,6 @@ def get_config(
|
||||
)
|
||||
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
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Optional, Tuple, Union
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sanic.http.constants import Stage
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sanic.response import BaseHTTPResponse
|
||||
from sanic.server.protocols.http_protocol import HttpProtocol
|
||||
@@ -12,16 +11,14 @@ if TYPE_CHECKING:
|
||||
|
||||
class Stream:
|
||||
stage: Stage
|
||||
response: Optional[BaseHTTPResponse]
|
||||
response: BaseHTTPResponse | None
|
||||
protocol: HttpProtocol
|
||||
url: Optional[str]
|
||||
request_body: Optional[bytes]
|
||||
request_max_size: Union[int, float]
|
||||
url: str | None
|
||||
request_body: bytes | None
|
||||
request_max_size: int | float
|
||||
|
||||
__touchup__: Tuple[str, ...] = tuple()
|
||||
__touchup__: tuple[str, ...] = ()
|
||||
__slots__ = ("request_max_size",)
|
||||
|
||||
def respond(
|
||||
self, response: BaseHTTPResponse
|
||||
) -> BaseHTTPResponse: # no cov
|
||||
def respond(self, response: BaseHTTPResponse) -> BaseHTTPResponse: # no cov
|
||||
raise NotImplementedError("Not implemented")
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from .context import process_to_context
|
||||
from .creators import get_ssl_context
|
||||
|
||||
|
||||
__all__ = ("get_ssl_context", "process_to_context")
|
||||
|
||||
@@ -2,12 +2,10 @@ from __future__ import annotations
|
||||
|
||||
import os
|
||||
import ssl
|
||||
|
||||
from typing import Any, Dict, Iterable, Optional, Union
|
||||
from typing import Any, Iterable
|
||||
|
||||
from sanic.log import logger
|
||||
|
||||
|
||||
# Only allow secure ciphers, notably leaving out AES-CBC mode
|
||||
# OpenSSL chooses ECDSA or RSA depending on the cert in use
|
||||
CIPHERS_TLS12 = [
|
||||
@@ -19,11 +17,14 @@ CIPHERS_TLS12 = [
|
||||
"ECDHE-RSA-AES128-GCM-SHA256",
|
||||
]
|
||||
|
||||
TlsDef = None | ssl.SSLContext | dict[str, Any] | str
|
||||
TlsDefs = TlsDef | list[TlsDef] | tuple[TlsDef, ...]
|
||||
|
||||
|
||||
def create_context(
|
||||
certfile: Optional[str] = None,
|
||||
keyfile: Optional[str] = None,
|
||||
password: Optional[str] = None,
|
||||
certfile: str | None = None,
|
||||
keyfile: str | None = None,
|
||||
password: str | None = None,
|
||||
purpose: ssl.Purpose = ssl.Purpose.CLIENT_AUTH,
|
||||
) -> ssl.SSLContext:
|
||||
"""Create a context with secure crypto and HTTP/1.1 in protocols."""
|
||||
@@ -38,9 +39,7 @@ def create_context(
|
||||
return context
|
||||
|
||||
|
||||
def shorthand_to_ctx(
|
||||
ctxdef: Union[None, ssl.SSLContext, dict, str]
|
||||
) -> Optional[ssl.SSLContext]:
|
||||
def shorthand_to_ctx(ctxdef: TlsDef) -> ssl.SSLContext | None:
|
||||
"""Convert an ssl argument shorthand to an SSLContext object."""
|
||||
if ctxdef is None or isinstance(ctxdef, ssl.SSLContext):
|
||||
return ctxdef
|
||||
@@ -54,9 +53,7 @@ def shorthand_to_ctx(
|
||||
)
|
||||
|
||||
|
||||
def process_to_context(
|
||||
ssldef: Union[None, ssl.SSLContext, dict, str, list, tuple]
|
||||
) -> Optional[ssl.SSLContext]:
|
||||
def process_to_context(ssldef: TlsDefs) -> ssl.SSLContext | None:
|
||||
"""Process app.run ssl argument from easy formats to full SSLContext."""
|
||||
return (
|
||||
CertSelector(map(shorthand_to_ctx, ssldef))
|
||||
@@ -71,13 +68,9 @@ def load_cert_dir(p: str) -> ssl.SSLContext:
|
||||
keyfile = os.path.join(p, "privkey.pem")
|
||||
certfile = os.path.join(p, "fullchain.pem")
|
||||
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):
|
||||
raise ValueError(
|
||||
f"Certificate not found or permission denied {certfile}"
|
||||
)
|
||||
raise ValueError(f"Certificate not found or permission denied {certfile}")
|
||||
return CertSimple(certfile, keyfile)
|
||||
|
||||
|
||||
@@ -89,9 +82,7 @@ def find_cert(self: CertSelector, server_name: str):
|
||||
if not server_name:
|
||||
if 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:
|
||||
if match_hostname(ctx, server_name):
|
||||
return ctx
|
||||
@@ -100,9 +91,7 @@ def find_cert(self: CertSelector, server_name: str):
|
||||
raise ValueError(f"No certificate found matching hostname {server_name!r}")
|
||||
|
||||
|
||||
def match_hostname(
|
||||
ctx: Union[ssl.SSLContext, CertSelector], hostname: str
|
||||
) -> bool:
|
||||
def match_hostname(ctx: ssl.SSLContext | CertSelector, hostname: str) -> bool:
|
||||
"""Match names from CertSelector against a received hostname."""
|
||||
# Local certs are considered trusted, so this can be less pedantic
|
||||
# and thus faster than the deprecated ssl.match_hostname function is.
|
||||
@@ -119,7 +108,7 @@ def match_hostname(
|
||||
|
||||
def selector_sni_callback(
|
||||
sslobj: ssl.SSLObject, server_name: str, ctx: CertSelector
|
||||
) -> Optional[int]:
|
||||
) -> int | None:
|
||||
"""Select a certificate matching the SNI."""
|
||||
# Call server_name_callback to store the SNI on sslobj
|
||||
server_name_callback(sslobj, server_name, ctx)
|
||||
@@ -142,7 +131,7 @@ def server_name_callback(
|
||||
|
||||
|
||||
class SanicSSLContext(ssl.SSLContext):
|
||||
sanic: Dict[str, os.PathLike]
|
||||
sanic: dict[str, os.PathLike]
|
||||
|
||||
@classmethod
|
||||
def create_from_ssl_context(cls, context: ssl.SSLContext):
|
||||
@@ -153,7 +142,7 @@ class SanicSSLContext(ssl.SSLContext):
|
||||
class CertSimple(SanicSSLContext):
|
||||
"""A wrapper for creating SSLContext with a sanic attribute."""
|
||||
|
||||
sanic: Dict[str, Any]
|
||||
sanic: dict[str, Any]
|
||||
|
||||
def __new__(cls, cert, key, **kw):
|
||||
# try common aliases, rename to cert/key
|
||||
@@ -166,9 +155,7 @@ class CertSimple(SanicSSLContext):
|
||||
if "names" not in kw:
|
||||
cert = ssl._ssl._test_decode_cert(certfile) # type: ignore
|
||||
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}
|
||||
self = create_context(certfile, keyfile, password)
|
||||
@@ -190,7 +177,7 @@ class CertSelector(ssl.SSLContext):
|
||||
def __new__(cls, ctxs):
|
||||
return super().__new__(cls)
|
||||
|
||||
def __init__(self, ctxs: Iterable[Optional[ssl.SSLContext]]):
|
||||
def __init__(self, ctxs: Iterable[ssl.SSLContext | None]):
|
||||
super().__init__()
|
||||
self.sni_callback = selector_sni_callback # type: ignore
|
||||
self.sanic_select = []
|
||||
@@ -205,7 +192,5 @@ class CertSelector(ssl.SSLContext):
|
||||
if i == 0:
|
||||
self.sanic_fallback = ctx
|
||||
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)}")
|
||||
|
||||
@@ -3,13 +3,12 @@ from __future__ import annotations
|
||||
import ssl
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from contextlib import suppress
|
||||
from pathlib import Path
|
||||
from tempfile import mkdtemp
|
||||
from types import ModuleType
|
||||
from typing import TYPE_CHECKING, Optional, Tuple, Type, Union, cast
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from sanic.application.constants import Mode
|
||||
from sanic.application.spinner import loading
|
||||
@@ -22,7 +21,6 @@ from sanic.exceptions import SanicException
|
||||
from sanic.helpers import Default
|
||||
from sanic.http.tls.context import CertSimple, SanicSSLContext
|
||||
|
||||
|
||||
try:
|
||||
import trustme
|
||||
|
||||
@@ -47,7 +45,7 @@ CIPHERS_TLS12 = [
|
||||
]
|
||||
|
||||
|
||||
def _make_path(maybe_path: Union[Path, str], tmpdir: Optional[Path]) -> Path:
|
||||
def _make_path(maybe_path: Path | str, tmpdir: Path | None) -> Path:
|
||||
if isinstance(maybe_path, Path):
|
||||
return maybe_path
|
||||
else:
|
||||
@@ -60,9 +58,7 @@ def _make_path(maybe_path: Union[Path, str], tmpdir: Optional[Path]) -> Path:
|
||||
return path
|
||||
|
||||
|
||||
def get_ssl_context(
|
||||
app: Sanic, ssl: Optional[ssl.SSLContext]
|
||||
) -> ssl.SSLContext:
|
||||
def get_ssl_context(app: Sanic, ssl: ssl.SSLContext | None) -> ssl.SSLContext:
|
||||
if ssl:
|
||||
return ssl
|
||||
|
||||
@@ -96,16 +92,8 @@ class CertCreator(ABC):
|
||||
if isinstance(self.key, Default) or isinstance(self.cert, Default):
|
||||
self.tmpdir = Path(mkdtemp())
|
||||
|
||||
key = (
|
||||
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
|
||||
)
|
||||
key = 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.cert_path = _make_path(cert, self.tmpdir)
|
||||
@@ -126,11 +114,9 @@ class CertCreator(ABC):
|
||||
local_tls_key,
|
||||
local_tls_cert,
|
||||
) -> CertCreator:
|
||||
creator: Optional[CertCreator] = None
|
||||
creator: CertCreator | None = None
|
||||
|
||||
cert_creator_options: Tuple[
|
||||
Tuple[Type[CertCreator], LocalCertCreator], ...
|
||||
] = (
|
||||
cert_creator_options: tuple[tuple[type[CertCreator], LocalCertCreator], ...] = (
|
||||
(MkcertCreator, LocalCertCreator.MKCERT),
|
||||
(TrustmeCreator, LocalCertCreator.TRUSTME),
|
||||
)
|
||||
@@ -160,8 +146,8 @@ class CertCreator(ABC):
|
||||
@staticmethod
|
||||
def _try_select(
|
||||
app: Sanic,
|
||||
creator: Optional[CertCreator],
|
||||
creator_class: Type[CertCreator],
|
||||
creator: CertCreator | None,
|
||||
creator_class: type[CertCreator],
|
||||
creator_requirement: LocalCertCreator,
|
||||
creator_requested: LocalCertCreator,
|
||||
local_tls_key,
|
||||
|
||||
18
sanic/log.py
18
sanic/log.py
@@ -1,13 +1,11 @@
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Any, Dict
|
||||
from warnings import warn
|
||||
|
||||
from sanic.helpers import is_atty
|
||||
|
||||
|
||||
# Python 3.11 changed the way Enum formatting works for mixed-in types.
|
||||
if sys.version_info < (3, 11, 0):
|
||||
|
||||
@@ -19,10 +17,10 @@ else:
|
||||
from enum import StrEnum
|
||||
|
||||
|
||||
LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict( # no cov
|
||||
version=1,
|
||||
disable_existing_loggers=False,
|
||||
loggers={
|
||||
LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = { # no cov
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"loggers": {
|
||||
"sanic.root": {"level": "INFO", "handlers": ["console"]},
|
||||
"sanic.error": {
|
||||
"level": "INFO",
|
||||
@@ -43,7 +41,7 @@ LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict( # no cov
|
||||
"qualname": "sanic.server",
|
||||
},
|
||||
},
|
||||
handlers={
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "generic",
|
||||
@@ -60,7 +58,7 @@ LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict( # no cov
|
||||
"stream": sys.stdout,
|
||||
},
|
||||
},
|
||||
formatters={
|
||||
"formatters": {
|
||||
"generic": {
|
||||
"format": "%(asctime)s [%(process)s] [%(levelname)s] %(message)s",
|
||||
"datefmt": "[%Y-%m-%d %H:%M:%S %z]",
|
||||
@@ -68,12 +66,12 @@ LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict( # no cov
|
||||
},
|
||||
"access": {
|
||||
"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]",
|
||||
"class": "logging.Formatter",
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
"""
|
||||
Defult logging configuration
|
||||
"""
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
from collections import deque
|
||||
from enum import IntEnum, auto
|
||||
from itertools import count
|
||||
from typing import Deque, Sequence, Union
|
||||
from typing import Sequence
|
||||
|
||||
from sanic.models.handler_types import MiddlewareType
|
||||
|
||||
@@ -69,9 +69,9 @@ class Middleware:
|
||||
@classmethod
|
||||
def convert(
|
||||
cls,
|
||||
*middleware_collections: Sequence[Union[Middleware, MiddlewareType]],
|
||||
*middleware_collections: Sequence[Middleware | MiddlewareType],
|
||||
location: MiddlewareLocation,
|
||||
) -> Deque[Middleware]:
|
||||
) -> deque[Middleware]:
|
||||
"""Convert middleware collections to a deque of Middleware objects.
|
||||
|
||||
Args:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user