Compare commits

..

17 Commits

Author SHA1 Message Date
L. Kärkkäinen
00493a6954 Format
Some checks failed
CodeQL / Analyze (python) (pull_request) Failing after 1m34s
Coverage check / Check coverage (pull_request) Failing after 29s
Tests / ${{ matrix.config.platform == 'windows-latest' && 'Windows' || 'Linux' }} / Python ${{ matrix.config.python-version }} / tox -e ${{ matrix.config.tox-env }} (map[max-attempts:3 python-version:3.10 tox-env:py310-no-ext]) (pull_request) Failing after 4s
Tests / ${{ matrix.config.platform == 'windows-latest' && 'Windows' || 'Linux' }} / Python ${{ matrix.config.python-version }} / tox -e ${{ matrix.config.tox-env }} (map[max-attempts:3 python-version:3.10 tox-env:py310]) (pull_request) Failing after 4s
Tests / ${{ matrix.config.platform == 'windows-latest' && 'Windows' || 'Linux' }} / Python ${{ matrix.config.python-version }} / tox -e ${{ matrix.config.tox-env }} (map[max-attempts:3 python-version:3.11 tox-env:py311-no-ext]) (pull_request) Failing after 4s
Tests / ${{ matrix.config.platform == 'windows-latest' && 'Windows' || 'Linux' }} / Python ${{ matrix.config.python-version }} / tox -e ${{ matrix.config.tox-env }} (map[max-attempts:3 python-version:3.11 tox-env:py311]) (pull_request) Failing after 4s
Tests / ${{ matrix.config.platform == 'windows-latest' && 'Windows' || 'Linux' }} / Python ${{ matrix.config.python-version }} / tox -e ${{ matrix.config.tox-env }} (map[max-attempts:3 python-version:3.8 tox-env:py38-no-ext]) (pull_request) Failing after 5s
Tests / ${{ matrix.config.platform == 'windows-latest' && 'Windows' || 'Linux' }} / Python ${{ matrix.config.python-version }} / tox -e ${{ matrix.config.tox-env }} (map[max-attempts:3 python-version:3.8 tox-env:py38]) (pull_request) Failing after 4s
Tests / ${{ matrix.config.platform == 'windows-latest' && 'Windows' || 'Linux' }} / Python ${{ matrix.config.python-version }} / tox -e ${{ matrix.config.tox-env }} (map[max-attempts:3 python-version:3.9 tox-env:py39-no-ext]) (pull_request) Failing after 4s
Tests / ${{ matrix.config.platform == 'windows-latest' && 'Windows' || 'Linux' }} / Python ${{ matrix.config.python-version }} / tox -e ${{ matrix.config.tox-env }} (map[max-attempts:3 python-version:3.9 tox-env:py39]) (pull_request) Failing after 4s
Tests / ${{ matrix.config.platform == 'windows-latest' && 'Windows' || 'Linux' }} / Python ${{ matrix.config.python-version }} / tox -e ${{ matrix.config.tox-env }} (map[python-version:3.10 tox-env:lint]) (pull_request) Failing after 4s
Tests / ${{ matrix.config.platform == 'windows-latest' && 'Windows' || 'Linux' }} / Python ${{ matrix.config.python-version }} / tox -e ${{ matrix.config.tox-env }} (map[python-version:3.10 tox-env:security]) (pull_request) Failing after 5s
Tests / ${{ matrix.config.platform == 'windows-latest' && 'Windows' || 'Linux' }} / Python ${{ matrix.config.python-version }} / tox -e ${{ matrix.config.tox-env }} (map[python-version:3.10 tox-env:type-checking]) (pull_request) Failing after 4s
Tests / ${{ matrix.config.platform == 'windows-latest' && 'Windows' || 'Linux' }} / Python ${{ matrix.config.python-version }} / tox -e ${{ matrix.config.tox-env }} (map[python-version:3.11 tox-env:security]) (pull_request) Failing after 4s
Tests / ${{ matrix.config.platform == 'windows-latest' && 'Windows' || 'Linux' }} / Python ${{ matrix.config.python-version }} / tox -e ${{ matrix.config.tox-env }} (map[python-version:3.11 tox-env:type-checking]) (pull_request) Failing after 5s
Tests / ${{ matrix.config.platform == 'windows-latest' && 'Windows' || 'Linux' }} / Python ${{ matrix.config.python-version }} / tox -e ${{ matrix.config.tox-env }} (map[python-version:3.8 tox-env:security]) (pull_request) Failing after 4s
Tests / ${{ matrix.config.platform == 'windows-latest' && 'Windows' || 'Linux' }} / Python ${{ matrix.config.python-version }} / tox -e ${{ matrix.config.tox-env }} (map[python-version:3.8 tox-env:type-checking]) (pull_request) Failing after 5s
Tests / ${{ matrix.config.platform == 'windows-latest' && 'Windows' || 'Linux' }} / Python ${{ matrix.config.python-version }} / tox -e ${{ matrix.config.tox-env }} (map[python-version:3.9 tox-env:security]) (pull_request) Failing after 4s
Tests / ${{ matrix.config.platform == 'windows-latest' && 'Windows' || 'Linux' }} / Python ${{ matrix.config.python-version }} / tox -e ${{ matrix.config.tox-env }} (map[python-version:3.9 tox-env:type-checking]) (pull_request) Failing after 5s
Tests / ${{ matrix.config.platform == 'windows-latest' && 'Windows' || 'Linux' }} / Python ${{ matrix.config.python-version }} / tox -e ${{ matrix.config.tox-env }} (map[ignore-errors:true platform:windows-latest python-version:3.10 tox-env:py310-no-ext]) (pull_request) Has been cancelled
Tests / ${{ matrix.config.platform == 'windows-latest' && 'Windows' || 'Linux' }} / Python ${{ matrix.config.python-version }} / tox -e ${{ matrix.config.tox-env }} (map[ignore-errors:true platform:windows-latest python-version:3.11 tox-env:py310-no-ext]) (pull_request) Has been cancelled
Tests / ${{ matrix.config.platform == 'windows-latest' && 'Windows' || 'Linux' }} / Python ${{ matrix.config.python-version }} / tox -e ${{ matrix.config.tox-env }} (map[ignore-errors:true platform:windows-latest python-version:3.8 tox-env:py38-no-ext]) (pull_request) Has been cancelled
Tests / ${{ matrix.config.platform == 'windows-latest' && 'Windows' || 'Linux' }} / Python ${{ matrix.config.python-version }} / tox -e ${{ matrix.config.tox-env }} (map[ignore-errors:true platform:windows-latest python-version:3.9 tox-env:py39-no-ext]) (pull_request) Has been cancelled
2023-10-25 07:34:16 +01:00
L. Kärkkäinen
c8d0c1bf28 Remove extra parenthesis of type unions 2023-10-25 07:32:01 +01:00
L. Kärkkäinen
6ea5c44566 Typing issues fixed. 2023-10-25 07:11:59 +01:00
L. Kärkkäinen
1afea39cb2 Format with default line-length=88 (was 79 with Sanic). 2023-10-25 04:13:13 +01:00
L. Kärkkäinen
e4daf1ab21 Removed flake8, black and isort; moving towards ruff default settings instead. 2023-10-25 04:12:24 +01:00
L. Kärkkäinen
469cb1663b Update sanic/handlers/error.py
Missed one in previous commit
2023-10-25 02:43:54 +00:00
L. Kärkkäinen
cdc5dd6b75 Hand-fixed typing extra () and changed some formatting to f-strings that Ruff had missed 2023-10-25 02:36:08 +00:00
L. Kärkkäinen
6fac60c6fe Two empty lines after imports also in guide 2023-10-25 03:02:26 +01:00
L. Kärkkäinen
53b7412c01 String literals and type annotations... 2023-10-25 02:59:41 +01:00
L. Kärkkäinen
65ba1942cc Flake8-comprehensions (C4) --fix go brrr 2023-10-25 02:28:52 +01:00
L. Kärkkäinen
9adb6e8ec0 Enable pycodestyle warnings (W) and --fix incorrectly escaped strings. 2023-10-25 02:06:06 +01:00
L. Kärkkäinen
ec35f5f2c8 Fix/ignore all remaining errors. 2023-10-25 02:03:44 +01:00
L. Kärkkäinen
9ae25e6744 ruff --fix # also import sorting 2023-10-25 01:03:35 +01:00
L. Kärkkäinen
758f10c513 ruff --fix --unsafe-fixes # Tests still A-OK! 2023-10-25 00:32:27 +01:00
L. Kärkkäinen
140d27ef96 ruff format 2023-10-25 00:22:00 +01:00
L. Kärkkäinen
209840b771 ruff --fix 2023-10-25 00:21:29 +01:00
L. Kärkkäinen
20fd58b8d7 Add config for Ruff 2023-10-25 00:21:09 +01:00
250 changed files with 1417 additions and 3420 deletions

View File

@@ -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
@@ -19,8 +17,7 @@ import sys
root_directory = os.path.dirname(os.getcwd())
sys.path.insert(0, root_directory)
import sanic
import sanic # noqa: E402
# -- General configuration ------------------------------------------------

View File

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

View File

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

View File

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

View File

@@ -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.

View File

@@ -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")

View File

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

View File

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

View File

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

View File

@@ -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
@@ -69,5 +66,6 @@ async def runner(app: Sanic, app_server: AsyncioServer):
app.is_running = False
app.is_stopping = True
if __name__ == "__main__":
https.run(port=HTTPS_PORT, debug=True)

View File

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

View File

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

View File

@@ -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("/")

View File

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

View File

@@ -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"

View File

@@ -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

View File

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

View File

@@ -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")

View File

@@ -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")

View File

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

View File

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

View File

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

View File

@@ -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")
@@ -35,11 +33,10 @@ async def after_server_stop(app, loop):
async def test(request):
return response.json({"answer": "42"})
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())

View File

@@ -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()],

View File

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

View File

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

View File

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

View File

@@ -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)
# ----------------------------------------------- #

View File

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

View File

@@ -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__":

View File

@@ -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")

View File

@@ -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")

View File

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

View File

@@ -1,12 +1,10 @@
# Sanic Application
See API docs: [sanic.app](/api/sanic.app)
## Instance
.. column::
The most basic building block is the :class:`sanic.app.Sanic` instance. It is not required, but the custom is to instantiate this in a file called `server.py`.
The most basic building block is the `Sanic()` instance. It is not required, but the custom is to instantiate this in a file called `server.py`.
.. column::
@@ -20,36 +18,33 @@ See API docs: [sanic.app](/api/sanic.app)
## Application context
Most applications will have the need to share/reuse data or objects across different parts of the code base. Sanic helps be providing the `ctx` object on application instances. It is a free space for the developer to attach any objects or data that should existe throughout the lifetime of the application.
Most applications will have the need to share/reuse data or objects across different parts of the code base. The most common example is DB connections.
.. column::
The most common pattern is to attach a database instance to the application.
In versions of Sanic prior to v21.3, this was commonly done by attaching an attribute to the application instance
.. column::
```python
# Raises a warning as deprecated feature in 21.3
app = Sanic("MyApp")
app.db = Database()
```
.. column::
Because this can create potential problems with name conflicts, and to be consistent with [request context](./request.md#context) objects, v21.3 introduces application level context object.
.. column::
```python
# Correct way to attach objects to the application
app = Sanic("MyApp")
app.ctx.db = Database()
```
.. column::
While the previous example will work and is illustrative, it is typically considered best practice to attach objects in one of the two application startup [listeners](./listeners).
.. column::
```python
app = Sanic("MyApp")
@app.before_server_start
async def attach_db(app, loop):
app.ctx.db = Database()
```
## App Registry
.. column::
@@ -73,7 +68,7 @@ Most applications will have the need to share/reuse data or objects across diffe
.. column::
If you call `Sanic.get_app("non-existing")` on an app that does not exist, it will raise :class:`sanic.exceptions.SanicException` by default. You can, instead, force the method to return a new instance of Sanic with that name.
If you call `Sanic.get_app("non-existing")` on an app that does not exist, it will raise `SanicException` by default. You can, instead, force the method to return a new instance of Sanic with that name.
.. column::
@@ -124,54 +119,17 @@ Most applications will have the need to share/reuse data or objects across diffe
.. note:: Heads up
Config keys _should_ be uppercase. But, this is mainly by convention, and lowercase will work most of the time.
```python
```
app.config.GOOD = "yay!"
app.config.bad = "boo"
```
There is much [more detail about configuration](../running/configuration.md) later on.
## Factory pattern
Many of the examples in these docs will show the instantiation of the :class:`sanic.app.Sanic` instance in a file called `server.py` in the "global scope" (i.e. not inside a function). This is a common pattern for very simple "hello world" style applications, but it is often beneficial to use a factory pattern instead.
A "factory" is just a function that returns an instance of the object you want to use. This allows you to abstract the instantiation of the object, but also may make it easier to isolate the application instance.
.. column::
A super simple factory pattern could look like this:
.. column::
```python
# ./path/to/server.py
from sanic import Sanic
from .path.to.config import MyConfig
from .path.to.some.blueprint import bp
def create_app(config=MyConfig) -> Sanic:
app = Sanic("MyApp", config=config)
app.blueprint(bp)
return app
```
.. column::
When we get to running Sanic later, you will learn that the Sanic CLI can detect this pattern and use it to run your application.
.. column::
```sh
sanic path.to.server:create_app
```
There is much [more detail about configuration](/guide/deployment/configuration.md) later on.
## Customization
The Sanic application instance can be customized for your application needs in a variety of ways at instantiation.
For complete details, see the [API docs](/api/sanic.app).
The Sanic application instance can be customized for your application needs in a variety of ways at instantiation.
### Custom configuration
@@ -179,7 +137,7 @@ For complete details, see the [API docs](/api/sanic.app).
This simplest form of custom configuration would be to pass your own object directly into that Sanic application instance
If you create a custom configuration object, it is *highly* recommended that you subclass the :class:`sanic.config.Config` option to inherit its behavior. You could use this option for adding properties, or your own set of custom logic.
If you create a custom configuration object, it is *highly* recommended that you subclass the Sanic `Config` option to inherit its behavior. You could use this option for adding properties, or your own set of custom logic.
*Added in v21.6*
@@ -335,7 +293,7 @@ For complete details, see the [API docs](/api/sanic.app).
```python
from orjson import dumps
app = Sanic("MyApp", dumps=dumps)
app = Sanic(__name__, dumps=dumps)
```
### Custom loads function
@@ -351,7 +309,7 @@ For complete details, see the [API docs](/api/sanic.app).
```python
from orjson import loads
app = Sanic("MyApp", loads=loads)
app = Sanic(__name__, loads=loads)
```
@@ -360,7 +318,7 @@ For complete details, see the [API docs](/api/sanic.app).
### Custom typed application
Beginnint in v23.6, the correct type annotation of a default Sanic application instance is:
The correct, default type of a Sanic application instance is:
```python
sanic.app.Sanic[sanic.config.Config, types.SimpleNamespace]
@@ -368,14 +326,14 @@ sanic.app.Sanic[sanic.config.Config, types.SimpleNamespace]
It refers to two generic types:
1. The first is the type of the configuration object. It defaults to :class:`sanic.config.Config`, but can be any subclass of that.
2. The second is the type of the application context. It defaults to [`SimpleNamespace()`](https://docs.python.org/3/library/types.html#types.SimpleNamespace), but can be **any object** as show above.
1. The first is the type of the configuration object. It defaults to `sanic.config.Config`, but can be any subclass of that.
2. The second is the type of the application context. It defaults to `types.SimpleNamespace`, but can be **any object** as show above.
Let's look at some examples of how the type will change.
.. column::
Consider this example where we pass a custom subclass of :class:`sanic.config.Config` and a custom context object.
Consider this example where we pass a custom subclass of `Config` and a custom context object.
.. column::
@@ -473,9 +431,7 @@ add_listeners(app)
*Added in v23.6*
.. new:: NEW in v23.6
### Custom typed request
### Custom typed request
Sanic also allows you to customize the type of the request object. This is useful if you want to add custom properties to the request object, or be able to access your custom properties of a typed application instance.
@@ -555,7 +511,7 @@ Let's look at some examples of how the type will change.
pass
```
See more information in the [custom request context](./request#custom-request-context) section.
See more information in the [custom request context](./request.md#custom-request-context) section.
*Added in v23.6*

View File

@@ -2,7 +2,9 @@
The next important building block are your _handlers_. These are also sometimes called "views".
In Sanic, a handler is any callable that takes at least a :class:`sanic.request.Request` instance as an argument, and returns either an :class:`sanic.response.HTTPResponse` instance, or a coroutine that does the same.
In Sanic, a handler is any callable that takes at least a `Request` instance as an argument, and returns either an `HTTPResponse` instance, or a coroutine that does the same.
.. column::
@@ -22,39 +24,26 @@ In Sanic, a handler is any callable that takes at least a :class:`sanic.request.
return HTTPResponse()
```
Two more important items to note:
1. You almost *never* will want to use :class:`sanic.response.HTTPresponse` directly. It is much simpler to use one of the [convenience methods](./response#methods).
- `from sanic import json`
- `from sanic import html`
- `from sanic import redirect`
- *etc*
1. As we will see in [the streaming section](../advanced/streaming#response-streaming), you do not always need to return an object. If you use this lower-level API, you can control the flow of the response from within the handler, and a return object is not used.
.. tip:: Heads up
If you want to learn more about encapsulating your logic, checkout [class based views](../advanced/class-based-views.md). For now, we will continue forward with just function-based views.
If you want to learn more about encapsulating your logic, checkout [class based views](/guide/advanced/class-based-views.md).
### A simple function-based handler
The most common way to create a route handler is to decorate the function. It creates a visually simple identification of a route definition. We'll learn more about [routing soon](./routing.md).
.. column::
Then, all you need to do is wire it up to an endpoint. We'll learn more about [routing soon](./routing.md).
Let's look at a practical example.
- We use a convenience decorator on our app instance: `@app.get()`
- And a handy convenience method for generating out response object: `text()`
Mission accomplished 💪
Mission accomplished :muscle:
.. column::
```python
from sanic import text
from sanic.response import text
@app.get("/foo")
async def foo_handler(request):
@@ -96,10 +85,16 @@ The most common way to create a route handler is to decorate the function. It cr
- Or, about **3,843.17** requests/second
.. attrs::
:class: is-size-2
:class: is-size-3
🤯
Okay... this is a ridiculously overdramatic result. And any benchmark you see is inherently very biased. This example is meant to over-the-top show the benefit of `async/await` in the web world. Results will certainly vary. Tools like Sanic and other async Python libraries are not magic bullets that make things faster. They make them _more efficient_.
In our example, the asynchronous version is so much better because while one request is sleeping, it is able to start another one, and another one, and another one, and another one...
But, this is the point! Sanic is fast because it takes the available resources and squeezes performance out of them. It can handle many requests concurrently, which means more requests per second.
.. column::
```python
@@ -110,20 +105,14 @@ The most common way to create a route handler is to decorate the function. It cr
```
Okay... this is a ridiculously overdramatic result. And any benchmark you see is inherently very biased. This example is meant to over-the-top show the benefit of `async/await` in the web world. Results will certainly vary. Tools like Sanic and other async Python libraries are not magic bullets that make things faster. They make them _more efficient_.
In our example, the asynchronous version is so much better because while one request is sleeping, it is able to start another one, and another one, and another one, and another one...
But, this is the point! Sanic is fast because it takes the available resources and squeezes performance out of them. It can handle many requests concurrently, which means more requests per second.
.. tip:: A common mistake!
.. warning:: A common mistake!
Don't do this! You need to ping a website. What do you use? `pip install your-fav-request-library` 🙈
Instead, try using a client that is `async/await` capable. Your server will thank you. Avoid using blocking tools, and favor those that play well in the asynchronous ecosystem. If you need recommendations, check out [Awesome Sanic](https://github.com/mekicha/awesome-sanic).
Sanic uses [httpx](https://www.python-httpx.org/) inside of its testing package (sanic-testing) 😉.
Sanic uses [httpx](https://www.python-httpx.org/) inside of its testing package (sanic-testing) :wink:.
---
@@ -140,52 +129,3 @@ from sanic.request import Request
async def typed_handler(request: Request) -> HTTPResponse:
return text("Done.")
```
## Naming your handlers
All handlers are named automatically. This is useful for debugging, and for generating URLs in templates. When not specified, the name that will be used is the name of the function.
.. column::
For example, this handler will be named `foo_handler`.
.. column::
```python
# Handler name will be "foo_handler"
@app.get("/foo")
async def foo_handler(request):
return text("I said foo!")
```
.. column::
However, you can override this by passing the `name` argument to the decorator.
.. column::
```python
# Handler name will be "foo"
@app.get("/foo", name="foo")
async def foo_handler(request):
return text("I said foo!")
```
.. column::
In fact, as you will, there may be times when you **MUST** supply a name. For example, if you use two decorators on the same function, you will need to supply a name for at least one of them.
If you do not, you will get an error and your app will not start. Names **must** be unique within your app.
.. column::
```python
# Two handlers, same function,
# different names:
# - "foo"
# - "foo_handler"
@app.get("/foo", name="foo")
@app.get("/bar")
async def foo_handler(request):
return text("I said foo!")
```

View File

@@ -1,49 +1,6 @@
# Request
See API docs: [sanic.request](/api/sanic.request)
The :class:`sanic.request.Request` instance contains **a lot** of helpful information available on its parameters. Refer to the [API documentation](https://sanic.readthedocs.io/) for full details.
As we saw in the section on [handlers](./handlers), the first argument in a route handler is usually the :class:`sanic.request.Request` object. Because Sanic is an async framework, the handler will run inside of a [`asyncio.Task`](https://docs.python.org/3/library/asyncio-task.html#asyncio.Task) and will be scheduled by the event loop. This means that the handler will be executed in an isolated context and the request object will be unique to that handler's task.
.. column::
By convention, the argument is named `request`, but you can name it whatever you want. The name of the argument is not important. Both of the following handlers are valid.
.. column::
```python
@app.get("/foo")
async def typical_use_case(request):
return text("I said foo!")
```
```python
@app.get("/foo")
async def atypical_use_case(req):
return text("I said foo!")
```
.. column::
Annotating a request object is super simple.
.. column::
```python
from sanic.request import Request
from sanic.response import text
@app.get("/typed")
async def typed_handler(request: Request):
return text("Done.")
```
.. tip::
For your convenience, assuming you are using a modern IDE, you should leverage type annotations to help with code completion and documentation. This is especially helpful when using the `request` object as it has **MANY** properties and methods.
To see the full list of available properties and methods, refer to the [API documentation](/api/sanic.request).
The `Request` instance contains **a lot** of helpful information available on its parameters. Refer to the [API documentation](https://sanic.readthedocs.io/) for full details.
## Body
@@ -155,15 +112,7 @@ The `Request` object allows you to access the content of the request body in a f
### Request context
The `request.ctx` object is your playground to store whatever information you need to about the request. This lives only for the duration of the request and is unique to the request.
This can be constrasted with the `app.ctx` object which is shared across all requests. Be careful not to confuse them!
The `request.ctx` object by default is a `SimpleNamespace` object allowing you to set arbitrary attributes on it. Sanic will not use this object for anything, so you are free to use it however you want without worrying about name clashes.
```python
### Typical use case
The `request.ctx` object is your playground to store whatever information you need to about the request.
This is often used to store items like authenticated user details. We will get more into [middleware](./middleware.md) later, but here is a simple example.
@@ -174,12 +123,12 @@ async def run_before_handler(request):
@app.route('/hi')
async def hi_my_name_is(request):
if not request.ctx.user:
return text("Hmm... I don't know you)
return text(f"Hi, my name is {request.ctx.user.name}")
return text("Hi, my name is {}".format(request.ctx.user.name))
```
As you can see, the `request.ctx` object is a great place to store information that you need to access in multiple handlers making your code more DRY and easier to maintain. But, as we will learn in the [middleware section](./middleware.md), you can also use it to store information from one middleware that will be used in another.
A typical use case would be to store the user object acquired from database in an authentication middleware. Keys added are accessible to all later middleware as well as the handler over the duration of the request.
Custom context is reserved for applications and extensions. Sanic itself makes no use of it.
### Connection context
@@ -212,15 +161,9 @@ As you can see, the `request.ctx` object is a great place to store information t
request.conn_info.ctx.foo=3
```
.. warning::
While this looks like a convenient place to store information between requests by a single HTTP connection, do not assume that all requests on a single connection came from a single end user. This is because HTTP proxies and load balancers can multiplex multiple connections into a single connection to your server.
**DO NOT** use this to store information about a single user. Use the `request.ctx` object for that.
### Custom Request Objects
As dicussed in [application customization](./app.md#custom-requests), you can create a subclass of :class:`sanic.request.Request` to add additional functionality to the request object. This is useful for adding additional attributes or methods that are specific to your application.
As dicussed in [application customization](./app.md#custom-requests), you can create a subclass of `sanic.Request` to add additional functionality to the request object. This is useful for adding additional attributes or methods that are specific to your application.
.. column::
@@ -258,13 +201,13 @@ As dicussed in [application customization](./app.md#custom-requests), you can cr
### Custom Request Context
By default, the request context (`request.ctx`) is a [`Simplenamespace`](https://docs.python.org/3/library/types.html#types.SimpleNamespace) object allowing you to set arbitrary attributes on it. While this is super helpful to reuse logic across your application, it can be difficult in the development experience since the IDE will not know what attributes are available.
By default, the request context (`request.ctx`) is a `SimpleNamespace` object allowing you to set arbitrary attributes on it. While this is super helpful to reuse logic across your application, it can be difficult in the development experience since the IDE will not know what attributes are available.
To help with this, you can create a custom request context object that will be used instead of the default `SimpleNamespace`. This allows you to add type hints to the context object and have them be available in your IDE.
.. column::
Start by subclassing the :class:`sanic.request.Request` class to create a custom request type. Then, you will need to add a `make_context()` method that returns an instance of your custom context object. *NOTE: the `make_context` method should be a static method.*
Start by subclassing the `sanic.Request` class to create a custom request type. Then, you will need to add a `make_context()` method that returns an instance of your custom context object. *NOTE: the `make_context` method should be a static method.*
.. column::
@@ -286,10 +229,6 @@ To help with this, you can create a custom request context object that will be u
user_id: str = None
```
.. note::
This is a Sanic poweruser feature that makes it super convenient in large codebases to have typed request context objects. It is of course not required, but can be very helpful.
*Added in v23.6*
@@ -297,7 +236,7 @@ To help with this, you can create a custom request context object that will be u
.. column::
Values that are extracted from the path parameters are injected into the handler as argumets, or more specifically as keyword arguments. There is much more detail about this in the [Routing section](./routing.md).
Values that are extracted from the path are injected into the handler as parameters, or more specifically as keyword arguments. There is much more detail about this in the [Routing section](./routing.md).
.. column::
@@ -305,11 +244,6 @@ To help with this, you can create a custom request context object that will be u
@app.route('/tag/<tag>')
async def tag_handler(request, tag):
return text("Tag - {}".format(tag))
# or, explicitly as keyword arguments
@app.route('/tag/<tag>')
async def tag_handler(request, *, tag):
return text("Tag - {}".format(tag))
```
@@ -320,43 +254,8 @@ There are two attributes on the `request` instance to get query parameters:
- `request.args`
- `request.query_args`
These allow you to access the query parameters from the request path (the part after the `?` in the URL).
### Typical use case
In most use cases, you will want to use the `request.args` object to access the query parameters. This will be the parsed query string as a dictionary.
This is by far the most common pattern.
.. column::
Consider the example where we have a `/search` endpoint with a `q` parameter that we want to use to search for something.
.. column::
```python
@app.get("/search")
async def search(request):
query = request.args.get("q")
if not query:
return text("No query string provided")
return text(f"Searching for: {query}")
```
### Parsing the query string
Sometimes, however, you may want to access the query string as a raw string or as a list of tuples. For this, you can use the `request.query_string` and `request.query_args` attributes.
It also should be noted that HTTP allows multiple values for a single key. Although `request.args` may seem like a regular dictionary, it is actually a special type that allows for multiple values for a single key. You can access this by using the `request.args.getlist()` method.
- `request.query_string` - The raw query string
- `request.query_args` - The parsed query string as a list of tuples
- `request.args` - The parsed query string as a *special* dictionary
- `request.args.get()` - Get the first value for a key (behaves like a regular dictionary)
- `request.args.getlist()` - Get all values for a key
```sh
curl "http://localhost:8000?key1=val1&key2=val2&key1=val3"
```bash
$ curl http://localhost:8000\?key1\=val1\&key2\=val2\&key1\=val3
```
```python
@@ -384,12 +283,11 @@ key1=val1&key2=val2&key1=val3
Most of the time you will want to use the `.get()` method to access the first element and not a list. If you do want a list of all items, you can use `.getlist()`.
## Current request getter
Sometimes you may find that you need access to the current request in your application in a location where it is not accessible. A typical example might be in a `logging` format. You can use `Request.get_current()` to fetch the current request (if any).
Remember, the request object is confined to the single [`asyncio.Task`](https://docs.python.org/3/library/asyncio-task.html#asyncio.Task) that is running the handler. If you are not in that task, there is no request object.
```python
import logging
@@ -419,8 +317,8 @@ def record_factory(*args, **kwargs):
logging.setLogRecordFactory(record_factory)
LOGGING_CONFIG_DEFAULTS["formatters"]["access"]["format"] = LOGGING_FORMAT
app = Sanic("Example", log_config=LOGGING_CONFIG_DEFAULTS)
```

View File

@@ -7,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):

View File

@@ -2,9 +2,6 @@ let burger;
let menu;
let menuLinks;
let menuGroups;
let anchors;
let lastUpdated = 0;
let updateFrequency = 300;
function trigger(el, eventType) {
if (typeof eventType === "string" && typeof el[eventType] === "function") {
el[eventType]();
@@ -47,30 +44,6 @@ function hasActiveLink(element) {
}
return false;
}
function scrollHandler(e) {
let now = Date.now();
if (now - lastUpdated < updateFrequency) return;
let closestAnchor = null;
let closestDistance = Infinity;
if (!anchors) { return; }
anchors.forEach(anchor => {
const rect = anchor.getBoundingClientRect();
const distance = Math.abs(rect.top);
if (distance < closestDistance) {
closestDistance = distance;
closestAnchor = anchor;
}
});
if (closestAnchor) {
history.replaceState(null, null, "#" + closestAnchor.id);
lastUpdated = now;
}
}
function initBurger() {
if (!burger || !menu) {
return;
@@ -137,10 +110,6 @@ function initSearch() {
);
});
}
function refreshAnchors() {
anchors = document.querySelectorAll("h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]");
};
function setMenuLinkActive(href) {
burger.classList.remove("is-active");
menu.classList.remove("is-active");
@@ -180,7 +149,6 @@ function init() {
refreshMenu();
refreshMenuLinks();
refreshMenuGroups();
refreshAnchors();
initBurger();
initMenuGroups();
initDetails();
@@ -194,4 +162,3 @@ function afterSwap(e) {
}
document.addEventListener("DOMContentLoaded", init);
document.body.addEventListener("htmx:afterSwap", afterSwap);
document.addEventListener("scroll", scrollHandler);

View File

@@ -272,7 +272,7 @@ th {
html {
background-color: white;
font-size: 24px;
font-size: 18px;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
min-width: 300px;
@@ -12652,39 +12652,37 @@ a.has-text-danger-dark:hover, a.has-text-danger-dark:focus {
.menu hr {
background-color: var(--menu-divider); }
.menu .is-anchor {
font-size: 0.75em; }
font-size: 0.85em; }
.menu .is-anchor a::before {
content: '# ';
color: var(--menu-contrast); }
.menu .menu-label {
margin-bottom: 1rem; }
.menu li.is-group > a {
font-size: 0.85rem; }
.menu li.is-group > a::after {
content: '';
position: relative;
top: -2px;
left: 8px;
display: inline-block;
width: 0;
height: 0;
border-left: 8px solid var(--menu-contrast);
border-top: 5.33333px solid transparent;
border-bottom: 5.33333px solid transparent;
transform: rotate(90deg); }
.menu li.is-group > a ~ .menu-list {
transition: all .15s ease-in-out;
transform: scaleY(1);
transform-origin: top;
overflow: hidden;
opacity: 1;
margin: 0; }
.menu li.is-group > a:not(.is-open)::after {
transform: rotate(0deg); }
.menu li.is-group > a:not(.is-open) ~ .menu-list {
transform: scaleY(0);
opacity: 0;
font-size: 0; }
.menu li.is-group > a::after {
content: '';
position: relative;
top: -2px;
left: 8px;
display: inline-block;
width: 0;
height: 0;
border-left: 8px solid var(--menu-contrast);
border-top: 5.33333px solid transparent;
border-bottom: 5.33333px solid transparent;
transform: rotate(90deg); }
.menu li.is-group > a ~ .menu-list {
transition: all .15s ease-in-out;
transform: scaleY(1);
transform-origin: top;
overflow: hidden;
opacity: 1;
margin: 0; }
.menu li.is-group > a:not(.is-open)::after {
transform: rotate(0deg); }
.menu li.is-group > a:not(.is-open) ~ .menu-list {
transform: scaleY(0);
opacity: 0;
font-size: 0; }
.menu ~ main {
margin-left: 360px; }
.menu .anchor-list {
@@ -12704,16 +12702,6 @@ a.has-text-danger-dark:hover, a.has-text-danger-dark:focus {
.burger {
display: block; } }
.menu-list li ul {
margin: 0;
padding-left: 0; }
.menu-list ul li:not(.is-anchor) {
margin-left: 0.75em; }
.menu-list .menu-list li a {
font-size: 0.85em; }
code {
color: #4a4a4a; }
@@ -12730,12 +12718,6 @@ a[target=_blank]::after {
content: "⇗";
margin-left: 0.25em; }
a > code {
border-bottom: 1px solid #ff0d68; }
a > code:hover {
background-color: #ff0d68;
color: white; }
h1 a.anchor,
h2 a.anchor,
h3 a.anchor,
@@ -12786,10 +12768,6 @@ p + p {
article {
margin-left: 0; } }
@media screen and (min-width: 1216px) {
.section {
padding: 3rem 0rem; } }
.footer {
margin-bottom: 4rem; }
.footer a[target=_blank]::after {
@@ -13020,51 +12998,6 @@ h3 + .code-block {
.tabs .tab-content {
display: none; }
.table-of-contents {
position: fixed;
right: 0;
bottom: 0;
z-index: 1000;
max-width: 500px;
padding: 1rem 2rem;
background-color: #fafafa;
box-shadow: 0 0 2px rgba(63, 63, 68, 0.5); }
@media (prefers-color-scheme: dark) {
.table-of-contents {
background-color: #0a0a0a;
box-shadow: 0 0 2px rgba(191, 191, 191, 0.5); } }
.table-of-contents .table-of-contents-item {
display: block;
margin-bottom: 0.5rem;
text-decoration: none; }
.table-of-contents .table-of-contents-item:hover {
text-decoration: underline;
color: #ff0d68; }
.table-of-contents .table-of-contents-item:hover strong, .table-of-contents .table-of-contents-item:hover small {
color: #ff0d68; }
.table-of-contents .table-of-contents-item strong {
color: #121212;
font-size: 1.15em;
display: block;
line-height: 1rem;
margin-top: 0.75rem; }
.table-of-contents .table-of-contents-item small {
color: #7a7a7a;
font-size: 0.85em; }
@media (prefers-color-scheme: dark) {
.table-of-contents .table-of-contents-item strong {
color: #dbdbdb; } }
@media (max-width: 768px) {
.table-of-contents {
position: static;
max-width: calc(100vw - 2rem); }
.table-of-contents .table-of-contents-item {
display: flex;
flex-direction: row-reverse;
justify-content: start; }
.table-of-contents .table-of-contents-item strong {
display: inline;
margin: 0 0 0 0.75rem; } }
footer .level,
footer .level .level-right,
footer .level .level-left {

5
guide/pyproject.toml Normal file
View File

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

View File

@@ -1,8 +1,3 @@
sanic>=23.6.*
sanic-ext>=23.6.*
msgspec
python-frontmatter
pygments
docstring-parser
libsass
mistune

View File

@@ -1,11 +1,3 @@
"""Sanic User Guide
https://sanic.dev
Built using the SHH stack:
- Sanic
- html5tagger
- HTMX"""
from pathlib import Path
from webapp.worker.factory import create_app

View File

@@ -290,68 +290,3 @@ h3 + .code-block { margin-top: 1rem; }
.tabs {
.tab-content { display: none; }
}
.table-of-contents {
position: fixed;
right: 0;
bottom: 0;
z-index: 1000;
max-width: 500px;
padding: 1rem 2rem;
background-color: $white-bis;
box-shadow: 0 0 2px rgba(63, 63, 68, 0.5);
@media (prefers-color-scheme: dark) {
background-color: $black;
box-shadow: 0 0 2px rgba(191, 191, 191, 0.5);
}
.table-of-contents-item {
display: block;
margin-bottom: 0.5rem;
text-decoration: none;
&:hover {
text-decoration: underline;
color: $primary;
strong, small {
color: $primary;
}
}
strong {
color: $black-bis;
font-size: 1.15em;
display: block;
line-height: 1rem;
margin-top: 0.75rem;
}
small {
color: $grey;
font-size: 0.85em;
}
@media (prefers-color-scheme: dark) {
strong { color: $grey-lighter; }
}
}
@media (max-width: 768px) {
position: static;
max-width: calc(100vw - 2rem);
.table-of-contents-item {
display: flex;
flex-direction: row-reverse;
justify-content: start;
strong {
display: inline;
margin: 0 0 0 0.75rem;
}
}
}
}

View File

@@ -7,18 +7,9 @@ code { color: #{$grey-dark}; }
}
}
a{
&[target=_blank]::after {
content: "";
margin-left: 0.25em;
}
& > code {
border-bottom: 1px solid $primary;
&:hover {
background-color: $primary;
color: $white;
}
}
a[target=_blank]::after {
content: "";
margin-left: 0.25em;
}
h1 a.anchor,
h2 a.anchor,
@@ -58,8 +49,4 @@ p + p { margin-top: 1rem; }
h2 { margin-left: 0; }
article { margin-left: 0; }
}
@media screen and (min-width: $widescreen) {
.section {
padding: 3rem 0rem;
}
}

View File

@@ -1,4 +1,4 @@
$body-size: 24px;
$body-size: 18px;

View File

@@ -60,7 +60,7 @@ $menu-width: 360px;
hr { background-color: var(--menu-divider); }
.is-anchor {
font-size: 0.75em;
font-size: 0.85em;
& a::before {
content: '# ';
color: var(--menu-contrast);
@@ -68,7 +68,6 @@ $menu-width: 360px;
}
.menu-label { margin-bottom: 1rem; }
li.is-group > a {
font-size: 0.85rem;
&::after {
content: '';
position: relative;
@@ -120,14 +119,3 @@ $menu-width: 360px;
}
.burger { display: block; }
}
.menu-list li ul {
margin: 0;
padding-left: 0;
}
.menu-list ul li:not(.is-anchor) {
margin-left: 0.75em;
}
.menu-list .menu-list li a {
font-size: 0.85em;
}

View File

@@ -13,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:
@@ -64,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,

View File

@@ -1,17 +1,14 @@
from webapp.display.layouts.models import MenuItem
from html5tagger import Builder, E # type: ignore
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"),
@@ -46,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",

View File

@@ -1,9 +1,9 @@
from webapp.display.layouts.models import MenuItem
from webapp.display.text import slugify
from html5tagger import Builder, E # type: ignore
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())
@@ -15,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"
@@ -73,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:

View File

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

View File

@@ -1,6 +1,7 @@
import re
from textwrap import dedent
from html5tagger import HTML, Builder, E # type: ignore
from mistune import HTMLRenderer, create_markdown, escape
from mistune.directives import RSTDirective, TableOfContents
from mistune.util import safe_entity
@@ -8,8 +9,6 @@ from pygments import highlight
from pygments.formatters import html
from pygments.lexers import get_lexer_by_name
from html5tagger import HTML, Builder, E # type: ignore
from .code_style import SanicCodeStyle
from .plugins.attrs import Attributes
from .plugins.columns import Column
@@ -17,7 +16,6 @@ from .plugins.hook import Hook
from .plugins.mermaid import Mermaid
from .plugins.notification import Notification
from .plugins.span import span
from .plugins.inline_directive import inline_directive
from .plugins.tabs import Tabs
from .text import slugify
@@ -38,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)
@@ -48,20 +46,16 @@ 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
)
def link(self, text: str, url: str, title: str | None = None) -> str:
url = self.safe_url(url).replace(".md", ".html")
url, anchor = url.split("#", 1) if "#" in url else (url, None)
if not url.endswith("/") and not url.endswith(".html"):
url = self.safe_url(url).removesuffix(".md")
if not url.endswith("/"):
url += ".html"
if anchor:
url += f"#{anchor}"
attributes: dict[str, str] = {"href": url}
if title:
attributes["title"] = safe_entity(title)
@@ -93,28 +87,10 @@ class DocsRenderer(HTMLRenderer):
attrs["class"] = "table is-fullwidth is-bordered"
return self._make_tag("table", attrs, text)
def inline_directive(self, text: str, **attrs) -> str:
num_dots = text.count(".")
display = self.codespan(text)
if num_dots <= 1:
return display
module, *_ = text.rsplit(".", num_dots - 1)
href = f"/api/{module}.html"
return self._make_tag(
"a",
{"href": href, "class": "inline-directive"},
display,
)
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}>"
@@ -145,7 +121,6 @@ _render_markdown = create_markdown(
"mark",
"table",
span,
inline_directive,
],
)

View File

@@ -10,7 +10,6 @@ from html import escape
from docstring_parser import Docstring, DocstringParam, DocstringRaises
from docstring_parser import parse as parse_docstring
from docstring_parser.common import DocstringExample
from html5tagger import HTML, Builder, E # type: ignore
from ..markdown import render_markdown, slugify
@@ -108,38 +107,19 @@ def _get_object_type(obj) -> str:
def organize_docobjects(package_name: str) -> dict[str, str]:
page_content: defaultdict[str, str] = defaultdict(str)
docobjects = _extract_docobjects(package_name)
page_registry: defaultdict[str, list[str]] = defaultdict(list)
for module, docobject in docobjects.items():
builder = Builder(name="Partial")
_docobject_to_html(docobject, builder)
ref = module.rsplit(".", module.count(".") - 1)[0]
page_registry[ref].append(module)
page_content[f"/api/{ref}.md"] += str(builder)
for ref, objects in page_registry.items():
page_content[f"/api/{ref}.md"] = _table_of_contents(objects) + page_content[f"/api/{ref}.md"]
return page_content
def _table_of_contents(objects: list[str]) -> str:
builder = Builder(name="Partial")
with builder.div(class_="table-of-contents"):
builder.h3("Table of Contents", class_="is-size-4")
for obj in objects:
module, name = obj.rsplit(".", 1)
builder.a(
E.strong(name), E.small(module),
href=f"#{slugify(obj.replace('.', '-'))}",
class_="table-of-contents-item",
)
return str(builder)
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 (
@@ -173,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)
@@ -229,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:
@@ -256,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>("
@@ -272,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":
@@ -285,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)
@@ -327,10 +297,7 @@ def _render_params(builder: Builder, params: list[DocstringParam]) -> None:
E.br(),
E.span(
param.type_name,
class_=(
"has-text-weight-normal has-text-purple "
"is-size-7 ml-2"
),
class_="has-text-weight-normal has-text-purple ml-2",
),
]
dt_args.extend(parts)
@@ -338,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 ""
)
)
)
@@ -354,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 ""))
)
@@ -374,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")
@@ -393,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 ""))
)

View File

@@ -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

View File

@@ -1,13 +1,12 @@
from __future__ import annotations
from contextlib import contextmanager
from typing import Type
from webapp.display.base import BaseRenderer
from html5tagger import HTML, Builder # type: ignore
from sanic import Request
from webapp.display.base import BaseRenderer
from ..layouts.base import BaseLayout
from .page import Page
@@ -21,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
@@ -39,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

View File

@@ -2,12 +2,11 @@ from re import Match
from textwrap import dedent
from typing import Any
from html5tagger import HTML, E
from mistune.block_parser import BlockParser
from mistune.core import BlockState
from mistune.directives import DirectivePlugin
from html5tagger import HTML, E
class Attributes(DirectivePlugin):
def __call__(self, directive, md):
@@ -16,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()

View File

@@ -10,9 +10,7 @@ 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()
@@ -36,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'

View File

@@ -16,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

View File

@@ -1,18 +0,0 @@
import re
from mistune.markdown import Markdown
DIRECTIVE_PATTERN = r":(?:class|func|meth|attr|exc|mod|data|const|obj|keyword|option|cmdoption|envvar):`(?P<ref>sanic\.[^`]+)`" # noqa: E501
def _parse_inline_directive(inline, m: re.Match, state):
state.append_token(
{
"type": "inline_directive",
"attrs": {},
"raw": m.group("ref"),
}
)
return m.end()
def inline_directive(md: Markdown):
md.inline.register("inline_directive", DIRECTIVE_PATTERN, _parse_inline_directive, before="escape",)

View File

@@ -3,19 +3,16 @@ from re import Match
from textwrap import dedent
from typing import Any
from html5tagger import HTML, E
from mistune import HTMLRenderer
from mistune.block_parser import BlockParser
from mistune.core import BlockState
from mistune.directives import DirectivePlugin, RSTDirective
from mistune.markdown import Markdown
from html5tagger import HTML, E
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()

View File

@@ -1,6 +1,5 @@
from mistune.directives import Admonition
from html5tagger import HTML, E
from mistune.directives import Admonition
class Notification(Admonition):
@@ -20,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(

View File

@@ -10,9 +10,7 @@ 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()
@@ -41,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'

View File

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

View File

@@ -5,6 +5,7 @@ from pathlib import Path
from typing import ClassVar
from msgspec import Struct
from webapp.display.page import Page
@@ -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,

View File

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

View File

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

View File

@@ -1,5 +1,7 @@
from pathlib import Path
from sanic import Request, Sanic, html, redirect
from webapp.display.layouts.models import MenuItem
from webapp.display.page import Page, PageRenderer
from webapp.endpoint.view import bp
@@ -7,8 +9,6 @@ from webapp.worker.config import load_config, load_menu
from webapp.worker.reload import setup_livereload
from webapp.worker.style import setup_style
from sanic import Request, Sanic, html, redirect
def _compile_sidebar_order(items: list[MenuItem]) -> list[str]:
order = []
@@ -28,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)
@@ -62,15 +58,10 @@ def create_app(root: Path) -> Sanic:
language: str,
path: str = "",
):
return html(
page_renderer.render(request, language, path),
headers={"vary": "hx-request"},
)
return html(page_renderer.render(request, language, path))
@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

View File

@@ -5,7 +5,6 @@ from queue import Empty, Queue
from typing import Any
import ujson
from sanic import Request, Sanic, Websocket
@@ -54,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"
)
@@ -109,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()

View File

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

View File

@@ -2,20 +2,28 @@
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"
[tool.black]
line-length = 79
[tool.ruff]
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.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.ruff.isort]
known-first-party = ["sanic"]
known-third-party = ["pytest"]
[[tool.mypy.overrides]]
module = [

View File

@@ -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.

View File

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

View File

@@ -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
@@ -119,59 +110,8 @@ class Sanic(
StartupMixin,
metaclass=TouchUpMeta,
):
"""The main application instance
You will create an instance of this class and use it to register
routes, listeners, middleware, blueprints, error handlers, etc.
By convention, it is often called `app`. It must be named using
the `name` parameter and is roughly constrained to the same
restrictions as a Python module name, however, it can contain
hyphens (`-`).
```python
# will cause an error because it contains spaces
Sanic("This is not legal")
```
```python
# this is legal
Sanic("Hyphens-are-legal_or_also_underscores")
```
Args:
name (str): The name of the application. Must be a valid
Python module name (including hyphens).
config (Optional[config_type]): The configuration to use for
the application. Defaults to `None`.
ctx (Optional[ctx_type]): The context to use for the
application. Defaults to `None`.
router (Optional[Router]): The router to use for the
application. Defaults to `None`.
signal_router (Optional[SignalRouter]): The signal router to
use for the application. Defaults to `None`.
error_handler (Optional[ErrorHandler]): The error handler to
use for the application. Defaults to `None`.
env_prefix (Optional[str]): The prefix to use for environment
variables. Defaults to `SANIC_`.
request_class (Optional[Type[Request]]): The request class to
use for the application. Defaults to `Request`.
strict_slashes (bool): Whether to enforce strict slashes.
Defaults to `False`.
log_config (Optional[Dict[str, Any]]): The logging configuration
to use for the application. Defaults to `None`.
configure_logging (bool): Whether to configure logging.
Defaults to `True`.
dumps (Optional[Callable[..., AnyStr]]): The function to use
for serializing JSON. Defaults to `None`.
loads (Optional[Callable[..., Any]]): The function to use
for deserializing JSON. Defaults to `None`.
inspector (bool): Whether to enable the inspector. Defaults
to `False`.
inspector_class (Optional[Type[Inspector]]): The inspector
class to use for the application. Defaults to `None`.
certloader_class (Optional[Type[CertLoader]]): The certloader
class to use for the application. Defaults to `None`.
"""
The main application instance
"""
__touchup__ = (
@@ -224,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
@@ -233,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:
...
@@ -253,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:
...
@@ -276,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:
...
@@ -297,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
@@ -354,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
@@ -447,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)
@@ -463,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:
@@ -512,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)
@@ -563,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
@@ -582,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)
@@ -618,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:
@@ -639,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,
@@ -652,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,
@@ -664,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:
@@ -713,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
@@ -800,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
@@ -831,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.
@@ -863,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:
@@ -876,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", ""),
@@ -891,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)
@@ -908,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)
@@ -974,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:
@@ -988,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
@@ -1029,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 "
@@ -1147,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 "
@@ -1197,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:
@@ -1245,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.
@@ -1272,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(
@@ -1336,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
@@ -1398,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
@@ -1487,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:
@@ -1566,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:
@@ -1597,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(
@@ -1617,7 +1515,7 @@ class Sanic(
app,
loop,
*,
name: Optional[str] = None,
name: str | None = None,
register: bool = True,
) -> Task:
if not isinstance(task, Future):
@@ -1679,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
@@ -1708,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)
@@ -1730,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
@@ -1759,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:
@@ -1802,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,)
@@ -1835,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.
@@ -1873,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
@@ -1904,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
@@ -1914,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.
"""
@@ -1954,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:
@@ -1999,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.
@@ -2119,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
@@ -2328,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 = (
@@ -2367,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 (
@@ -2375,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
@@ -2398,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**.
@@ -2443,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
@@ -2478,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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

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

View File

@@ -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

View File

@@ -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.
"""

View File

@@ -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:

View File

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

View File

@@ -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

View File

@@ -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:

View File

@@ -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",

View File

@@ -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"

View File

@@ -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):

View File

@@ -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::

View File

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

View File

@@ -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:

View File

@@ -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")

View File

@@ -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

View File

@@ -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 = {

View File

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

View File

@@ -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

View File

@@ -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 "📄"

View File

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

View File

@@ -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,11 +336,10 @@ 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))
m.group(1).lower(): (m.group(2) or m.group(3))
.replace("%22", '"')
.replace("%0D%0A", "\n")
for m in _param.finditer(value[pos:])
@@ -367,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
@@ -381,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)
@@ -405,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
@@ -416,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):
@@ -452,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:
@@ -489,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:
@@ -531,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:

View File

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

View File

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

View File

@@ -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

View File

@@ -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

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