Merge branch 'main' of github.com:sanic-org/sanic into signal-routing

This commit is contained in:
Adam Hopkins 2021-12-13 09:39:36 +02:00
commit ce2bb294f9
No known key found for this signature in database
GPG Key ID: 9F85EE6C807303FB
115 changed files with 3819 additions and 1293 deletions

View File

@ -1,5 +1,7 @@
exclude_patterns:
- "sanic/__main__.py"
- "sanic/application/logo.py"
- "sanic/application/motd.py"
- "sanic/reloader_helpers.py"
- "sanic/simple.py"
- "sanic/utils.py"
@ -8,7 +10,6 @@ exclude_patterns:
- "docker/"
- "docs/"
- "examples/"
- "hack/"
- "scripts/"
- "tests/"
checks:
@ -22,3 +23,6 @@ checks:
threshold: 40
complex-logic:
enabled: false
method-complexity:
config:
threshold: 10

View File

@ -3,6 +3,9 @@ branch = True
source = sanic
omit =
site-packages
sanic/application/logo.py
sanic/application/motd.py
sanic/cli
sanic/__main__.py
sanic/reloader_helpers.py
sanic/simple.py

View File

@ -5,11 +5,13 @@ on:
branches: [ main ]
pull_request:
branches: [ main ]
types: [opened, synchronize, reopened, ready_for_review]
schedule:
- cron: '25 16 * * 0'
jobs:
analyze:
if: github.event.pull_request.draft == false
name: Analyze
runs-on: ubuntu-latest

View File

@ -1,19 +1,20 @@
name: Coverage check
# on:
# push:
# branches:
# - main
# tags:
# - "!*" # Do not execute on tags
# paths:
# - sanic/*
# - tests/*
# pull_request:
# paths:
# - "!*.MD"
on: [push, pull_request]
on:
push:
branches:
- main
tags:
- "!*" # Do not execute on tags
paths:
- sanic/*
- tests/*
pull_request:
paths:
- "!*.MD"
types: [opened, synchronize, reopened, ready_for_review]
jobs:
test:
if: github.event.pull_request.draft == false
runs-on: ${{ matrix.os }}
strategy:
matrix:

View File

@ -3,9 +3,11 @@ on:
pull_request:
branches:
- main
types: [opened, synchronize, reopened, ready_for_review]
jobs:
bandit:
if: github.event.pull_request.draft == false
name: type-check-${{ matrix.config.python-version }}
runs-on: ${{ matrix.os }}
strategy:
@ -16,6 +18,7 @@ jobs:
- { python-version: 3.7, tox-env: security}
- { python-version: 3.8, tox-env: security}
- { python-version: 3.9, tox-env: security}
- { python-version: "3.10", tox-env: security}
steps:
- name: Checkout the repository
uses: actions/checkout@v2

View File

@ -3,9 +3,11 @@ on:
pull_request:
branches:
- main
types: [opened, synchronize, reopened, ready_for_review]
jobs:
docsLinter:
if: github.event.pull_request.draft == false
name: Lint Documentation
runs-on: ubuntu-latest
strategy:

View File

@ -3,9 +3,11 @@ on:
pull_request:
branches:
- main
types: [opened, synchronize, reopened, ready_for_review]
jobs:
linter:
if: github.event.pull_request.draft == false
name: lint
runs-on: ${{ matrix.os }}
strategy:

46
.github/workflows/pr-python310.yml vendored Normal file
View File

@ -0,0 +1,46 @@
name: Python 3.10 Tests
on:
pull_request:
branches:
- main
types: [opened, synchronize, reopened, ready_for_review]
jobs:
testPy310:
if: github.event.pull_request.draft == false
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
# os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest]
config:
- {
python-version: "3.10",
tox-env: py310,
ignore-error-flake: "false",
command-timeout: "0",
}
- {
python-version: "3.10",
tox-env: py310-no-ext,
ignore-error-flake: "true",
command-timeout: "600000",
}
steps:
- name: Checkout the Repository
uses: actions/checkout@v2
id: checkout-branch
- name: Run Unit Tests
uses: harshanarayana/custom-actions@main
with:
python-version: ${{ matrix.config.python-version }}
test-infra-tool: tox
test-infra-version: latest
action: tests
test-additional-args: "-e=${{ matrix.config.tox-env }},-vv=''"
experimental-ignore-error: "${{ matrix.config.ignore-error-flake }}"
command-timeout: "${{ matrix.config.command-timeout }}"
test-failure-retry: "3"

View File

@ -3,19 +3,15 @@ on:
pull_request:
branches:
- main
push:
branches:
- main
paths:
- sanic/*
- tests/*
types: [opened, synchronize, reopened, ready_for_review]
jobs:
testPy37:
if: github.event.pull_request.draft == false
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
fail-fast: true
matrix:
# os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest]

View File

@ -3,19 +3,15 @@ on:
pull_request:
branches:
- main
push:
branches:
- main
paths:
- sanic/*
- tests/*
types: [opened, synchronize, reopened, ready_for_review]
jobs:
testPy38:
if: github.event.pull_request.draft == false
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
fail-fast: true
matrix:
# os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest]

View File

@ -3,19 +3,15 @@ on:
pull_request:
branches:
- main
push:
branches:
- main
paths:
- sanic/*
- tests/*
types: [opened, synchronize, reopened, ready_for_review]
jobs:
testPy39:
if: github.event.pull_request.draft == false
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
fail-fast: true
matrix:
# os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest]

View File

@ -3,9 +3,11 @@ on:
pull_request:
branches:
- main
types: [opened, synchronize, reopened, ready_for_review]
jobs:
typeChecking:
if: github.event.pull_request.draft == false
name: type-check-${{ matrix.config.python-version }}
runs-on: ${{ matrix.os }}
strategy:
@ -16,6 +18,7 @@ jobs:
- { python-version: 3.7, tox-env: type-checking}
- { python-version: 3.8, tox-env: type-checking}
- { python-version: 3.9, tox-env: type-checking}
- { python-version: "3.10", tox-env: type-checking}
steps:
- name: Checkout the repository
uses: actions/checkout@v2

View File

@ -3,9 +3,11 @@ on:
pull_request:
branches:
- main
types: [opened, synchronize, reopened, ready_for_review]
jobs:
testsOnWindows:
if: github.event.pull_request.draft == false
name: ut-${{ matrix.config.tox-env }}
runs-on: windows-latest
strategy:
@ -15,6 +17,7 @@ jobs:
- { python-version: 3.7, tox-env: py37-no-ext }
- { python-version: 3.8, tox-env: py38-no-ext }
- { python-version: 3.9, tox-env: py39-no-ext }
- { python-version: "3.10", tox-env: py310-no-ext }
- { python-version: pypy-3.7, tox-env: pypy37-no-ext }
steps:

View File

@ -14,7 +14,7 @@ jobs:
strategy:
fail-fast: true
matrix:
python-version: ["3.7", "3.8", "3.9"]
python-version: ["3.7", "3.8", "3.9", "3.10"]
steps:
- name: Checkout repository

View File

@ -657,7 +657,7 @@ Improved Documentation
Version 20.6.0
---------------
*Released, but unintentionally ommitting PR #1880, so was replaced by 20.6.1*
*Released, but unintentionally omitting PR #1880, so was replaced by 20.6.1*
Version 20.3.0
@ -1090,7 +1090,7 @@ Version 18.12
* Fix Range header handling for static files (#1402)
* Fix the logger and make it work (#1397)
* Fix type pikcle->pickle in multiprocessing test
* Fix pickling blueprints Change the string passed in the "name" section of the namedtuples in Blueprint to match the name of the Blueprint module attribute name. This allows blueprints to be pickled and unpickled, without errors, which is a requirment of running Sanic in multiprocessing mode in Windows. Added a test for pickling and unpickling blueprints Added a test for pickling and unpickling sanic itself Added a test for enabling multiprocessing on an app with a blueprint (only useful to catch this bug if the tests are run on Windows).
* Fix pickling blueprints Change the string passed in the "name" section of the namedtuples in Blueprint to match the name of the Blueprint module attribute name. This allows blueprints to be pickled and unpickled, without errors, which is a requirement of running Sanic in multiprocessing mode in Windows. Added a test for pickling and unpickling blueprints Added a test for pickling and unpickling sanic itself Added a test for enabling multiprocessing on an app with a blueprint (only useful to catch this bug if the tests are run on Windows).
* Fix document for logging
Version 0.8
@ -1129,7 +1129,7 @@ Version 0.8
* Content-length header on 204/304 responses (Arnulfo Solís)
* Extend WebSocketProtocol arguments and add docs (Bob Olde Hampsink, yunstanford)
* Update development status from pre-alpha to beta (Maksim Anisenkov)
* KeepAlive Timout log level changed to debug (Arnulfo Solís)
* KeepAlive Timeout log level changed to debug (Arnulfo Solís)
* Pin pytest to 3.3.2 because of pytest-dev/pytest#3170 (Maksim Aniskenov)
* Install Python 3.5 and 3.6 on docker container for tests (Shahin Azad)
* Add support for blueprint groups and nesting (Elias Tarhini)

View File

@ -11,7 +11,7 @@ Sanic | Build fast. Run fast.
:stub-columns: 1
* - Build
- | |Py39Test| |Py38Test| |Py37Test| |Codecov|
- | |Py39Test| |Py38Test| |Py37Test|
* - Docs
- | |UserGuide| |Documentation|
* - Package
@ -27,8 +27,6 @@ Sanic | Build fast. Run fast.
:target: https://community.sanicframework.org/
.. |Discord| image:: https://img.shields.io/discord/812221182594121728?logo=discord
:target: https://discord.gg/FARQzAEMAA
.. |Codecov| image:: https://codecov.io/gh/sanic-org/sanic/branch/master/graph/badge.svg
:target: https://codecov.io/gh/sanic-org/sanic
.. |Py39Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python39.yml/badge.svg?branch=main
:target: https://github.com/sanic-org/sanic/actions/workflows/pr-python39.yml
.. |Py38Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python38.yml/badge.svg?branch=main
@ -79,6 +77,10 @@ Sponsor
Check out `open collective <https://opencollective.com/sanic-org>`_ to learn more about helping to fund Sanic.
Thanks to `Linode <https://www.linode.com>`_ for their contribution towards the development and community of Sanic.
|Linode|
Installation
------------
@ -162,3 +164,8 @@ Contribution
------------
We are always happy to have new contributions. We have `marked issues good for anyone looking to get started <https://github.com/sanic-org/sanic/issues?q=is%3Aopen+is%3Aissue+label%3Abeginner>`_, and welcome `questions on the forums <https://community.sanicframework.org/>`_. Please take a look at our `Contribution guidelines <https://github.com/sanic-org/sanic/blob/master/CONTRIBUTING.rst>`_.
.. |Linode| image:: https://www.linode.com/wp-content/uploads/2021/01/Linode-Logo-Black.svg
:alt: Linode
:target: https://www.linode.com
:width: 200px

View File

@ -4,12 +4,14 @@ import asyncio
from sanic import Sanic
app = Sanic()
app = Sanic(__name__)
async def notify_server_started_after_five_seconds():
await asyncio.sleep(5)
print('Server successfully started!')
print("Server successfully started!")
app.add_task(notify_server_started_after_five_seconds())

View File

@ -1,30 +1,29 @@
from sanic import Sanic
from sanic.response import text
from random import randint
app = Sanic()
from sanic import Sanic
from sanic.response import text
@app.middleware('request')
app = Sanic(__name__)
@app.middleware("request")
def append_request(request):
# Add new key with random value
request['num'] = randint(0, 100)
request.ctx.num = randint(0, 100)
@app.get('/pop')
@app.get("/pop")
def pop_handler(request):
# Pop key from request object
num = request.pop('num')
return text(num)
return text(request.ctx.num)
@app.get('/key_exist')
@app.get("/key_exist")
def key_exist_handler(request):
# Check the key is exist or not
if 'num' in request:
return text('num exist in request')
if hasattr(request.ctx, "num"):
return text("num exist in request")
return text('num does not exist in reqeust')
return text("num does not exist in request")
app.run(host="0.0.0.0", port=8000, debug=True)

View File

@ -1,10 +1,12 @@
# -*- coding: utf-8 -*-
from sanic import Sanic
from functools import wraps
from sanic import Sanic
from sanic.response import json
app = Sanic()
app = Sanic(__name__)
def check_request_for_authorization_status(request):
@ -27,14 +29,16 @@ def authorized(f):
return response
else:
# the user is not authorized.
return json({'status': 'not_authorized'}, 403)
return json({"status": "not_authorized"}, 403)
return decorated_function
@app.route("/")
@authorized
async def test(request):
return json({'status': 'authorized'})
return json({"status": "authorized"})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)

View File

@ -1,43 +1,53 @@
from sanic import Sanic, Blueprint
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.
On a valid request, it should print "1 2 3 6 5 4" to terminal
'''
"""
app = Sanic(__name__)
bp = Blueprint("bp_"+__name__)
bp = Blueprint("bp_" + __name__)
@bp.middleware('request')
@bp.on_request
def request_middleware_1(request):
print('1')
print("1")
@bp.middleware('request')
@bp.on_request
def request_middleware_2(request):
print('2')
print("2")
@bp.middleware('request')
@bp.on_request
def request_middleware_3(request):
print('3')
print("3")
@bp.middleware('response')
@bp.on_response
def resp_middleware_4(request, response):
print('4')
print("4")
@bp.middleware('response')
@bp.on_response
def resp_middleware_5(request, response):
print('5')
print("5")
@bp.middleware('response')
@bp.on_response
def resp_middleware_6(request, response):
print('6')
print("6")
@bp.route('/')
@bp.route("/")
def pop_handler(request):
return text('hello world')
return text("hello world")
app.blueprint(bp, url_prefix='/bp')
app.blueprint(bp, url_prefix="/bp")
app.run(host="0.0.0.0", port=8000, debug=True, auto_reload=False)

View File

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

View File

@ -2,17 +2,20 @@ from asyncio import sleep
from sanic import Sanic, response
app = Sanic(__name__, strict_slashes=True)
@app.get("/")
async def handler(request):
return response.redirect("/sleep/3")
@app.get("/sleep/<t:number>")
async def handler2(request, t=0.3):
await sleep(t)
return response.text(f"Slept {t:.1f} seconds.\n")
if __name__ == '__main__':
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)

View File

@ -7,8 +7,10 @@ and pass in an instance of it when we create our Sanic instance. Inside this
class' default handler, we can do anything including sending exceptions to
an external service.
"""
from sanic.handlers import ErrorHandler
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)
@ -16,7 +18,6 @@ Imports and code relevant for our CustomHandler class
class CustomHandler(ErrorHandler):
def default(self, request, exception):
# Here, we have access to the exception object
# and can do anything with it (log, send to external service, etc)
@ -38,17 +39,17 @@ server's error_handler to an instance of our CustomHandler
from sanic import Sanic
app = Sanic(__name__)
handler = CustomHandler()
app.error_handler = handler
app = Sanic(__name__, error_handler=handler)
@app.route("/")
async def test(request):
# Here, something occurs which causes an unexpected exception
# This exception will flow to our custom handler.
raise SanicException('You Broke It!')
raise SanicException("You Broke It!")
if __name__ == '__main__':
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000, debug=True)

View File

@ -1,4 +1,6 @@
from sanic import Sanic, response, text
from sanic.handlers import ErrorHandler
from sanic.server.async_server import AsyncioServer
HTTP_PORT = 9999
@ -32,20 +34,40 @@ def proxy(request, path):
return response.redirect(url)
@https.listener("main_process_start")
@https.main_process_start
async def start(app, _):
global http
app.http_server = await http.create_server(
http_server = await http.create_server(
port=HTTP_PORT, return_asyncio_server=True
)
app.http_server.after_start()
app.add_task(runner(http, http_server))
app.ctx.http_server = http_server
app.ctx.http = http
@https.listener("main_process_stop")
@https.main_process_stop
async def stop(app, _):
app.http_server.before_stop()
await app.http_server.close()
app.http_server.after_stop()
await app.ctx.http_server.before_stop()
await app.ctx.http_server.close()
for connection in app.ctx.http_server.connections:
connection.close_if_idle()
await app.ctx.http_server.after_stop()
app.ctx.http = False
async def runner(app: Sanic, app_server: AsyncioServer):
app.is_running = True
try:
app.signalize()
app.finalize()
ErrorHandler.finalize(app.error_handler)
app_server.init = True
await app_server.before_start()
await app_server.after_start()
await app_server.serve_forever()
finally:
app.is_running = False
app.is_stopping = True
https.run(port=HTTPS_PORT, debug=True)

View File

@ -1,26 +1,30 @@
import asyncio
import httpx
from sanic import Sanic
from sanic.response import json
import asyncio
import aiohttp
app = Sanic(__name__)
sem = None
@app.listener('before_server_start')
def init(sanic, loop):
@app.before_server_start
def init(sanic, _):
global sem
concurrency_per_worker = 4
sem = asyncio.Semaphore(concurrency_per_worker, loop=loop)
sem = asyncio.Semaphore(concurrency_per_worker)
async def bounded_fetch(session, url):
"""
Use session object to perform 'get' request on url
"""
async with sem, session.get(url) as response:
return await response.json()
async with sem:
response = await session.get(url)
return response.json()
@app.route("/")
@ -28,9 +32,9 @@ async def test(request):
"""
Download and serve example JSON
"""
url = "https://api.github.com/repos/channelcat/sanic"
url = "https://api.github.com/repos/sanic-org/sanic"
async with aiohttp.ClientSession() as session:
async with httpx.AsyncClient() as session:
response = await bounded_fetch(session, url)
return json(response)

View File

@ -1,6 +1,6 @@
import logging
import aiotask_context as context
from contextvars import ContextVar
from sanic import Sanic, response
@ -11,8 +11,8 @@ log = logging.getLogger(__name__)
class RequestIdFilter(logging.Filter):
def filter(self, record):
try:
record.request_id = context.get("X-Request-ID")
except ValueError:
record.request_id = app.ctx.request_id.get(None) or "n/a"
except AttributeError:
record.request_id = "n/a"
return True
@ -49,8 +49,7 @@ app = Sanic(__name__, log_config=LOG_SETTINGS)
@app.on_request
async def set_request_id(request):
request_id = request.id
context.set("X-Request-ID", request_id)
request.app.ctx.request_id.set(request.id)
log.info(f"Setting {request.id=}")
@ -61,14 +60,14 @@ async def set_request_header(request, response):
@app.route("/")
async def test(request):
log.debug("X-Request-ID: %s", context.get("X-Request-ID"))
log.debug("X-Request-ID: %s", request.id)
log.info("Hello from test!")
return response.json({"test": True})
@app.before_server_start
def setup(app, loop):
loop.set_task_factory(context.task_factory)
app.ctx.request_id = ContextVar("request_id")
if __name__ == "__main__":

View File

@ -1,5 +1,6 @@
import logging
import socket
from os import getenv
from platform import node
from uuid import getnode as get_mac
@ -7,10 +8,11 @@ from uuid import getnode as get_mac
from logdna import LogDNAHandler
from sanic import Sanic
from sanic.response import json
from sanic.request import Request
from sanic.response import json
log = logging.getLogger('logdna')
log = logging.getLogger("logdna")
log.setLevel(logging.INFO)
@ -30,10 +32,12 @@ logdna_options = {
"index_meta": True,
"hostname": node(),
"ip": get_my_ip_address(),
"mac": get_mac_address()
"mac": get_mac_address(),
}
logdna_handler = LogDNAHandler(getenv("LOGDNA_API_KEY"), options=logdna_options)
logdna_handler = LogDNAHandler(
getenv("LOGDNA_API_KEY"), options=logdna_options
)
logdna = logging.getLogger(__name__)
logdna.setLevel(logging.INFO)
@ -49,13 +53,8 @@ def log_request(request: Request):
@app.route("/")
def default(request):
return json({
"response": "I was here"
})
return json({"response": "I was here"})
if __name__ == "__main__":
app.run(
host="0.0.0.0",
port=getenv("PORT", 8080)
)
app.run(host="0.0.0.0", port=getenv("PORT", 8080))

View File

@ -59,31 +59,31 @@ async def handler_stream(request):
return response.stream(body)
@app.listener("before_server_start")
@app.before_server_start
async def listener_before_server_start(*args, **kwargs):
print("before_server_start")
@app.listener("after_server_start")
@app.after_server_start
async def listener_after_server_start(*args, **kwargs):
print("after_server_start")
@app.listener("before_server_stop")
@app.before_server_stop
async def listener_before_server_stop(*args, **kwargs):
print("before_server_stop")
@app.listener("after_server_stop")
@app.after_server_stop
async def listener_after_server_stop(*args, **kwargs):
print("after_server_stop")
@app.middleware("request")
@app.on_request
async def print_on_request(request):
print("print_on_request")
@app.middleware("response")
@app.on_response
async def print_on_response(request, response):
print("print_on_response")

View File

@ -1,9 +1,10 @@
from sanic import Sanic
from sanic import response
from signal import signal, SIGINT
import asyncio
import uvloop
from sanic import Sanic, response
app = Sanic(__name__)
@ -11,12 +12,19 @@ app = Sanic(__name__)
async def test(request):
return response.json({"answer": "42"})
asyncio.set_event_loop(uvloop.new_event_loop())
server = app.create_server(host="0.0.0.0", port=8000, return_asyncio_server=True)
loop = asyncio.get_event_loop()
task = asyncio.ensure_future(server)
signal(SIGINT, lambda s, f: loop.stop())
try:
loop.run_forever()
except:
loop.stop()
async def main():
server = await app.create_server(
port=8000, host="0.0.0.0", return_asyncio_server=True
)
if server is None:
return
await server.startup()
await server.serve_forever()
if __name__ == "__main__":
asyncio.set_event_loop(uvloop.new_event_loop())
asyncio.run(main())

View File

@ -11,9 +11,24 @@ from sanic.server import AsyncioServer
app = Sanic(__name__)
@app.listener("after_server_start")
async def after_start_test(app, loop):
print("Async Server Started!")
@app.before_server_start
async def before_server_start(app, loop):
print("Async Server starting")
@app.after_server_start
async def after_server_start(app, loop):
print("Async Server started")
@app.before_server_stop
async def before_server_stop(app, loop):
print("Async Server stopping")
@app.after_server_stop
async def after_server_stop(app, loop):
print("Async Server stopped")
@app.route("/")
@ -28,20 +43,20 @@ serv_coro = app.create_server(
loop = asyncio.get_event_loop()
serv_task = asyncio.ensure_future(serv_coro, loop=loop)
signal(SIGINT, lambda s, f: loop.stop())
server: AsyncioServer = loop.run_until_complete(serv_task) # type: ignore
server.startup()
server: AsyncioServer = loop.run_until_complete(serv_task)
loop.run_until_complete(server.startup())
# When using app.run(), this actually triggers before the serv_coro.
# But, in this example, we are using the convenience method, even if it is
# out of order.
server.before_start()
server.after_start()
loop.run_until_complete(server.before_start())
loop.run_until_complete(server.after_start())
try:
loop.run_forever()
except KeyboardInterrupt:
loop.stop()
finally:
server.before_stop()
loop.run_until_complete(server.before_stop())
# Wait for server to close
close_task = server.close()
@ -50,4 +65,4 @@ finally:
# Complete all tasks on the loop
for connection in server.connections:
connection.close_if_idle()
server.after_stop()
loop.run_until_complete(server.after_stop())

View File

@ -1,42 +1,41 @@
from sanic import Sanic
from sanic.views import HTTPMethodView
from sanic.response import text
from sanic.views import HTTPMethodView
app = Sanic('some_name')
app = Sanic("some_name")
class SimpleView(HTTPMethodView):
def get(self, request):
return text('I am get method')
return text("I am get method")
def post(self, request):
return text('I am post method')
return text("I am post method")
def put(self, request):
return text('I am put method')
return text("I am put method")
def patch(self, request):
return text('I am patch method')
return text("I am patch method")
def delete(self, request):
return text('I am delete method')
return text("I am delete method")
class SimpleAsyncView(HTTPMethodView):
async def get(self, request):
return text('I am async get method')
return text("I am async get method")
async def post(self, request):
return text('I am async post method')
return text("I am async post method")
async def put(self, request):
return text('I am async put method')
return text("I am async put method")
app.add_route(SimpleView.as_view(), '/')
app.add_route(SimpleAsyncView.as_view(), '/async')
app.add_route(SimpleView.as_view(), "/")
app.add_route(SimpleAsyncView.as_view(), "/async")
if __name__ == '__main__':
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000, debug=True)

View File

@ -1,9 +1,9 @@
import os
from sanic import Sanic
from sanic.log import logger as log
from sanic import response
from sanic import Sanic, response
from sanic.exceptions import ServerError
from sanic.log import logger as log
app = Sanic(__name__)
@ -13,7 +13,7 @@ async def test_async(request):
return response.json({"test": True})
@app.route("/sync", methods=['GET', 'POST'])
@app.route("/sync", methods=["GET", "POST"])
def test_sync(request):
return response.json({"test": True})
@ -31,6 +31,7 @@ def exception(request):
@app.route("/await")
async def test_await(request):
import asyncio
await asyncio.sleep(5)
return response.text("I'm feeling sleepy")
@ -42,8 +43,10 @@ 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
)
# ----------------------------------------------- #
# Exceptions
@ -52,14 +55,17 @@ async def test_file_stream(request):
@app.exception(ServerError)
async def test(request, exception):
return response.json({"exception": "{}".format(exception), "status": exception.status_code},
status=exception.status_code)
return response.json(
{"exception": str(exception), "status": exception.status_code},
status=exception.status_code,
)
# ----------------------------------------------- #
# Read from request
# ----------------------------------------------- #
@app.route("/json")
def post_json(request):
return response.json({"received": True, "message": request.json})
@ -67,38 +73,51 @@ def post_json(request):
@app.route("/form")
def post_form_json(request):
return response.json({"received": True, "form_data": request.form, "test": request.form.get('test')})
return response.json(
{
"received": True,
"form_data": request.form,
"test": request.form.get("test"),
}
)
@app.route("/query_string")
def query_string(request):
return response.json({"parsed": True, "args": request.args, "url": request.url,
"query_string": request.query_string})
return response.json(
{
"parsed": True,
"args": request.args,
"url": request.url,
"query_string": request.query_string,
}
)
# ----------------------------------------------- #
# Run Server
# ----------------------------------------------- #
@app.listener('before_server_start')
@app.before_server_start
def before_start(app, loop):
log.info("SERVER STARTING")
@app.listener('after_server_start')
@app.after_server_start
def after_start(app, loop):
log.info("OH OH OH OH OHHHHHHHH")
@app.listener('before_server_stop')
@app.before_server_stop
def before_stop(app, loop):
log.info("SERVER STOPPING")
@app.listener('after_server_stop')
@app.after_server_stop
def after_stop(app, loop):
log.info("TRIED EVERYTHING")
if __name__ == '__main__':
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000, debug=True)

View File

@ -1,7 +1,8 @@
from sanic import Sanic
from sanic import response
import socket
import os
import socket
from sanic import Sanic, response
app = Sanic(__name__)
@ -10,14 +11,15 @@ app = Sanic(__name__)
async def test(request):
return response.text("OK")
if __name__ == '__main__':
server_address = './uds_socket'
if __name__ == "__main__":
server_address = "./uds_socket"
# Make sure the socket does not already exist
try:
os.unlink(server_address)
os.unlink(server_address)
except OSError:
if os.path.exists(server_address):
raise
if os.path.exists(server_address):
raise
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.bind(server_address)
app.run(sock=sock)

View File

@ -1,20 +1,21 @@
from sanic import Sanic
from sanic import response
from sanic import Sanic, response
app = Sanic(__name__)
@app.route('/')
@app.route("/")
async def index(request):
# generate a URL for the endpoint `post_handler`
url = app.url_for('post_handler', post_id=5)
url = app.url_for("post_handler", post_id=5)
# the URL is `/posts/5`, redirect to it
return response.redirect(url)
@app.route('/posts/<post_id>')
@app.route("/posts/<post_id>")
async def post_handler(request, post_id):
return response.text('Post - {}'.format(post_id))
return response.text("Post - {}".format(post_id))
if __name__ == '__main__':
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000, debug=True)

View File

@ -8,7 +8,9 @@ app = Sanic(name="blue-print-group-version-example")
bp1 = Blueprint(name="ultron", url_prefix="/ultron")
bp2 = Blueprint(name="vision", url_prefix="/vision", strict_slashes=None)
bpg = Blueprint.group([bp1, bp2], url_prefix="/sentient/robot", version=1, strict_slashes=True)
bpg = Blueprint.group(
bp1, bp2, url_prefix="/sentient/robot", version=1, strict_slashes=True
)
@bp1.get("/name")
@ -31,5 +33,5 @@ async def bp2_revised_name(request):
app.blueprint(bpg)
if __name__ == '__main__':
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)

View File

@ -1,25 +1,27 @@
from sanic import Sanic
from sanic.response import redirect
app = Sanic(__name__)
app.static('index.html', "websocket.html")
app.static("index.html", "websocket.html")
@app.route('/')
@app.route("/")
def index(request):
return redirect("index.html")
@app.websocket('/feed')
@app.websocket("/feed")
async def feed(request, ws):
while True:
data = 'hello!'
print('Sending: ' + data)
data = "hello!"
print("Sending: " + data)
await ws.send(data)
data = await ws.recv()
print('Received: ' + data)
print("Received: " + data)
if __name__ == '__main__':
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000, debug=True)

View File

@ -1,6 +0,0 @@
FROM catthehacker/ubuntu:act-latest
SHELL [ "/bin/bash", "-c" ]
ENTRYPOINT []
RUN apt-get update
RUN apt-get install gcc -y
RUN apt-get install -y --no-install-recommends g++

View File

@ -1,196 +1,15 @@
import os
import sys
from argparse import ArgumentParser, RawTextHelpFormatter
from importlib import import_module
from pathlib import Path
from typing import Any, Dict, Optional
from sanic_routing import __version__ as __routing_version__ # type: ignore
from sanic import __version__
from sanic.app import Sanic
from sanic.config import BASE_LOGO
from sanic.log import error_logger
from sanic.simple import create_simple_server
from sanic.cli.app import SanicCLI
from sanic.compat import OS_IS_WINDOWS, enable_windows_color_support
class SanicArgumentParser(ArgumentParser):
def add_bool_arguments(self, *args, **kwargs):
group = self.add_mutually_exclusive_group()
group.add_argument(*args, action="store_true", **kwargs)
kwargs["help"] = f"no {kwargs['help']}\n "
group.add_argument(
"--no-" + args[0][2:], *args[1:], action="store_false", **kwargs
)
if OS_IS_WINDOWS:
enable_windows_color_support()
def main():
parser = SanicArgumentParser(
prog="sanic",
description=BASE_LOGO,
formatter_class=lambda prog: RawTextHelpFormatter(
prog, max_help_position=33
),
)
parser.add_argument(
"-v",
"--version",
action="version",
version=f"Sanic {__version__}; Routing {__routing_version__}",
)
parser.add_argument(
"--factory",
action="store_true",
help=(
"Treat app as an application factory, "
"i.e. a () -> <Sanic app> callable"
),
)
parser.add_argument(
"-s",
"--simple",
dest="simple",
action="store_true",
help="Run Sanic as a Simple Server (module arg should be a path)\n ",
)
parser.add_argument(
"-H",
"--host",
dest="host",
type=str,
default="127.0.0.1",
help="Host address [default 127.0.0.1]",
)
parser.add_argument(
"-p",
"--port",
dest="port",
type=int,
default=8000,
help="Port to serve on [default 8000]",
)
parser.add_argument(
"-u",
"--unix",
dest="unix",
type=str,
default="",
help="location of unix socket\n ",
)
parser.add_argument(
"--cert", dest="cert", type=str, help="Location of certificate for SSL"
)
parser.add_argument(
"--key", dest="key", type=str, help="location of keyfile for SSL\n "
)
parser.add_bool_arguments(
"--access-logs", dest="access_log", help="display access logs"
)
parser.add_argument(
"-w",
"--workers",
dest="workers",
type=int,
default=1,
help="number of worker processes [default 1]\n ",
)
parser.add_argument("-d", "--debug", dest="debug", action="store_true")
parser.add_argument(
"-r",
"--reload",
"--auto-reload",
dest="auto_reload",
action="store_true",
help="Watch source directory for file changes and reload on changes",
)
parser.add_argument(
"-R",
"--reload-dir",
dest="path",
action="append",
help="Extra directories to watch and reload on changes\n ",
)
parser.add_argument(
"module",
help=(
"Path to your Sanic app. Example: path.to.server:app\n"
"If running a Simple Server, path to directory to serve. "
"Example: ./\n"
),
)
args = parser.parse_args()
try:
module_path = os.path.abspath(os.getcwd())
if module_path not in sys.path:
sys.path.append(module_path)
if args.simple:
path = Path(args.module)
app = create_simple_server(path)
else:
delimiter = ":" if ":" in args.module else "."
module_name, app_name = args.module.rsplit(delimiter, 1)
if app_name.endswith("()"):
args.factory = True
app_name = app_name[:-2]
module = import_module(module_name)
app = getattr(module, app_name, None)
if args.factory:
app = app()
app_type_name = type(app).__name__
if not isinstance(app, Sanic):
raise ValueError(
f"Module is not a Sanic app, it is a {app_type_name}. "
f"Perhaps you meant {args.module}.app?"
)
if args.cert is not None or args.key is not None:
ssl: Optional[Dict[str, Any]] = {
"cert": args.cert,
"key": args.key,
}
else:
ssl = None
kwargs = {
"host": args.host,
"port": args.port,
"unix": args.unix,
"workers": args.workers,
"debug": args.debug,
"access_log": args.access_log,
"ssl": ssl,
}
if args.auto_reload:
kwargs["auto_reload"] = True
if args.path:
if args.auto_reload or args.debug:
kwargs["reload_dir"] = args.path
else:
error_logger.warning(
"Ignoring '--reload-dir' since auto reloading was not "
"enabled. If you would like to watch directories for "
"changes, consider using --debug or --auto-reload."
)
app.run(**kwargs)
except ImportError as e:
if module_name.startswith(e.name):
error_logger.error(
f"No module named {e.name} found.\n"
" Example File: project/sanic_server.py -> app\n"
" Example Module: project.sanic_server.app"
)
else:
raise e
except ValueError:
error_logger.exception("Failed to run app")
cli = SanicCLI()
cli.attach()
cli.run()
if __name__ == "__main__":

View File

@ -1 +1 @@
__version__ = "21.9.1"
__version__ = "21.12.0dev"

View File

@ -3,7 +3,9 @@ from __future__ import annotations
import logging
import logging.config
import os
import platform
import re
import sys
from asyncio import (
AbstractEventLoop,
@ -16,10 +18,11 @@ from asyncio import (
from asyncio.futures import Future
from collections import defaultdict, deque
from functools import partial
from importlib import import_module
from inspect import isawaitable
from pathlib import Path
from socket import socket
from ssl import Purpose, SSLContext, create_default_context
from ssl import SSLContext
from traceback import format_exc
from types import SimpleNamespace
from typing import (
@ -39,17 +42,24 @@ from typing import (
Union,
)
from urllib.parse import urlencode, urlunparse
from warnings import filterwarnings, warn
from sanic_routing.exceptions import FinalizationError # type: ignore
from sanic_routing.exceptions import NotFound # type: ignore
from sanic_routing.exceptions import ( # type: ignore
FinalizationError,
NotFound,
)
from sanic_routing.route import Route # type: ignore
from sanic import reloader_helpers
from sanic.application.logo import get_logo
from sanic.application.motd import MOTD
from sanic.application.state import ApplicationState, Mode
from sanic.asgi import ASGIApp
from sanic.base import BaseSanic
from sanic.blueprint_group import BlueprintGroup
from sanic.blueprints import Blueprint
from sanic.config import BASE_LOGO, SANIC_PREFIX, Config
from sanic.compat import OS_IS_WINDOWS, enable_windows_color_support
from sanic.config import SANIC_PREFIX, Config
from sanic.exceptions import (
InvalidUsage,
SanicException,
@ -57,17 +67,20 @@ from sanic.exceptions import (
URLBuildError,
)
from sanic.handlers import ErrorHandler
from sanic.log import LOGGING_CONFIG_DEFAULTS, error_logger, logger
from sanic.http import Stage
from sanic.log import LOGGING_CONFIG_DEFAULTS, Colors, error_logger, logger
from sanic.mixins.listeners import ListenerEvent
from sanic.models.futures import (
FutureException,
FutureListener,
FutureMiddleware,
FutureRegistry,
FutureRoute,
FutureSignal,
FutureStatic,
)
from sanic.models.handler_types import ListenerType, MiddlewareType
from sanic.models.handler_types import Sanic as SanicVar
from sanic.request import Request
from sanic.response import BaseHTTPResponse, HTTPResponse
from sanic.router import Router
@ -77,9 +90,16 @@ from sanic.server import serve, serve_multiple, serve_single
from sanic.server.protocols.websocket_protocol import WebSocketProtocol
from sanic.server.websockets.impl import ConnectionClosed
from sanic.signals import Signal, SignalRouter
from sanic.tls import process_to_context
from sanic.touchup import TouchUp, TouchUpMeta
if OS_IS_WINDOWS:
enable_windows_color_support()
filterwarnings("once", category=DeprecationWarning)
class Sanic(BaseSanic, metaclass=TouchUpMeta):
"""
The main application instance
@ -92,21 +112,24 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
"_run_request_middleware",
)
__fake_slots__ = (
"_asgi_app",
"_app_registry",
"_asgi_app",
"_asgi_client",
"_blueprint_order",
"_delayed_tasks",
"_future_routes",
"_future_statics",
"_future_middleware",
"_future_listeners",
"_future_exceptions",
"_future_listeners",
"_future_middleware",
"_future_registry",
"_future_routes",
"_future_signals",
"_future_statics",
"_state",
"_test_client",
"_test_manager",
"auto_reload",
"asgi",
"auto_reload",
"auto_reload",
"blueprints",
"config",
"configure_logging",
@ -120,7 +143,6 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
"name",
"named_request_middleware",
"named_response_middleware",
"reload_dirs",
"request_class",
"request_middleware",
"response_middleware",
@ -157,7 +179,8 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
# logging
if configure_logging:
logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS)
dict_config = log_config or LOGGING_CONFIG_DEFAULTS
logging.config.dictConfig(dict_config) # type: ignore
if config and (load_env is not True or env_prefix != SANIC_PREFIX):
raise SanicException(
@ -165,38 +188,35 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
"load_env or env_prefix"
)
self._asgi_client = None
self._asgi_client: Any = None
self._test_client: Any = None
self._test_manager: Any = None
self._blueprint_order: List[Blueprint] = []
self._delayed_tasks: List[str] = []
self._test_client = None
self._test_manager = None
self.asgi = False
self.auto_reload = False
self._future_registry: FutureRegistry = FutureRegistry()
self._state: ApplicationState = ApplicationState(app=self)
self.blueprints: Dict[str, Blueprint] = {}
self.config = config or Config(
load_env=load_env, env_prefix=env_prefix
self.config: Config = config or Config(
load_env=load_env,
env_prefix=env_prefix,
app=self,
)
self.configure_logging = configure_logging
self.ctx = ctx or SimpleNamespace()
self.debug = None
self.error_handler = error_handler or ErrorHandler(
fallback=self.config.FALLBACK_ERROR_FORMAT,
)
self.is_running = False
self.is_stopping = False
self.listeners: Dict[str, List[ListenerType]] = defaultdict(list)
self.configure_logging: bool = configure_logging
self.ctx: Any = ctx or SimpleNamespace()
self.debug = False
self.error_handler: ErrorHandler = error_handler or ErrorHandler()
self.listeners: Dict[str, List[ListenerType[Any]]] = defaultdict(list)
self.named_request_middleware: Dict[str, Deque[MiddlewareType]] = {}
self.named_response_middleware: Dict[str, Deque[MiddlewareType]] = {}
self.reload_dirs: Set[Path] = set()
self.request_class = request_class
self.request_class: Type[Request] = request_class or Request
self.request_middleware: Deque[MiddlewareType] = deque()
self.response_middleware: Deque[MiddlewareType] = deque()
self.router = router or Router()
self.signal_router = signal_router or SignalRouter()
self.sock = None
self.strict_slashes = strict_slashes
self.websocket_enabled = False
self.websocket_tasks: Set[Future] = set()
self.router: Router = router or Router()
self.signal_router: SignalRouter = signal_router or SignalRouter()
self.sock: Optional[socket] = None
self.strict_slashes: bool = strict_slashes
self.websocket_enabled: bool = False
self.websocket_tasks: Set[Future[Any]] = set()
# Register alternative method names
self.go_fast = self.run
@ -232,7 +252,10 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
# Registration
# -------------------------------------------------------------------- #
def add_task(self, task) -> None:
def add_task(
self,
task: Union[Future[Any], Coroutine[Any, Any, Any], Awaitable[Any]],
) -> None:
"""
Schedule a task to run later, after the loop has started.
Different from asyncio.ensure_future in that it does not
@ -255,7 +278,9 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
self.signal(task_name)(partial(self.run_delayed_task, task=task))
self._delayed_tasks.append(task_name)
def register_listener(self, listener: Callable, event: str) -> Any:
def register_listener(
self, listener: ListenerType[SanicVar], event: str
) -> ListenerType[SanicVar]:
"""
Register the listener for a given event.
@ -281,7 +306,9 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
return listener
def register_middleware(self, middleware, attach_to: str = "request"):
def register_middleware(
self, middleware: MiddlewareType, attach_to: str = "request"
) -> MiddlewareType:
"""
Register an application level middleware that will be attached
to all the API URLs registered under this application.
@ -307,14 +334,14 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
def register_named_middleware(
self,
middleware,
middleware: MiddlewareType,
route_names: Iterable[str],
attach_to: str = "request",
):
"""
Method for attaching middleware to specific routes. This is mainly an
internal tool for use by Blueprints to attach middleware to only its
specfic routes. But, it could be used in a more generalized fashion.
specific routes. But, it could be used in a more generalized fashion.
:param middleware: the middleware to execute
:param route_names: a list of the names of the endpoints
@ -701,9 +728,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
A handler that catches specific exceptions and outputs a response.
:param request: The current request object
:type request: :class:`SanicASGITestClient`
:param exception: The exception that was raised
:type exception: BaseException
:raises ServerError: response 500
"""
await self.dispatch(
@ -712,6 +737,50 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
context={"request": request, "exception": exception},
)
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 "
f'the following exception:"{exception}". A previous response '
"has at least partially been sent."
)
# ----------------- deprecated -----------------
handler = self.error_handler._lookup(
exception, request.name if request else None
)
if handler:
warn(
"An error occurred while handling the request after at "
"least some part of the response was sent to the client. "
"Therefore, the response from your custom exception "
f"handler {handler.__name__} will not be sent to the "
"client. Beginning in v22.6, Sanic will stop executing "
"custom exception handlers in this scenario. Exception "
"handlers should only be used to generate the exception "
"responses. If you would like to perform any other "
"action on a raised exception, please consider using a "
"signal handler like "
'`@app.signal("http.lifecycle.exception")`\n'
"For further information, please see the docs: "
"https://sanicframework.org/en/guide/advanced/"
"signals.html",
DeprecationWarning,
)
try:
response = self.error_handler.response(request, exception)
if isawaitable(response):
response = await response
except BaseException as e:
logger.error("An error occurred in the exception handler.")
error_logger.exception(e)
# ----------------------------------------------
return
# -------------------------------------------- #
# Request Middleware
# -------------------------------------------- #
@ -741,6 +810,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
)
if response is not None:
try:
request.reset_response()
response = await request.respond(response)
except BaseException:
# Skip response middleware
@ -752,6 +822,14 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
if request.stream:
response = request.stream.response
if isinstance(response, BaseHTTPResponse):
await self.dispatch(
"http.lifecycle.response",
inline=True,
context={
"request": request,
"response": response,
},
)
await response.send(end_stream=True)
else:
raise ServerError(
@ -842,7 +920,16 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
if isawaitable(response):
response = await response
if response is not None:
if request.responded:
if response is not None:
error_logger.error(
"The response object returned by the route handler "
"will not be sent to client. The request has already "
"been responded to."
)
if request.stream is not None:
response = request.stream.response
elif response is not None:
response = await request.respond(response)
elif not hasattr(handler, "is_websocket"):
response = request.stream.response # type: ignore
@ -937,6 +1024,10 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
# Execution
# -------------------------------------------------------------------- #
def make_coffee(self, *args, **kwargs):
self.state.coffee = True
self.run(*args, **kwargs)
def run(
self,
host: Optional[str] = None,
@ -944,7 +1035,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
*,
debug: bool = False,
auto_reload: Optional[bool] = None,
ssl: Union[Dict[str, str], SSLContext, None] = None,
ssl: Union[None, SSLContext, dict, str, list, tuple] = None,
sock: Optional[socket] = None,
workers: int = 1,
protocol: Optional[Type[Protocol]] = None,
@ -952,8 +1043,13 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
register_sys_signals: bool = True,
access_log: Optional[bool] = None,
unix: Optional[str] = None,
loop: None = None,
loop: AbstractEventLoop = None,
reload_dir: Optional[Union[List[str], str]] = None,
noisy_exceptions: Optional[bool] = None,
motd: bool = True,
fast: bool = False,
verbosity: int = 0,
motd_display: Optional[Dict[str, str]] = None,
) -> None:
"""
Run the HTTP Server and listen until keyboard interrupt or term
@ -970,7 +1066,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
:type auto_relaod: bool
:param ssl: SSLContext, or location of certificate and key
for SSL encryption of worker(s)
:type ssl: SSLContext or dict
:type ssl: str, dict, SSLContext or list
:param sock: Socket for the server to accept connections from
:type sock: socket
:param workers: Number of processes received before it is respected
@ -986,8 +1082,19 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
:type access_log: bool
:param unix: Unix socket to listen on instead of TCP port
:type unix: str
:param noisy_exceptions: Log exceptions that are normally considered
to be quiet/silent
:type noisy_exceptions: bool
:return: Nothing
"""
self.state.verbosity = verbosity
if fast and workers != 1:
raise RuntimeError("You cannot use both fast=True and workers=X")
if motd_display:
self.config.MOTD_DISPLAY.update(motd_display)
if reload_dir:
if isinstance(reload_dir, str):
reload_dir = [reload_dir]
@ -998,7 +1105,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
logger.warning(
f"Directory {directory} could not be located"
)
self.reload_dirs.add(Path(directory))
self.state.reload_dirs.add(Path(directory))
if loop is not None:
raise TypeError(
@ -1009,7 +1116,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
)
if auto_reload or auto_reload is None and debug:
self.auto_reload = True
auto_reload = True
if os.environ.get("SANIC_SERVER_RUNNING") != "true":
return reloader_helpers.watchdog(1.0, self)
@ -1020,9 +1127,23 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
protocol = (
WebSocketProtocol if self.websocket_enabled else HttpProtocol
)
# if access_log is passed explicitly change config.ACCESS_LOG
if access_log is not None:
self.config.ACCESS_LOG = access_log
# Set explicitly passed configuration values
for attribute, value in {
"ACCESS_LOG": access_log,
"AUTO_RELOAD": auto_reload,
"MOTD": motd,
"NOISY_EXCEPTIONS": noisy_exceptions,
}.items():
if value is not None:
setattr(self.config, attribute, value)
if fast:
self.state.fast = True
try:
workers = len(os.sched_getaffinity(0))
except AttributeError:
workers = os.cpu_count() or 1
server_settings = self._helper(
host=host,
@ -1035,7 +1156,6 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
protocol=protocol,
backlog=backlog,
register_sys_signals=register_sys_signals,
auto_reload=auto_reload,
)
try:
@ -1074,7 +1194,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
port: Optional[int] = None,
*,
debug: bool = False,
ssl: Union[Dict[str, str], SSLContext, None] = None,
ssl: Union[None, SSLContext, dict, str, list, tuple] = None,
sock: Optional[socket] = None,
protocol: Type[Protocol] = None,
backlog: int = 100,
@ -1082,6 +1202,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
unix: Optional[str] = None,
return_asyncio_server: bool = False,
asyncio_server_kwargs: Dict[str, Any] = None,
noisy_exceptions: Optional[bool] = None,
) -> Optional[AsyncioServer]:
"""
Asynchronous version of :func:`run`.
@ -1119,6 +1240,9 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
:param asyncio_server_kwargs: key-value arguments for
asyncio/uvloop create_server method
:type asyncio_server_kwargs: dict
:param noisy_exceptions: Log exceptions that are normally considered
to be quiet/silent
:type noisy_exceptions: bool
:return: AsyncioServer if return_asyncio_server is true, else Nothing
"""
@ -1129,10 +1253,14 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
protocol = (
WebSocketProtocol if self.websocket_enabled else HttpProtocol
)
# if access_log is passed explicitly change config.ACCESS_LOG
if access_log is not None:
self.config.ACCESS_LOG = access_log
if noisy_exceptions is not None:
self.config.NOISY_EXCEPTIONS = noisy_exceptions
server_settings = self._helper(
host=host,
port=port,
@ -1243,31 +1371,20 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
def _helper(
self,
host=None,
port=None,
debug=False,
ssl=None,
sock=None,
unix=None,
workers=1,
loop=None,
protocol=HttpProtocol,
backlog=100,
register_sys_signals=True,
run_async=False,
auto_reload=False,
host: Optional[str] = None,
port: Optional[int] = None,
debug: bool = False,
ssl: Union[None, SSLContext, dict, str, list, tuple] = None,
sock: Optional[socket] = None,
unix: Optional[str] = None,
workers: int = 1,
loop: AbstractEventLoop = None,
protocol: Type[Protocol] = HttpProtocol,
backlog: int = 100,
register_sys_signals: bool = True,
run_async: bool = False,
):
"""Helper function used by `run` and `create_server`."""
if isinstance(ssl, dict):
# try common aliaseses
cert = ssl.get("cert") or ssl.get("certificate")
key = ssl.get("key") or ssl.get("keyfile")
if cert is None or key is None:
raise ValueError("SSLContext or certificate and key required.")
context = create_default_context(purpose=Purpose.CLIENT_AUTH)
context.load_cert_chain(cert, keyfile=key)
ssl = context
if self.config.PROXIES_COUNT and self.config.PROXIES_COUNT < 0:
raise ValueError(
"PROXIES_COUNT cannot be negative. "
@ -1275,8 +1392,26 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
"#proxy-configuration"
)
self.error_handler.debug = debug
self.debug = debug
self.state.host = host
self.state.port = port
self.state.workers = workers
# Serve
serve_location = ""
proto = "http"
if ssl is not None:
proto = "https"
if unix:
serve_location = f"{unix} {proto}://..."
elif sock:
serve_location = f"{sock.getsockname()} {proto}://..."
elif host and port:
# colon(:) is legal for a host only in an ipv6 address
display_host = f"[{host}]" if ":" in host else host
serve_location = f"{proto}://{display_host}:{port}"
ssl = process_to_context(ssl)
server_settings = {
"protocol": protocol,
@ -1292,8 +1427,16 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
"backlog": backlog,
}
# Register start/stop events
self.motd(serve_location)
if sys.stdout.isatty() and not self.state.is_debug:
error_logger.warning(
f"{Colors.YELLOW}Sanic is running in PRODUCTION mode. "
"Consider using '--debug' or '--dev' while actively "
f"developing your application.{Colors.END}"
)
# Register start/stop events
for event_name, settings_name, reverse in (
("main_process_start", "main_start", False),
("main_process_stop", "main_stop", True),
@ -1303,39 +1446,11 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
listeners.reverse()
# Prepend sanic to the arguments when listeners are triggered
listeners = [partial(listener, self) for listener in listeners]
server_settings[settings_name] = listeners
if self.configure_logging and debug:
logger.setLevel(logging.DEBUG)
if (
self.config.LOGO
and os.environ.get("SANIC_SERVER_RUNNING") != "true"
):
logger.debug(
self.config.LOGO
if isinstance(self.config.LOGO, str)
else BASE_LOGO
)
server_settings[settings_name] = listeners # type: ignore
if run_async:
server_settings["run_async"] = True
# Serve
if host and port:
proto = "http"
if ssl is not None:
proto = "https"
if unix:
logger.info(f"Goin' Fast @ {unix} {proto}://...")
else:
logger.info(f"Goin' Fast @ {proto}://{host}:{port}")
debug_mode = "enabled" if self.debug else "disabled"
reload_mode = "enabled" if auto_reload else "disabled"
logger.debug(f"Sanic auto-reload: {reload_mode}")
logger.debug(f"Sanic debug mode: {debug_mode}")
return server_settings
def _build_endpoint_name(self, *parts):
@ -1392,6 +1507,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
details: https://asgi.readthedocs.io/en/latest
"""
self.asgi = True
self.motd("")
self._asgi_app = await ASGIApp.create(self, scope, receive, send)
asgi_app = self._asgi_app
await asgi_app()
@ -1412,6 +1528,114 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
self.config.update_config(config)
@property
def asgi(self):
return self.state.asgi
@asgi.setter
def asgi(self, value: bool):
self.state.asgi = value
@property
def debug(self):
return self.state.is_debug
@debug.setter
def debug(self, value: bool):
mode = Mode.DEBUG if value else Mode.PRODUCTION
self.state.mode = mode
@property
def auto_reload(self):
return self.config.AUTO_RELOAD
@auto_reload.setter
def auto_reload(self, value: bool):
self.config.AUTO_RELOAD = value
@property
def state(self):
return self._state
@property
def is_running(self):
return self.state.is_running
@is_running.setter
def is_running(self, value: bool):
self.state.is_running = value
@property
def is_stopping(self):
return self.state.is_stopping
@is_stopping.setter
def is_stopping(self, value: bool):
self.state.is_stopping = value
@property
def reload_dirs(self):
return self.state.reload_dirs
def motd(self, serve_location):
if self.config.MOTD:
mode = [f"{self.state.mode},"]
if self.state.fast:
mode.append("goin' fast")
if self.state.asgi:
mode.append("ASGI")
else:
if self.state.workers == 1:
mode.append("single worker")
else:
mode.append(f"w/ {self.state.workers} workers")
display = {
"mode": " ".join(mode),
"server": self.state.server,
"python": platform.python_version(),
"platform": platform.platform(),
}
extra = {}
if self.config.AUTO_RELOAD:
reload_display = "enabled"
if self.state.reload_dirs:
reload_display += ", ".join(
[
"",
*(
str(path.absolute())
for path in self.state.reload_dirs
),
]
)
display["auto-reload"] = reload_display
packages = []
for package_name, module_name in {
"sanic-routing": "sanic_routing",
"sanic-testing": "sanic_testing",
"sanic-ext": "sanic_ext",
}.items():
try:
module = import_module(module_name)
packages.append(f"{package_name}=={module.__version__}")
except ImportError:
...
if packages:
display["packages"] = ", ".join(packages)
if self.config.MOTD_DISPLAY:
extra.update(self.config.MOTD_DISPLAY)
logo = (
get_logo(coffee=self.state.coffee)
if self.config.LOGO == "" or self.config.LOGO is True
else self.config.LOGO
)
MOTD.output(logo, serve_location, display, extra)
# -------------------------------------------------------------------- #
# Class methods
# -------------------------------------------------------------------- #
@ -1472,10 +1696,14 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
raise e
async def _startup(self):
self._future_registry.clear()
self.signalize()
self.finalize()
ErrorHandler.finalize(self.error_handler)
ErrorHandler.finalize(
self.error_handler, fallback=self.config.FALLBACK_ERROR_FORMAT
)
TouchUp.run(self)
self.state.is_started = True
async def _server_event(
self,
@ -1489,7 +1717,8 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
"shutdown",
):
raise SanicException(f"Invalid server event: {event}")
logger.debug(f"Triggering server events: {event}")
if self.state.verbosity >= 1:
logger.debug(f"Triggering server events: {event}")
reverse = concern == "shutdown"
if loop is None:
loop = self.loop

View File

57
sanic/application/logo.py Normal file
View File

@ -0,0 +1,57 @@
import re
import sys
from os import environ
BASE_LOGO = """
Sanic
Build Fast. Run Fast.
"""
COFFEE_LOGO = """\033[48;2;255;13;104m \033[0m
\033[38;2;255;255;255;48;2;255;13;104m \033[0m
\033[38;2;255;255;255;48;2;255;13;104m \033[0m
\033[38;2;255;255;255;48;2;255;13;104m \033[0m
\033[38;2;255;255;255;48;2;255;13;104m \033[0m
\033[38;2;255;255;255;48;2;255;13;104m \033[0m
\033[48;2;255;13;104m \033[0m
Dark roast. No sugar."""
COLOR_LOGO = """\033[48;2;255;13;104m \033[0m
\033[38;2;255;255;255;48;2;255;13;104m \033[0m
\033[38;2;255;255;255;48;2;255;13;104m \033[0m
\033[38;2;255;255;255;48;2;255;13;104m \033[0m
\033[38;2;255;255;255;48;2;255;13;104m \033[0m
\033[38;2;255;255;255;48;2;255;13;104m \033[0m
\033[48;2;255;13;104m \033[0m
Build Fast. Run Fast."""
FULL_COLOR_LOGO = """
\033[38;2;255;13;104m \033[0m
\033[38;2;255;13;104m \033[0m
\033[38;2;255;13;104m \033[0m
\033[38;2;255;13;104m \033[0m
\033[38;2;255;13;104m \033[0m
""" # noqa
ansi_pattern = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
def get_logo(full=False, coffee=False):
logo = (
(FULL_COLOR_LOGO if full else (COFFEE_LOGO if coffee else COLOR_LOGO))
if sys.stdout.isatty()
else BASE_LOGO
)
if (
sys.platform == "darwin"
and environ.get("TERM_PROGRAM") == "Apple_Terminal"
):
logo = ansi_pattern.sub("", logo)
return logo

146
sanic/application/motd.py Normal file
View File

@ -0,0 +1,146 @@
import sys
from abc import ABC, abstractmethod
from shutil import get_terminal_size
from textwrap import indent, wrap
from typing import Dict, Optional
from sanic import __version__
from sanic.log import logger
class MOTD(ABC):
def __init__(
self,
logo: Optional[str],
serve_location: str,
data: Dict[str, str],
extra: Dict[str, str],
) -> None:
self.logo = logo
self.serve_location = serve_location
self.data = data
self.extra = extra
self.key_width = 0
self.value_width = 0
@abstractmethod
def display(self):
... # noqa
@classmethod
def output(
cls,
logo: Optional[str],
serve_location: str,
data: Dict[str, str],
extra: Dict[str, str],
) -> None:
motd_class = MOTDTTY if sys.stdout.isatty() else MOTDBasic
motd_class(logo, serve_location, data, extra).display()
class MOTDBasic(MOTD):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
def display(self):
if self.logo:
logger.debug(self.logo)
lines = [f"Sanic v{__version__}"]
if self.serve_location:
lines.append(f"Goin' Fast @ {self.serve_location}")
lines += [
*(f"{key}: {value}" for key, value in self.data.items()),
*(f"{key}: {value}" for key, value in self.extra.items()),
]
for line in lines:
logger.info(line)
class MOTDTTY(MOTD):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.set_variables()
def set_variables(self): # no cov
fallback = (108, 24)
terminal_width = max(
get_terminal_size(fallback=fallback).columns, fallback[0]
)
self.max_value_width = terminal_width - fallback[0] + 36
self.key_width = 4
self.value_width = self.max_value_width
if self.data:
self.key_width = max(map(len, self.data.keys()))
self.value_width = min(
max(map(len, self.data.values())), self.max_value_width
)
self.logo_lines = self.logo.split("\n") if self.logo else []
self.logo_line_length = 24
self.centering_length = (
self.key_width + self.value_width + 2 + self.logo_line_length
)
self.display_length = self.key_width + self.value_width + 2
def display(self):
version = f"Sanic v{__version__}".center(self.centering_length)
running = (
f"Goin' Fast @ {self.serve_location}"
if self.serve_location
else ""
).center(self.centering_length)
length = len(version) + 2 - self.logo_line_length
first_filler = "" * (self.logo_line_length - 1)
second_filler = "" * length
display_filler = "" * (self.display_length + 2)
lines = [
f"\n{first_filler}{second_filler}",
f"{version}",
f"{running}",
f"{first_filler}{second_filler}",
]
self._render_data(lines, self.data, 0)
if self.extra:
logo_part = self._get_logo_part(len(lines) - 4)
lines.append(f"| {logo_part}{display_filler}")
self._render_data(lines, self.extra, len(lines) - 4)
self._render_fill(lines)
lines.append(f"{first_filler}{second_filler}\n")
logger.info(indent("\n".join(lines), " "))
def _render_data(self, lines, data, start):
offset = 0
for idx, (key, value) in enumerate(data.items(), start=start):
key = key.rjust(self.key_width)
wrapped = wrap(value, self.max_value_width, break_on_hyphens=False)
for wrap_index, part in enumerate(wrapped):
part = part.ljust(self.value_width)
logo_part = self._get_logo_part(idx + offset + wrap_index)
display = (
f"{key}: {part}"
if wrap_index == 0
else (" " * len(key) + f" {part}")
)
lines.append(f"{logo_part}{display}")
if wrap_index:
offset += 1
def _render_fill(self, lines):
filler = " " * self.display_length
idx = len(lines) - 5
for i in range(1, len(self.logo_lines) - idx):
logo_part = self.logo_lines[idx + i]
lines.append(f"{logo_part}{filler}")
def _get_logo_part(self, idx):
try:
logo_part = self.logo_lines[idx]
except IndexError:
logo_part = " " * (self.logo_line_length - 3)
return logo_part

View File

@ -0,0 +1,74 @@
from __future__ import annotations
import logging
from dataclasses import dataclass, field
from enum import Enum, auto
from pathlib import Path
from typing import TYPE_CHECKING, Any, Set, Union
from sanic.log import logger
if TYPE_CHECKING:
from sanic import Sanic
class StrEnum(str, Enum):
def _generate_next_value_(name: str, *args) -> str: # type: ignore
return name.lower()
class Server(StrEnum):
SANIC = auto()
ASGI = auto()
GUNICORN = auto()
class Mode(StrEnum):
PRODUCTION = auto()
DEBUG = auto()
@dataclass
class ApplicationState:
app: Sanic
asgi: bool = field(default=False)
coffee: bool = field(default=False)
fast: bool = field(default=False)
host: str = field(default="")
mode: Mode = field(default=Mode.PRODUCTION)
port: int = field(default=0)
reload_dirs: Set[Path] = field(default_factory=set)
server: Server = field(default=Server.SANIC)
is_running: bool = field(default=False)
is_started: bool = field(default=False)
is_stopping: bool = field(default=False)
verbosity: int = field(default=0)
workers: int = field(default=0)
# This property relates to the ApplicationState instance and should
# not be changed except in the __post_init__ method
_init: bool = field(default=False)
def __post_init__(self) -> None:
self._init = True
def __setattr__(self, name: str, value: Any) -> None:
if self._init and name == "_init":
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]):
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:
logger.setLevel(logging.DEBUG)
@property
def is_debug(self):
return self.mode is Mode.DEBUG

View File

@ -7,8 +7,10 @@ import sanic.app # noqa
from sanic.compat import Header
from sanic.exceptions import ServerError
from sanic.http import Stage
from sanic.models.asgi import ASGIReceive, ASGIScope, ASGISend, MockTransport
from sanic.request import Request
from sanic.response import BaseHTTPResponse
from sanic.server import ConnInfo
from sanic.server.websockets.connection import WebSocketConnection
@ -83,6 +85,8 @@ class ASGIApp:
transport: MockTransport
lifespan: Lifespan
ws: Optional[WebSocketConnection]
stage: Stage
response: Optional[BaseHTTPResponse]
def __init__(self) -> None:
self.ws = None
@ -95,6 +99,8 @@ class ASGIApp:
instance.sanic_app = sanic_app
instance.transport = MockTransport(scope, receive, send)
instance.transport.loop = sanic_app.loop
instance.stage = Stage.IDLE
instance.response = None
setattr(instance.transport, "add_task", sanic_app.loop.create_task)
headers = Header(
@ -149,6 +155,8 @@ class ASGIApp:
"""
Read and stream the body in chunks from an incoming ASGI message.
"""
if self.stage is Stage.IDLE:
self.stage = Stage.REQUEST
message = await self.transport.receive()
body = message.get("body", b"")
if not message.get("more_body", False):
@ -163,11 +171,17 @@ class ASGIApp:
if data:
yield data
def respond(self, response):
def respond(self, response: BaseHTTPResponse):
if self.stage is not Stage.HANDLER:
self.stage = Stage.FAILED
raise RuntimeError("Response already started")
if self.response is not None:
self.response.stream = None
response.stream, self.response = self, response
return response
async def send(self, data, end_stream):
self.stage = Stage.IDLE if end_stream else Stage.RESPONSE
if self.response:
response, self.response = self.response, None
await self.transport.send(
@ -195,6 +209,7 @@ class ASGIApp:
Handle the incoming request.
"""
try:
self.stage = Stage.HANDLER
await self.sanic_app.handle_request(self.request)
except Exception as e:
await self.sanic_app.handle_exception(self.request, e)

View File

@ -11,7 +11,7 @@ from sanic.mixins.routes import RouteMixin
from sanic.mixins.signals import SignalMixin
VALID_NAME = re.compile(r"^[a-zA-Z][a-zA-Z0-9_\-]*$")
VALID_NAME = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_\-]*$")
class BaseSanic(
@ -23,7 +23,7 @@ class BaseSanic(
):
__fake_slots__: Tuple[str, ...]
def __init__(self, name: str = None, *args, **kwargs) -> None:
def __init__(self, name: str = None, *args: Any, **kwargs: Any) -> None:
class_name = self.__class__.__name__
if name is None:

View File

@ -4,8 +4,22 @@ import asyncio
from collections import defaultdict
from copy import deepcopy
from functools import wraps
from inspect import isfunction
from itertools import chain
from types import SimpleNamespace
from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Set, Union
from typing import (
TYPE_CHECKING,
Any,
Dict,
Iterable,
List,
Optional,
Sequence,
Set,
Tuple,
Union,
)
from sanic_routing.exceptions import NotFound # type: ignore
from sanic_routing.route import Route # type: ignore
@ -26,6 +40,32 @@ if TYPE_CHECKING:
from sanic import Sanic # noqa
def lazy(func, as_decorator=True):
@wraps(func)
def decorator(bp, *args, **kwargs):
nonlocal as_decorator
kwargs["apply"] = False
pass_handler = None
if args and isfunction(args[0]):
as_decorator = False
def wrapper(handler):
future = func(bp, *args, **kwargs)
if as_decorator:
future = future(handler)
if bp.registered:
for app in bp.apps:
bp.register(app, {})
return future
return wrapper if as_decorator else wrapper(pass_handler)
return decorator
class Blueprint(BaseSanic):
"""
In *Sanic* terminology, a **Blueprint** is a logical collection of
@ -39,7 +79,7 @@ class Blueprint(BaseSanic):
:param name: unique name of the blueprint
:param url_prefix: URL to be prefixed before all route URLs
:param host: IP Address of FQDN for the sanic server to use.
:param host: IP Address or FQDN for the sanic server to use.
:param version: Blueprint Version
:param strict_slashes: Enforce the API urls are requested with a
trailing */*
@ -72,7 +112,7 @@ class Blueprint(BaseSanic):
self,
name: str = None,
url_prefix: Optional[str] = None,
host: Optional[str] = None,
host: Optional[Union[List[str], str]] = None,
version: Optional[Union[int, str, float]] = None,
strict_slashes: Optional[bool] = None,
version_prefix: str = "/v",
@ -115,34 +155,21 @@ class Blueprint(BaseSanic):
)
return self._apps
def route(self, *args, **kwargs):
kwargs["apply"] = False
return super().route(*args, **kwargs)
@property
def registered(self) -> bool:
return bool(self._apps)
def static(self, *args, **kwargs):
kwargs["apply"] = False
return super().static(*args, **kwargs)
def middleware(self, *args, **kwargs):
kwargs["apply"] = False
return super().middleware(*args, **kwargs)
def listener(self, *args, **kwargs):
kwargs["apply"] = False
return super().listener(*args, **kwargs)
def exception(self, *args, **kwargs):
kwargs["apply"] = False
return super().exception(*args, **kwargs)
def signal(self, event: str, *args, **kwargs):
kwargs["apply"] = False
return super().signal(event, *args, **kwargs)
exception = lazy(BaseSanic.exception)
listener = lazy(BaseSanic.listener)
middleware = lazy(BaseSanic.middleware)
route = lazy(BaseSanic.route)
signal = lazy(BaseSanic.signal)
static = lazy(BaseSanic.static, as_decorator=False)
def reset(self):
self._apps: Set[Sanic] = set()
self.exceptions: List[RouteHandler] = []
self.listeners: Dict[str, List[ListenerType]] = {}
self.listeners: Dict[str, List[ListenerType[Any]]] = {}
self.middlewares: List[MiddlewareType] = []
self.routes: List[Route] = []
self.statics: List[RouteHandler] = []
@ -221,7 +248,7 @@ class Blueprint(BaseSanic):
version: Optional[Union[int, str, float]] = None,
strict_slashes: Optional[bool] = None,
version_prefix: str = "/v",
):
) -> BlueprintGroup:
"""
Create a list of blueprints, optionally grouping them under a
general URL prefix.
@ -274,6 +301,7 @@ class Blueprint(BaseSanic):
middleware = []
exception_handlers = []
listeners = defaultdict(list)
registered = set()
# Routes
for future in self._future_routes:
@ -300,12 +328,15 @@ class Blueprint(BaseSanic):
)
name = app._generate_name(future.name)
host = future.host or self.host
if isinstance(host, list):
host = tuple(host)
apply_route = FutureRoute(
future.handler,
uri[1:] if uri.startswith("//") else uri,
future.methods,
future.host or self.host,
host,
strict_slashes,
future.stream,
version,
@ -319,6 +350,10 @@ class Blueprint(BaseSanic):
error_format,
)
if (self, apply_route) in app._future_registry:
continue
registered.add(apply_route)
route = app._apply_route(apply_route)
operation = (
routes.extend if isinstance(route, list) else routes.append
@ -330,6 +365,11 @@ class Blueprint(BaseSanic):
# Prepend the blueprint URI prefix if available
uri = url_prefix + future.uri if url_prefix else future.uri
apply_route = FutureStatic(uri, *future[1:])
if (self, apply_route) in app._future_registry:
continue
registered.add(apply_route)
route = app._apply_static(apply_route)
routes.append(route)
@ -338,30 +378,51 @@ class Blueprint(BaseSanic):
if route_names:
# Middleware
for future in self._future_middleware:
if (self, future) in app._future_registry:
continue
middleware.append(app._apply_middleware(future, route_names))
# Exceptions
for future in self._future_exceptions:
if (self, future) in app._future_registry:
continue
exception_handlers.append(
app._apply_exception_handler(future, route_names)
)
# Event listeners
for listener in self._future_listeners:
listeners[listener.event].append(app._apply_listener(listener))
for future in self._future_listeners:
if (self, future) in app._future_registry:
continue
listeners[future.event].append(app._apply_listener(future))
# Signals
for signal in self._future_signals:
signal.condition.update({"blueprint": self.name})
app._apply_signal(signal)
for future in self._future_signals:
if (self, future) in app._future_registry:
continue
future.condition.update({"blueprint": self.name})
app._apply_signal(future)
self.routes = [route for route in routes if isinstance(route, Route)]
self.websocket_routes = [
self.routes += [route for route in routes if isinstance(route, Route)]
self.websocket_routes += [
route for route in self.routes if route.ctx.websocket
]
self.middlewares = middleware
self.exceptions = exception_handlers
self.listeners = dict(listeners)
self.middlewares += middleware
self.exceptions += exception_handlers
self.listeners.update(dict(listeners))
if self.registered:
self.register_futures(
self.apps,
self,
chain(
registered,
self._future_middleware,
self._future_exceptions,
self._future_listeners,
self._future_signals,
),
)
async def dispatch(self, *args, **kwargs):
condition = kwargs.pop("condition", {})
@ -393,3 +454,10 @@ class Blueprint(BaseSanic):
value = v
break
return value
@staticmethod
def register_futures(
apps: Set[Sanic], bp: Blueprint, futures: Sequence[Tuple[Any, ...]]
):
for app in apps:
app._future_registry.update(set((bp, item) for item in futures))

0
sanic/cli/__init__.py Normal file
View File

189
sanic/cli/app.py Normal file
View File

@ -0,0 +1,189 @@
import os
import shutil
import sys
from argparse import ArgumentParser, RawTextHelpFormatter
from importlib import import_module
from pathlib import Path
from textwrap import indent
from typing import Any, List, Union
from sanic.app import Sanic
from sanic.application.logo import get_logo
from sanic.cli.arguments import Group
from sanic.log import error_logger
from sanic.simple import create_simple_server
class SanicArgumentParser(ArgumentParser):
...
class SanicCLI:
DESCRIPTION = indent(
f"""
{get_logo(True)}
To start running a Sanic application, provide a path to the module, where
app is a Sanic() instance:
$ sanic path.to.server:app
Or, a path to a callable that returns a Sanic() instance:
$ sanic path.to.factory:create_app --factory
Or, a path to a directory to run as a simple HTTP server:
$ sanic ./path/to/static --simple
""",
prefix=" ",
)
def __init__(self) -> None:
width = shutil.get_terminal_size().columns
self.parser = SanicArgumentParser(
prog="sanic",
description=self.DESCRIPTION,
formatter_class=lambda prog: RawTextHelpFormatter(
prog,
max_help_position=36 if width > 96 else 24,
indent_increment=4,
width=None,
),
)
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.args: List[Any] = []
def attach(self):
for group in Group._registry:
group.create(self.parser).attach()
def run(self):
# This is to provide backwards compat -v to display version
legacy_version = len(sys.argv) == 2 and sys.argv[-1] == "-v"
parse_args = ["--version"] if legacy_version else None
self.args = self.parser.parse_args(args=parse_args)
self._precheck()
try:
app = self._get_app()
kwargs = self._build_run_kwargs()
app.run(**kwargs)
except ValueError:
error_logger.exception("Failed to run app")
def _precheck(self):
if self.args.debug and self.main_process:
error_logger.warning(
"Starting in v22.3, --debug will no "
"longer automatically run the auto-reloader.\n Switch to "
"--dev to continue using that functionality."
)
# # Custom TLS mismatch handling for better diagnostics
if self.main_process and (
# one of cert/key missing
bool(self.args.cert) != bool(self.args.key)
# new and old style self.args used together
or self.args.tls
and self.args.cert
# strict host checking without certs would always fail
or self.args.tlshost
and not self.args.tls
and not self.args.cert
):
self.parser.print_usage(sys.stderr)
message = (
"TLS certificates must be specified by either of:\n"
" --cert certdir/fullchain.pem --key certdir/privkey.pem\n"
" --tls certdir (equivalent to the above)"
)
error_logger.error(message)
sys.exit(1)
def _get_app(self):
try:
module_path = os.path.abspath(os.getcwd())
if module_path not in sys.path:
sys.path.append(module_path)
if self.args.simple:
path = Path(self.args.module)
app = create_simple_server(path)
else:
delimiter = ":" if ":" in self.args.module else "."
module_name, app_name = self.args.module.rsplit(delimiter, 1)
if app_name.endswith("()"):
self.args.factory = True
app_name = app_name[:-2]
module = import_module(module_name)
app = getattr(module, app_name, None)
if self.args.factory:
app = app()
app_type_name = type(app).__name__
if not isinstance(app, Sanic):
raise ValueError(
f"Module is not a Sanic app, it is a {app_type_name}\n"
f" Perhaps you meant {self.args.module}.app?"
)
except ImportError as e:
if module_name.startswith(e.name):
error_logger.error(
f"No module named {e.name} found.\n"
" Example File: project/sanic_server.py -> app\n"
" Example Module: project.sanic_server.app"
)
else:
raise e
return app
def _build_run_kwargs(self):
ssl: Union[None, dict, str, list] = []
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))
if self.args.tls:
ssl += self.args.tls
if not ssl:
ssl = None
elif len(ssl) == 1 and ssl[0] is not None:
# Use only one cert, no TLSSelector.
ssl = ssl[0]
kwargs = {
"access_log": self.args.access_log,
"debug": self.args.debug,
"fast": self.args.fast,
"host": self.args.host,
"motd": self.args.motd,
"noisy_exceptions": self.args.noisy_exceptions,
"port": self.args.port,
"ssl": ssl,
"unix": self.args.unix,
"verbosity": self.args.verbosity or 0,
"workers": self.args.workers,
}
if self.args.auto_reload:
kwargs["auto_reload"] = True
if self.args.path:
if self.args.auto_reload or self.args.debug:
kwargs["reload_dir"] = self.args.path
else:
error_logger.warning(
"Ignoring '--reload-dir' since auto reloading was not "
"enabled. If you would like to watch directories for "
"changes, consider using --debug or --auto-reload."
)
return kwargs

237
sanic/cli/arguments.py Normal file
View File

@ -0,0 +1,237 @@
from __future__ import annotations
from argparse import ArgumentParser, _ArgumentGroup
from typing import List, Optional, Type, Union
from sanic_routing import __version__ as __routing_version__ # type: ignore
from sanic import __version__
class Group:
name: Optional[str]
container: Union[ArgumentParser, _ArgumentGroup]
_registry: List[Type[Group]] = []
def __init_subclass__(cls) -> None:
Group._registry.append(cls)
def __init__(self, parser: ArgumentParser, title: Optional[str]):
self.parser = parser
if title:
self.container = self.parser.add_argument_group(title=f" {title}")
else:
self.container = self.parser
@classmethod
def create(cls, parser: ArgumentParser):
instance = cls(parser, cls.name)
return instance
def add_bool_arguments(self, *args, **kwargs):
group = self.container.add_mutually_exclusive_group()
kwargs["help"] = kwargs["help"].capitalize()
group.add_argument(*args, action="store_true", **kwargs)
kwargs["help"] = f"no {kwargs['help'].lower()}".capitalize()
group.add_argument(
"--no-" + args[0][2:], *args[1:], action="store_false", **kwargs
)
class GeneralGroup(Group):
name = None
def attach(self):
self.container.add_argument(
"--version",
action="version",
version=f"Sanic {__version__}; Routing {__routing_version__}",
)
self.container.add_argument(
"module",
help=(
"Path to your Sanic app. Example: path.to.server:app\n"
"If running a Simple Server, path to directory to serve. "
"Example: ./\n"
),
)
class ApplicationGroup(Group):
name = "Application"
def attach(self):
self.container.add_argument(
"--factory",
action="store_true",
help=(
"Treat app as an application factory, "
"i.e. a () -> <Sanic app> callable"
),
)
self.container.add_argument(
"-s",
"--simple",
dest="simple",
action="store_true",
help=(
"Run Sanic as a Simple Server, and serve the contents of "
"a directory\n(module arg should be a path)"
),
)
class SocketGroup(Group):
name = "Socket binding"
def attach(self):
self.container.add_argument(
"-H",
"--host",
dest="host",
type=str,
default="127.0.0.1",
help="Host address [default 127.0.0.1]",
)
self.container.add_argument(
"-p",
"--port",
dest="port",
type=int,
default=8000,
help="Port to serve on [default 8000]",
)
self.container.add_argument(
"-u",
"--unix",
dest="unix",
type=str,
default="",
help="location of unix socket",
)
class TLSGroup(Group):
name = "TLS certificate"
def attach(self):
self.container.add_argument(
"--cert",
dest="cert",
type=str,
help="Location of fullchain.pem, bundle.crt or equivalent",
)
self.container.add_argument(
"--key",
dest="key",
type=str,
help="Location of privkey.pem or equivalent .key file",
)
self.container.add_argument(
"--tls",
metavar="DIR",
type=str,
action="append",
help=(
"TLS certificate folder with fullchain.pem and privkey.pem\n"
"May be specified multiple times to choose multiple "
"certificates"
),
)
self.container.add_argument(
"--tls-strict-host",
dest="tlshost",
action="store_true",
help="Only allow clients that send an SNI matching server certs",
)
class WorkerGroup(Group):
name = "Worker"
def attach(self):
group = self.container.add_mutually_exclusive_group()
group.add_argument(
"-w",
"--workers",
dest="workers",
type=int,
default=1,
help="Number of worker processes [default 1]",
)
group.add_argument(
"--fast",
dest="fast",
action="store_true",
help="Set the number of workers to max allowed",
)
self.add_bool_arguments(
"--access-logs", dest="access_log", help="display access logs"
)
class DevelopmentGroup(Group):
name = "Development"
def attach(self):
self.container.add_argument(
"--debug",
dest="debug",
action="store_true",
help="Run the server in debug mode",
)
self.container.add_argument(
"-d",
"--dev",
dest="debug",
action="store_true",
help=(
"Currently is an alias for --debug. But starting in v22.3, \n"
"--debug will no longer automatically trigger auto_restart. \n"
"However, --dev will continue, effectively making it the \n"
"same as debug + auto_reload."
),
)
self.container.add_argument(
"-r",
"--reload",
"--auto-reload",
dest="auto_reload",
action="store_true",
help=(
"Watch source directory for file changes and reload on "
"changes"
),
)
self.container.add_argument(
"-R",
"--reload-dir",
dest="path",
action="append",
help="Extra directories to watch and reload on changes",
)
class OutputGroup(Group):
name = "Output"
def attach(self):
self.add_bool_arguments(
"--motd",
dest="motd",
default=True,
help="Show the startup display",
)
self.container.add_argument(
"-v",
"--verbosity",
action="count",
help="Control logging noise, eg. -vv or --verbosity=2 [default 0]",
)
self.add_bool_arguments(
"--noisy-exceptions",
dest="noisy_exceptions",
help="Output stack traces for all exceptions",
)

View File

@ -10,6 +10,13 @@ from multidict import CIMultiDict # type: ignore
OS_IS_WINDOWS = os.name == "nt"
def enable_windows_color_support():
import ctypes
kernel = ctypes.windll.kernel32
kernel.SetConsoleMode(kernel.GetStdHandle(-11), 7)
class Header(CIMultiDict):
"""
Container used for both request and response headers. It is a subclass of

View File

@ -1,25 +1,26 @@
from __future__ import annotations
from inspect import isclass
from os import environ
from pathlib import Path
from typing import Any, Dict, Optional, Union
from typing import TYPE_CHECKING, Any, Dict, Optional, Union
from warnings import warn
from sanic.errorpages import check_error_format
from sanic.http import Http
from sanic.utils import load_module_from_file_location, str_to_bool
from .utils import load_module_from_file_location, str_to_bool
if TYPE_CHECKING: # no cov
from sanic import Sanic
SANIC_PREFIX = "SANIC_"
BASE_LOGO = """
Sanic
Build Fast. Run Fast.
"""
DEFAULT_CONFIG = {
"ACCESS_LOG": True,
"AUTO_RELOAD": False,
"EVENT_AUTOREGISTER": False,
"FALLBACK_ERROR_FORMAT": "auto",
"FORWARDED_FOR_HEADER": "X-Forwarded-For",
@ -27,6 +28,9 @@ DEFAULT_CONFIG = {
"GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec
"KEEP_ALIVE_TIMEOUT": 5, # 5 seconds
"KEEP_ALIVE": True,
"MOTD": True,
"MOTD_DISPLAY": {},
"NOISY_EXCEPTIONS": False,
"PROXIES_COUNT": None,
"REAL_IP_HEADER": None,
"REGISTER": True,
@ -44,6 +48,7 @@ DEFAULT_CONFIG = {
class Config(dict):
ACCESS_LOG: bool
AUTO_RELOAD: bool
EVENT_AUTOREGISTER: bool
FALLBACK_ERROR_FORMAT: str
FORWARDED_FOR_HEADER: str
@ -51,6 +56,9 @@ class Config(dict):
GRACEFUL_SHUTDOWN_TIMEOUT: float
KEEP_ALIVE_TIMEOUT: int
KEEP_ALIVE: bool
NOISY_EXCEPTIONS: bool
MOTD: bool
MOTD_DISPLAY: Dict[str, str]
PROXIES_COUNT: Optional[int]
REAL_IP_HEADER: Optional[str]
REGISTER: bool
@ -71,11 +79,14 @@ class Config(dict):
load_env: Optional[Union[bool, str]] = True,
env_prefix: Optional[str] = SANIC_PREFIX,
keep_alive: Optional[bool] = None,
*,
app: Optional[Sanic] = None,
):
defaults = defaults or {}
super().__init__({**DEFAULT_CONFIG, **defaults})
self.LOGO = BASE_LOGO
self._app = app
self._LOGO = ""
if keep_alive is not None:
self.KEEP_ALIVE = keep_alive
@ -97,6 +108,7 @@ class Config(dict):
self._configure_header_size()
self._check_error_format()
self._init = True
def __getattr__(self, attr):
try:
@ -104,16 +116,51 @@ class Config(dict):
except KeyError as ke:
raise AttributeError(f"Config has no '{ke.args[0]}'")
def __setattr__(self, attr, value):
self[attr] = value
if attr in (
"REQUEST_MAX_HEADER_SIZE",
"REQUEST_BUFFER_SIZE",
"REQUEST_MAX_SIZE",
):
self._configure_header_size()
elif attr == "FALLBACK_ERROR_FORMAT":
self._check_error_format()
def __setattr__(self, attr, value) -> None:
self.update({attr: value})
def __setitem__(self, attr, value) -> None:
self.update({attr: value})
def update(self, *other, **kwargs) -> None:
other_mapping = {k: v for item in other for k, v in dict(item).items()}
super().update(*other, **kwargs)
for attr, value in {**other_mapping, **kwargs}.items():
self._post_set(attr, value)
def _post_set(self, attr, value) -> None:
if self.get("_init"):
if attr in (
"REQUEST_MAX_HEADER_SIZE",
"REQUEST_BUFFER_SIZE",
"REQUEST_MAX_SIZE",
):
self._configure_header_size()
elif attr == "FALLBACK_ERROR_FORMAT":
self._check_error_format()
if self.app and value != self.app.error_handler.fallback:
if self.app.error_handler.fallback != "auto":
warn(
"Overriding non-default ErrorHandler fallback "
"value. Changing from "
f"{self.app.error_handler.fallback} to {value}."
)
self.app.error_handler.fallback = value
elif attr == "LOGO":
self._LOGO = value
warn(
"Setting the config.LOGO is deprecated and will no longer "
"be supported starting in v22.6.",
DeprecationWarning,
)
@property
def app(self):
return self._app
@property
def LOGO(self):
return self._LOGO
def _configure_header_size(self):
Http.set_header_max_size(
@ -127,11 +174,11 @@ class Config(dict):
def load_environment_vars(self, prefix=SANIC_PREFIX):
"""
Looks for prefixed environment variables and applies
them to the configuration if present. This is called automatically when
Sanic starts up to load environment variables into config.
Looks for prefixed environment variables and applies them to the
configuration if present. This is called automatically when Sanic
starts up to load environment variables into config.
It will automatically hyrdate the following types:
It will automatically hydrate the following types:
- ``int``
- ``float``
@ -139,19 +186,18 @@ class Config(dict):
Anything else will be imported as a ``str``.
"""
for k, v in environ.items():
if k.startswith(prefix):
_, config_key = k.split(prefix, 1)
for key, value in environ.items():
if not key.startswith(prefix):
continue
_, config_key = key.split(prefix, 1)
for converter in (int, float, str_to_bool, str):
try:
self[config_key] = int(v)
self[config_key] = converter(value)
break
except ValueError:
try:
self[config_key] = float(v)
except ValueError:
try:
self[config_key] = str_to_bool(v)
except ValueError:
self[config_key] = v
pass
def update_config(self, config: Union[bytes, str, dict, Any]):
"""

View File

@ -25,12 +25,13 @@ from sanic.request import Request
from sanic.response import HTTPResponse, html, json, text
dumps: t.Callable[..., str]
try:
from ujson import dumps
dumps = partial(dumps, escape_forward_slashes=False)
except ImportError: # noqa
from json import dumps # type: ignore
from json import dumps
FALLBACK_TEXT = (
@ -45,6 +46,8 @@ class BaseRenderer:
Base class that all renderers must inherit from.
"""
dumps = staticmethod(dumps)
def __init__(self, request, exception, debug):
self.request = request
self.exception = exception
@ -112,14 +115,16 @@ class HTMLRenderer(BaseRenderer):
TRACEBACK_STYLE = """
html { font-family: sans-serif }
h2 { color: #888; }
.tb-wrapper p { margin: 0 }
.tb-wrapper p, dl, dd { margin: 0 }
.frame-border { margin: 1rem }
.frame-line > * { padding: 0.3rem 0.6rem }
.frame-line { margin-bottom: 0.3rem }
.frame-code { font-size: 16px; padding-left: 4ch }
.tb-wrapper { border: 1px solid #eee }
.tb-header { background: #eee; padding: 0.3rem; font-weight: bold }
.frame-descriptor { background: #e2eafb; font-size: 14px }
.frame-line > *, dt, dd { padding: 0.3rem 0.6rem }
.frame-line, dl { margin-bottom: 0.3rem }
.frame-code, dd { font-size: 16px; padding-left: 4ch }
.tb-wrapper, dl { border: 1px solid #eee }
.tb-header,.obj-header {
background: #eee; padding: 0.3rem; font-weight: bold
}
.frame-descriptor, dt { background: #e2eafb; font-size: 14px }
"""
TRACEBACK_WRAPPER_HTML = (
"<div class=tb-header>{exc_name}: {exc_value}</div>"
@ -138,6 +143,11 @@ class HTMLRenderer(BaseRenderer):
"<p class=frame-code><code>{0.line}</code>"
"</div>"
)
OBJECT_WRAPPER_HTML = (
"<div class=obj-header>{title}</div>"
"<dl class={obj_type}>{display_html}</dl>"
)
OBJECT_DISPLAY_HTML = "<dt>{key}</dt><dd><code>{value}</code></dd>"
OUTPUT_HTML = (
"<!DOCTYPE html><html lang=en>"
"<meta charset=UTF-8><title>{title}</title>\n"
@ -152,7 +162,7 @@ class HTMLRenderer(BaseRenderer):
title=self.title,
text=self.text,
style=self.TRACEBACK_STYLE,
body=self._generate_body(),
body=self._generate_body(full=True),
),
status=self.status,
)
@ -163,7 +173,7 @@ class HTMLRenderer(BaseRenderer):
title=self.title,
text=self.text,
style=self.TRACEBACK_STYLE,
body="",
body=self._generate_body(full=False),
),
status=self.status,
headers=self.headers,
@ -177,27 +187,49 @@ class HTMLRenderer(BaseRenderer):
def title(self):
return escape(f"⚠️ {super().title}")
def _generate_body(self):
_, exc_value, __ = sys.exc_info()
exceptions = []
while exc_value:
exceptions.append(self._format_exc(exc_value))
exc_value = exc_value.__cause__
def _generate_body(self, *, full):
lines = []
if full:
_, exc_value, __ = sys.exc_info()
exceptions = []
while exc_value:
exceptions.append(self._format_exc(exc_value))
exc_value = exc_value.__cause__
traceback_html = self.TRACEBACK_BORDER.join(reversed(exceptions))
appname = escape(self.request.app.name)
name = escape(self.exception.__class__.__name__)
value = escape(self.exception)
path = escape(self.request.path)
lines += [
f"<h2>Traceback of {appname} " "(most recent call last):</h2>",
f"{traceback_html}",
"<div class=summary><p>",
f"<b>{name}: {value}</b> "
f"while handling path <code>{path}</code>",
"</div>",
]
for attr, display in (("context", True), ("extra", bool(full))):
info = getattr(self.exception, attr, None)
if info and display:
lines.append(self._generate_object_display(info, attr))
traceback_html = self.TRACEBACK_BORDER.join(reversed(exceptions))
appname = escape(self.request.app.name)
name = escape(self.exception.__class__.__name__)
value = escape(self.exception)
path = escape(self.request.path)
lines = [
f"<h2>Traceback of {appname} (most recent call last):</h2>",
f"{traceback_html}",
"<div class=summary><p>",
f"<b>{name}: {value}</b> while handling path <code>{path}</code>",
"</div>",
]
return "\n".join(lines)
def _generate_object_display(
self, obj: t.Dict[str, t.Any], descriptor: str
) -> str:
display = "".join(
self.OBJECT_DISPLAY_HTML.format(key=key, value=value)
for key, value in obj.items()
)
return self.OBJECT_WRAPPER_HTML.format(
title=descriptor.title(),
display_html=display,
obj_type=descriptor.lower(),
)
def _format_exc(self, exc):
frames = extract_tb(exc.__traceback__)
frame_html = "".join(
@ -224,7 +256,7 @@ class TextRenderer(BaseRenderer):
title=self.title,
text=self.text,
bar=("=" * len(self.title)),
body=self._generate_body(),
body=self._generate_body(full=True),
),
status=self.status,
)
@ -235,7 +267,7 @@ class TextRenderer(BaseRenderer):
title=self.title,
text=self.text,
bar=("=" * len(self.title)),
body="",
body=self._generate_body(full=False),
),
status=self.status,
headers=self.headers,
@ -245,21 +277,31 @@ class TextRenderer(BaseRenderer):
def title(self):
return f"⚠️ {super().title}"
def _generate_body(self):
_, exc_value, __ = sys.exc_info()
exceptions = []
def _generate_body(self, *, full):
lines = []
if full:
_, exc_value, __ = sys.exc_info()
exceptions = []
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",
]
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",
]
while exc_value:
exceptions.append(self._format_exc(exc_value))
exc_value = exc_value.__cause__
while exc_value:
exceptions.append(self._format_exc(exc_value))
exc_value = exc_value.__cause__
return "\n".join(lines + exceptions[::-1])
lines += exceptions[::-1]
for attr, display in (("context", True), ("extra", bool(full))):
info = getattr(self.exception, attr, None)
if info and display:
lines += self._generate_object_display_list(info, attr)
return "\n".join(lines)
def _format_exc(self, exc):
frames = "\n\n".join(
@ -272,6 +314,13 @@ class TextRenderer(BaseRenderer):
)
return f"{self.SPACER}{exc.__class__.__name__}: {exc}\n{frames}"
def _generate_object_display_list(self, obj, descriptor):
lines = [f"\n{descriptor.title()}"]
for key, value in obj.items():
display = self.dumps(value)
lines.append(f"{self.SPACER * 2}{key}: {display}")
return lines
class JSONRenderer(BaseRenderer):
"""
@ -280,11 +329,11 @@ class JSONRenderer(BaseRenderer):
def full(self) -> HTTPResponse:
output = self._generate_output(full=True)
return json(output, status=self.status, dumps=dumps)
return json(output, status=self.status, dumps=self.dumps)
def minimal(self) -> HTTPResponse:
output = self._generate_output(full=False)
return json(output, status=self.status, dumps=dumps)
return json(output, status=self.status, dumps=self.dumps)
def _generate_output(self, *, full):
output = {
@ -293,6 +342,11 @@ class JSONRenderer(BaseRenderer):
"message": self.text,
}
for attr, display in (("context", True), ("extra", bool(full))):
info = getattr(self.exception, attr, None)
if info and display:
output[attr] = info
if full:
_, exc_value, __ = sys.exc_info()
exceptions = []
@ -393,7 +447,8 @@ def exception_response(
# from the route
if request.route:
try:
render_format = request.route.ctx.error_format
if request.route.ctx.error_format:
render_format = request.route.ctx.error_format
except AttributeError:
...

View File

@ -1,4 +1,4 @@
from typing import Optional, Union
from typing import Any, Dict, Optional, Union
from sanic.helpers import STATUS_CODES
@ -11,7 +11,11 @@ class SanicException(Exception):
message: Optional[Union[str, bytes]] = None,
status_code: Optional[int] = None,
quiet: Optional[bool] = None,
context: Optional[Dict[str, Any]] = None,
extra: Optional[Dict[str, Any]] = None,
) -> None:
self.context = context
self.extra = extra
if message is None:
if self.message:
message = self.message

View File

@ -1,5 +1,6 @@
from inspect import signature
from typing import Dict, List, Optional, Tuple, Type
from warnings import warn
from sanic.errorpages import BaseRenderer, HTMLRenderer, exception_response
from sanic.exceptions import (
@ -38,7 +39,14 @@ class ErrorHandler:
self.base = base
@classmethod
def finalize(cls, error_handler):
def finalize(cls, error_handler, fallback: Optional[str] = None):
if (
fallback
and fallback != "auto"
and error_handler.fallback == "auto"
):
error_handler.fallback = fallback
if not isinstance(error_handler, cls):
error_logger.warning(
f"Error handler is non-conforming: {type(error_handler)}"
@ -46,16 +54,15 @@ class ErrorHandler:
sig = signature(error_handler.lookup)
if len(sig.parameters) == 1:
error_logger.warning(
DeprecationWarning(
"You are using a deprecated error handler. The lookup "
"method should accept two positional parameters: "
"(exception, route_name: Optional[str]). "
"Until you upgrade your ErrorHandler.lookup, Blueprint "
"specific exceptions will not work properly. Beginning "
"in v22.3, the legacy style lookup method will not "
"work at all."
),
warn(
"You are using a deprecated error handler. The lookup "
"method should accept two positional parameters: "
"(exception, route_name: Optional[str]). "
"Until you upgrade your ErrorHandler.lookup, Blueprint "
"specific exceptions will not work properly. Beginning "
"in v22.3, the legacy style lookup method will not "
"work at all.",
DeprecationWarning,
)
error_handler._lookup = error_handler._legacy_lookup
@ -192,7 +199,8 @@ class ErrorHandler:
@staticmethod
def log(request, exception):
quiet = getattr(exception, "quiet", False)
if quiet is False:
noisy = getattr(request.app.config, "NOISY_EXCEPTIONS", False)
if quiet is False or noisy is True:
try:
url = repr(request.url)
except AttributeError:

View File

@ -28,7 +28,7 @@ _host_re = re.compile(
# 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,
# even though no client espaces in a way that would allow perfect handling.
# even though no client escapes in a way that would allow perfect handling.
# For more information, consult ../tests/test_requests.py

View File

@ -144,7 +144,7 @@ def import_string(module_name, package=None):
import a module or class by string path.
:module_name: str with path of module or path to import and
instanciate a class
instantiate a class
:returns: a module object or one instance from class if
module_name is a valid path to class

View File

@ -105,7 +105,6 @@ class Http(metaclass=TouchUpMeta):
self.keep_alive = True
self.stage: Stage = Stage.IDLE
self.dispatch = self.protocol.app.dispatch
self.init_for_request()
def init_for_request(self):
"""Init/reset all per-request variables."""
@ -129,14 +128,20 @@ class Http(metaclass=TouchUpMeta):
"""
HTTP 1.1 connection handler
"""
while True: # As long as connection stays keep-alive
# Handle requests while the connection stays reusable
while self.keep_alive and self.stage is Stage.IDLE:
self.init_for_request()
# Wait for incoming bytes (in IDLE stage)
if not self.recv_buffer:
await self._receive_more()
self.stage = Stage.REQUEST
try:
# Receive and handle a request
self.stage = Stage.REQUEST
self.response_func = self.http1_response_header
await self.http1_request_header()
self.stage = Stage.HANDLER
self.request.conn_info = self.protocol.conn_info
await self.protocol.request_handler(self.request)
@ -187,16 +192,6 @@ class Http(metaclass=TouchUpMeta):
if self.response:
self.response.stream = None
# Exit and disconnect if no more requests can be taken
if self.stage is not Stage.IDLE or not self.keep_alive:
break
self.init_for_request()
# Wait for the next request
if not self.recv_buffer:
await self._receive_more()
async def http1_request_header(self): # no cov
"""
Receive and parse request header into self.request.
@ -299,7 +294,6 @@ class Http(metaclass=TouchUpMeta):
# Remove header and its trailing CRLF
del buf[: pos + 4]
self.stage = Stage.HANDLER
self.request, request.stream = request, self
self.protocol.state["requests_count"] += 1
@ -590,6 +584,11 @@ class Http(metaclass=TouchUpMeta):
self.stage = Stage.FAILED
raise RuntimeError("Response already started")
# Disconnect any earlier but unused response object
if self.response is not None:
self.response.stream = None
# Connect and return the response
self.response, response.stream = response, self
return response

View File

@ -1,8 +1,11 @@
import logging
import sys
from enum import Enum
from typing import Any, Dict
LOGGING_CONFIG_DEFAULTS = dict(
LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict(
version=1,
disable_existing_loggers=False,
loggers={
@ -53,6 +56,14 @@ LOGGING_CONFIG_DEFAULTS = dict(
)
class Colors(str, Enum):
END = "\033[0m"
BLUE = "\033[01;34m"
GREEN = "\033[01;32m"
YELLOW = "\033[01;33m"
RED = "\033[01;31m"
logger = logging.getLogger("sanic.root")
"""
General Sanic logger

View File

@ -3,7 +3,7 @@ from functools import partial
from typing import List, Optional, Union
from sanic.models.futures import FutureListener
from sanic.models.handler_types import ListenerType
from sanic.models.handler_types import ListenerType, Sanic
class ListenerEvent(str, Enum):
@ -27,10 +27,10 @@ class ListenerMixin:
def listener(
self,
listener_or_event: Union[ListenerType, str],
listener_or_event: Union[ListenerType[Sanic], str],
event_or_none: Optional[str] = None,
apply: bool = True,
):
) -> ListenerType[Sanic]:
"""
Create a listener from a decorated function.
@ -62,20 +62,32 @@ class ListenerMixin:
else:
return partial(register_listener, event=listener_or_event)
def main_process_start(self, listener: ListenerType) -> ListenerType:
def main_process_start(
self, listener: ListenerType[Sanic]
) -> ListenerType[Sanic]:
return self.listener(listener, "main_process_start")
def main_process_stop(self, listener: ListenerType) -> ListenerType:
def main_process_stop(
self, listener: ListenerType[Sanic]
) -> ListenerType[Sanic]:
return self.listener(listener, "main_process_stop")
def before_server_start(self, listener: ListenerType) -> ListenerType:
def before_server_start(
self, listener: ListenerType[Sanic]
) -> ListenerType[Sanic]:
return self.listener(listener, "before_server_start")
def after_server_start(self, listener: ListenerType) -> ListenerType:
def after_server_start(
self, listener: ListenerType[Sanic]
) -> ListenerType[Sanic]:
return self.listener(listener, "after_server_start")
def before_server_stop(self, listener: ListenerType) -> ListenerType:
def before_server_stop(
self, listener: ListenerType[Sanic]
) -> ListenerType[Sanic]:
return self.listener(listener, "before_server_stop")
def after_server_stop(self, listener: ListenerType) -> ListenerType:
def after_server_stop(
self, listener: ListenerType[Sanic]
) -> ListenerType[Sanic]:
return self.listener(listener, "after_server_stop")

View File

@ -52,7 +52,7 @@ class RouteMixin:
self,
uri: str,
methods: Optional[Iterable[str]] = None,
host: Optional[str] = None,
host: Optional[Union[str, List[str]]] = None,
strict_slashes: Optional[bool] = None,
stream: bool = False,
version: Optional[Union[int, str, float]] = None,
@ -189,9 +189,9 @@ class RouteMixin:
handler: RouteHandler,
uri: str,
methods: Iterable[str] = frozenset({"GET"}),
host: Optional[str] = None,
host: Optional[Union[str, List[str]]] = None,
strict_slashes: Optional[bool] = None,
version: Optional[int] = None,
version: Optional[Union[int, str, float]] = None,
name: Optional[str] = None,
stream: bool = False,
version_prefix: str = "/v",
@ -254,9 +254,9 @@ class RouteMixin:
def get(
self,
uri: str,
host: Optional[str] = None,
host: Optional[Union[str, List[str]]] = None,
strict_slashes: Optional[bool] = None,
version: Optional[int] = None,
version: Optional[Union[int, str, float]] = None,
name: Optional[str] = None,
ignore_body: bool = True,
version_prefix: str = "/v",
@ -290,10 +290,10 @@ class RouteMixin:
def post(
self,
uri: str,
host: Optional[str] = None,
host: Optional[Union[str, List[str]]] = None,
strict_slashes: Optional[bool] = None,
stream: bool = False,
version: Optional[int] = None,
version: Optional[Union[int, str, float]] = None,
name: Optional[str] = None,
version_prefix: str = "/v",
error_format: Optional[str] = None,
@ -326,10 +326,10 @@ class RouteMixin:
def put(
self,
uri: str,
host: Optional[str] = None,
host: Optional[Union[str, List[str]]] = None,
strict_slashes: Optional[bool] = None,
stream: bool = False,
version: Optional[int] = None,
version: Optional[Union[int, str, float]] = None,
name: Optional[str] = None,
version_prefix: str = "/v",
error_format: Optional[str] = None,
@ -362,9 +362,9 @@ class RouteMixin:
def head(
self,
uri: str,
host: Optional[str] = None,
host: Optional[Union[str, List[str]]] = None,
strict_slashes: Optional[bool] = None,
version: Optional[int] = None,
version: Optional[Union[int, str, float]] = None,
name: Optional[str] = None,
ignore_body: bool = True,
version_prefix: str = "/v",
@ -406,9 +406,9 @@ class RouteMixin:
def options(
self,
uri: str,
host: Optional[str] = None,
host: Optional[Union[str, List[str]]] = None,
strict_slashes: Optional[bool] = None,
version: Optional[int] = None,
version: Optional[Union[int, str, float]] = None,
name: Optional[str] = None,
ignore_body: bool = True,
version_prefix: str = "/v",
@ -450,10 +450,10 @@ class RouteMixin:
def patch(
self,
uri: str,
host: Optional[str] = None,
host: Optional[Union[str, List[str]]] = None,
strict_slashes: Optional[bool] = None,
stream=False,
version: Optional[int] = None,
version: Optional[Union[int, str, float]] = None,
name: Optional[str] = None,
version_prefix: str = "/v",
error_format: Optional[str] = None,
@ -496,9 +496,9 @@ class RouteMixin:
def delete(
self,
uri: str,
host: Optional[str] = None,
host: Optional[Union[str, List[str]]] = None,
strict_slashes: Optional[bool] = None,
version: Optional[int] = None,
version: Optional[Union[int, str, float]] = None,
name: Optional[str] = None,
ignore_body: bool = True,
version_prefix: str = "/v",
@ -532,10 +532,10 @@ class RouteMixin:
def websocket(
self,
uri: str,
host: Optional[str] = None,
host: Optional[Union[str, List[str]]] = None,
strict_slashes: Optional[bool] = None,
subprotocols: Optional[List[str]] = None,
version: Optional[int] = None,
version: Optional[Union[int, str, float]] = None,
name: Optional[str] = None,
apply: bool = True,
version_prefix: str = "/v",
@ -573,10 +573,10 @@ class RouteMixin:
self,
handler,
uri: str,
host: Optional[str] = None,
host: Optional[Union[str, List[str]]] = None,
strict_slashes: Optional[bool] = None,
subprotocols=None,
version: Optional[int] = None,
version: Optional[Union[int, str, float]] = None,
name: Optional[str] = None,
version_prefix: str = "/v",
error_format: Optional[str] = None,
@ -918,7 +918,7 @@ class RouteMixin:
return route
def _determine_error_format(self, handler) -> str:
def _determine_error_format(self, handler) -> Optional[str]:
if not isinstance(handler, CompositionView):
try:
src = dedent(getsource(handler))
@ -930,7 +930,7 @@ class RouteMixin:
except (OSError, TypeError):
...
return "auto"
return None
def _get_response_types(self, node):
types = set()

View File

@ -1,4 +1,5 @@
from typing import Any, Callable, Dict, Optional, Set
from enum import Enum
from typing import Any, Callable, Dict, Optional, Set, Union
from sanic.models.futures import FutureSignal
from sanic.models.handler_types import SignalHandler
@ -19,7 +20,7 @@ class SignalMixin:
def signal(
self,
event: str,
event: Union[str, Enum],
*,
apply: bool = True,
condition: Dict[str, Any] = None,
@ -41,13 +42,11 @@ class SignalMixin:
filtering, defaults to None
:type condition: Dict[str, Any], optional
"""
event_value = str(event.value) if isinstance(event, Enum) else event
def decorator(handler: SignalHandler):
nonlocal event
nonlocal apply
future_signal = FutureSignal(
handler, event, HashableDict(condition or {})
handler, event_value, HashableDict(condition or {})
)
self._future_signals.add(future_signal)

View File

@ -1,4 +1,5 @@
import asyncio
import sys
from typing import Any, Awaitable, Callable, MutableMapping, Optional, Union
@ -14,10 +15,20 @@ ASGIReceive = Callable[[], Awaitable[ASGIMessage]]
class MockProtocol:
def __init__(self, transport: "MockTransport", loop):
# This should be refactored when < 3.8 support is dropped
self.transport = transport
self._not_paused = asyncio.Event(loop=loop)
self._not_paused.set()
self._complete = asyncio.Event(loop=loop)
# Fixup for 3.8+; Sanic still supports 3.7 where loop is required
loop = loop if sys.version_info[:2] < (3, 8) else None
# Optional in 3.9, necessary in 3.10 because the parameter "loop"
# was completely removed
if not loop:
self._not_paused = asyncio.Event()
self._not_paused.set()
self._complete = asyncio.Event()
else:
self._not_paused = asyncio.Event(loop=loop)
self._not_paused.set()
self._complete = asyncio.Event(loop=loop)
def pause_writing(self) -> None:
self._not_paused.clear()

View File

@ -13,7 +13,7 @@ class FutureRoute(NamedTuple):
handler: str
uri: str
methods: Optional[Iterable[str]]
host: str
host: Union[str, List[str]]
strict_slashes: bool
stream: bool
version: Optional[int]
@ -60,3 +60,7 @@ class FutureSignal(NamedTuple):
handler: SignalHandler
event: str
condition: Optional[Dict[str, str]]
class FutureRegistry(set):
...

View File

@ -1,4 +1,6 @@
from ssl import SSLObject
from types import SimpleNamespace
from typing import Any, Dict, Optional
from sanic.models.protocol_types import TransportProtocol
@ -20,8 +22,10 @@ class ConnInfo:
"peername",
"server_port",
"server",
"server_name",
"sockname",
"ssl",
"cert",
)
def __init__(self, transport: TransportProtocol, unix=None):
@ -31,8 +35,16 @@ class ConnInfo:
self.server_port = self.client_port = 0
self.client_ip = ""
self.sockname = addr = transport.get_extra_info("sockname")
self.ssl: bool = bool(transport.get_extra_info("sslcontext"))
self.ssl = False
self.server_name = ""
self.cert: Dict[str, Any] = {}
sslobj: Optional[SSLObject] = transport.get_extra_info(
"ssl_object"
) # type: ignore
if sslobj:
self.ssl = True
self.server_name = getattr(sslobj, "sanic_server_name", None) or ""
self.cert = dict(getattr(sslobj.context, "sanic", {}))
if isinstance(addr, str): # UNIX socket
self.server = unix or addr
return

View File

@ -6,9 +6,6 @@ import sys
from time import sleep
from sanic.config import BASE_LOGO
from sanic.log import logger
def _iter_module_files():
"""This iterates over all relevant Python files.
@ -50,13 +47,19 @@ def _get_args_for_reloading():
return [sys.executable] + sys.argv
def restart_with_reloader():
def restart_with_reloader(changed=None):
"""Create a new process and a subprocess in it with the same arguments as
this one.
"""
reloaded = ",".join(changed) if changed else ""
return subprocess.Popen(
_get_args_for_reloading(),
env={**os.environ, "SANIC_SERVER_RUNNING": "true"},
env={
**os.environ,
"SANIC_SERVER_RUNNING": "true",
"SANIC_RELOADER_PROCESS": "true",
"SANIC_RELOADED_FILES": reloaded,
},
)
@ -91,31 +94,29 @@ def watchdog(sleep_interval, app):
worker_process = restart_with_reloader()
if app.config.LOGO:
logger.debug(
app.config.LOGO if isinstance(app.config.LOGO, str) else BASE_LOGO
)
try:
while True:
need_reload = False
changed = set()
for filename in itertools.chain(
_iter_module_files(),
*(d.glob("**/*") for d in app.reload_dirs),
):
try:
check = _check_file(filename, mtimes)
if _check_file(filename, mtimes):
path = (
filename
if isinstance(filename, str)
else filename.resolve()
)
changed.add(str(path))
except OSError:
continue
if check:
need_reload = True
if need_reload:
if changed:
worker_process.terminate()
worker_process.wait()
worker_process = restart_with_reloader()
worker_process = restart_with_reloader(changed)
sleep(sleep_interval)
except KeyboardInterrupt:

View File

@ -18,7 +18,6 @@ from sanic_routing.route import Route # type: ignore
if TYPE_CHECKING:
from sanic.server import ConnInfo
from sanic.app import Sanic
from sanic.http import Http
import email.utils
import uuid
@ -32,7 +31,7 @@ from httptools import parse_url # type: ignore
from sanic.compat import CancelledErrors, Header
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE
from sanic.exceptions import InvalidUsage
from sanic.exceptions import InvalidUsage, ServerError
from sanic.headers import (
AcceptContainer,
Options,
@ -42,6 +41,7 @@ from sanic.headers import (
parse_host,
parse_xforwarded,
)
from sanic.http import Http, Stage
from sanic.log import error_logger, logger
from sanic.models.protocol_types import TransportProtocol
from sanic.response import BaseHTTPResponse, HTTPResponse
@ -104,6 +104,7 @@ class Request:
"parsed_json",
"parsed_forwarded",
"raw_url",
"responded",
"request_middleware_started",
"route",
"stream",
@ -155,6 +156,7 @@ class Request:
self.stream: Optional[Http] = None
self.route: Optional[Route] = None
self._protocol = None
self.responded: bool = False
def __repr__(self):
class_name = self.__class__.__name__
@ -164,6 +166,21 @@ class Request:
def generate_id(*_):
return uuid.uuid4()
def reset_response(self):
try:
if (
self.stream is not None
and self.stream.stage is not Stage.HANDLER
):
raise ServerError(
"Cannot reset response because previous response was sent."
)
self.stream.response.stream = None
self.stream.response = None
self.responded = False
except AttributeError:
pass
async def respond(
self,
response: Optional[BaseHTTPResponse] = None,
@ -172,13 +189,19 @@ class Request:
headers: Optional[Union[Header, Dict[str, str]]] = None,
content_type: Optional[str] = None,
):
try:
if self.stream is not None and self.stream.response:
raise ServerError("Second respond call is not allowed.")
except AttributeError:
pass
# This logic of determining which response to use is subject to change
if response is None:
response = (self.stream and self.stream.response) or HTTPResponse(
response = HTTPResponse(
status=status,
headers=headers,
content_type=content_type,
)
# Connect the response
if isinstance(response, BaseHTTPResponse) and self.stream:
response = self.stream.respond(response)
@ -193,6 +216,7 @@ class Request:
error_logger.exception(
"Exception occurred in one of response middleware handlers"
)
self.responded = True
return response
async def receive_body(self):
@ -760,9 +784,10 @@ def parse_multipart_form(body, boundary):
break
colon_index = form_line.index(":")
idx = colon_index + 2
form_header_field = form_line[0:colon_index].lower()
form_header_value, form_parameters = parse_content_header(
form_line[colon_index + 2 :]
form_line[idx:]
)
if form_header_field == "content-disposition":

View File

@ -3,6 +3,7 @@ from mimetypes import guess_type
from os import path
from pathlib import PurePath
from typing import (
TYPE_CHECKING,
Any,
AnyStr,
Callable,
@ -19,11 +20,15 @@ from warnings import warn
from sanic.compat import Header, open_async
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE
from sanic.cookies import CookieJar
from sanic.exceptions import SanicException, ServerError
from sanic.helpers import has_message_body, remove_entity_headers
from sanic.http import Http
from sanic.models.protocol_types import HTMLProtocol, Range
if TYPE_CHECKING:
from sanic.asgi import ASGIApp
try:
from ujson import dumps as json_dumps
except ImportError:
@ -45,7 +50,7 @@ class BaseHTTPResponse:
self.asgi: bool = False
self.body: Optional[bytes] = None
self.content_type: Optional[str] = None
self.stream: Http = None
self.stream: Optional[Union[Http, ASGIApp]] = None
self.status: int = None
self.headers = Header({})
self._cookies: Optional[CookieJar] = None
@ -101,7 +106,7 @@ class BaseHTTPResponse:
async def send(
self,
data: Optional[Union[AnyStr]] = None,
data: Optional[AnyStr] = None,
end_stream: Optional[bool] = None,
) -> None:
"""
@ -112,8 +117,17 @@ class BaseHTTPResponse:
"""
if data is None and end_stream is None:
end_stream = True
if end_stream and not data and self.stream.send is None:
return
if self.stream is None:
raise SanicException(
"No stream is connected to the response object instance."
)
if self.stream.send is None:
if end_stream and not data:
return
raise ServerError(
"Response stream was ended, no more response data is "
"allowed to be sent."
)
data = (
data.encode() # type: ignore
if hasattr(data, "encode")

View File

@ -54,7 +54,7 @@ class Router(BaseRouter):
self, path: str, method: str, host: Optional[str]
) -> Tuple[Route, RouteHandler, Dict[str, Any]]:
"""
Retrieve a `Route` object containg the details about how to handle
Retrieve a `Route` object containing the details about how to handle
a response for a given request
:param request: the incoming request object
@ -139,11 +139,10 @@ class Router(BaseRouter):
route.ctx.stream = stream
route.ctx.hosts = hosts
route.ctx.static = static
route.ctx.error_format = (
error_format or self.ctx.app.config.FALLBACK_ERROR_FORMAT
)
route.ctx.error_format = error_format
check_error_format(route.ctx.error_format)
if error_format:
check_error_format(route.ctx.error_format)
routes.append(route)

View File

@ -2,20 +2,27 @@ from __future__ import annotations
import asyncio
from typing import TYPE_CHECKING
from warnings import warn
from sanic.exceptions import SanicException
if TYPE_CHECKING:
from sanic import Sanic
class AsyncioServer:
"""
Wraps an asyncio server with functionality that might be useful to
a user who needs to manage the server lifecycle manually.
"""
__slots__ = ("app", "connections", "loop", "serve_coro", "server", "init")
__slots__ = ("app", "connections", "loop", "serve_coro", "server")
def __init__(
self,
app,
app: Sanic,
loop,
serve_coro,
connections,
@ -27,13 +34,20 @@ class AsyncioServer:
self.loop = loop
self.serve_coro = serve_coro
self.server = None
self.init = False
@property
def init(self):
warn(
"AsyncioServer.init has been deprecated and will be removed "
"in v22.6. Use Sanic.state.is_started instead.",
DeprecationWarning,
)
return self.app.state.is_started
def startup(self):
"""
Trigger "before_server_start" events
"""
self.init = True
return self.app._startup()
def before_start(self):
@ -77,30 +91,33 @@ class AsyncioServer:
return task
def start_serving(self):
if self.server:
try:
return self.server.start_serving()
except AttributeError:
raise NotImplementedError(
"server.start_serving not available in this version "
"of asyncio or uvloop."
)
return self._serve(self.server.start_serving)
def serve_forever(self):
return self._serve(self.server.serve_forever)
def _serve(self, serve_func):
if self.server:
if not self.app.state.is_started:
raise SanicException(
"Cannot run Sanic server without first running "
"await server.startup()"
)
try:
return self.server.serve_forever()
return serve_func()
except AttributeError:
name = serve_func.__name__
raise NotImplementedError(
"server.serve_forever not available in this version "
f"server.{name} not available in this version "
"of asyncio or uvloop."
)
def _server_event(self, concern: str, action: str):
if not self.init:
if not self.app.state.is_started:
raise SanicException(
"Cannot dispatch server event without "
"first running server.startup()"
"first running await server.startup()"
)
return self.app._server_event(concern, action, loop=self.loop)

View File

@ -1,7 +1,9 @@
from typing import TYPE_CHECKING, Optional, Sequence
from typing import TYPE_CHECKING, Optional, Sequence, cast
from warnings import warn
from websockets.connection import CLOSED, CLOSING, OPEN
from websockets.server import ServerConnection
from websockets.typing import Subprotocol
from sanic.exceptions import ServerError
from sanic.log import error_logger
@ -15,13 +17,6 @@ if TYPE_CHECKING:
class WebSocketProtocol(HttpProtocol):
websocket: Optional[WebsocketImplProtocol]
websocket_timeout: float
websocket_max_size = Optional[int]
websocket_ping_interval = Optional[float]
websocket_ping_timeout = Optional[float]
def __init__(
self,
*args,
@ -35,32 +30,29 @@ class WebSocketProtocol(HttpProtocol):
**kwargs,
):
super().__init__(*args, **kwargs)
self.websocket = None
self.websocket: Optional[WebsocketImplProtocol] = None
self.websocket_timeout = websocket_timeout
self.websocket_max_size = websocket_max_size
if websocket_max_queue is not None and websocket_max_queue > 0:
# TODO: Reminder remove this warning in v22.3
error_logger.warning(
DeprecationWarning(
"Websocket no longer uses queueing, so websocket_max_queue"
" is no longer required."
)
warn(
"Websocket no longer uses queueing, so websocket_max_queue"
" is no longer required.",
DeprecationWarning,
)
if websocket_read_limit is not None and websocket_read_limit > 0:
# TODO: Reminder remove this warning in v22.3
error_logger.warning(
DeprecationWarning(
"Websocket no longer uses read buffers, so "
"websocket_read_limit is not required."
)
warn(
"Websocket no longer uses read buffers, so "
"websocket_read_limit is not required.",
DeprecationWarning,
)
if websocket_write_limit is not None and websocket_write_limit > 0:
# TODO: Reminder remove this warning in v22.3
error_logger.warning(
DeprecationWarning(
"Websocket no longer uses write buffers, so "
"websocket_write_limit is not required."
)
warn(
"Websocket no longer uses write buffers, so "
"websocket_write_limit is not required.",
DeprecationWarning,
)
self.websocket_ping_interval = websocket_ping_interval
self.websocket_ping_timeout = websocket_ping_timeout
@ -109,14 +101,22 @@ class WebSocketProtocol(HttpProtocol):
return super().close_if_idle()
async def websocket_handshake(
self, request, subprotocols=Optional[Sequence[str]]
self, request, subprotocols: Optional[Sequence[str]] = None
):
# let the websockets package do the handshake with the client
try:
if subprotocols is not None:
# subprotocols can be a set or frozenset,
# but ServerConnection needs a list
subprotocols = list(subprotocols)
subprotocols = cast(
Optional[Sequence[Subprotocol]],
list(
[
Subprotocol(subprotocol)
for subprotocol in subprotocols
]
),
)
ws_conn = ServerConnection(
max_size=self.websocket_max_size,
subprotocols=subprotocols,
@ -131,21 +131,18 @@ class WebSocketProtocol(HttpProtocol):
)
raise ServerError(msg, status_code=500)
if 100 <= resp.status_code <= 299:
rbody = "".join(
[
"HTTP/1.1 ",
str(resp.status_code),
" ",
resp.reason_phrase,
"\r\n",
]
)
rbody += "".join(f"{k}: {v}\r\n" for k, v in resp.headers.items())
first_line = (
f"HTTP/1.1 {resp.status_code} {resp.reason_phrase}\r\n"
).encode()
rbody = bytearray(first_line)
rbody += (
"".join([f"{k}: {v}\r\n" for k, v in resp.headers.items()])
).encode()
rbody += b"\r\n"
if resp.body is not None:
rbody += f"\r\n{resp.body}\r\n\r\n"
else:
rbody += "\r\n"
await super().send(rbody.encode())
rbody += resp.body
rbody += b"\r\n\r\n"
await super().send(rbody)
else:
raise ServerError(resp.body, resp.status_code)
self.websocket = WebsocketImplProtocol(

View File

@ -134,6 +134,7 @@ def serve(
# Ignore SIGINT when run_multiple
if run_multiple:
signal_func(SIGINT, SIG_IGN)
os.environ["SANIC_WORKER_PROCESS"] = "true"
# Register signals for graceful termination
if register_sys_signals:
@ -181,7 +182,6 @@ def serve(
else:
conn.abort()
loop.run_until_complete(app._server_event("shutdown", "after"))
remove_unix_socket(unix)
@ -249,7 +249,10 @@ def serve_multiple(server_settings, workers):
mp = multiprocessing.get_context("fork")
for _ in range(workers):
process = mp.Process(target=serve, kwargs=server_settings)
process = mp.Process(
target=serve,
kwargs=server_settings,
)
process.daemon = True
process.start()
processes.append(process)

View File

@ -2,6 +2,7 @@ from __future__ import annotations
import asyncio
from enum import Enum
from inspect import isawaitable
from typing import Any, Dict, List, Optional, Tuple, Union
@ -14,29 +15,47 @@ from sanic.log import error_logger, logger
from sanic.models.handler_types import SignalHandler
class Event(Enum):
SERVER_INIT_AFTER = "server.init.after"
SERVER_INIT_BEFORE = "server.init.before"
SERVER_SHUTDOWN_AFTER = "server.shutdown.after"
SERVER_SHUTDOWN_BEFORE = "server.shutdown.before"
HTTP_LIFECYCLE_BEGIN = "http.lifecycle.begin"
HTTP_LIFECYCLE_COMPLETE = "http.lifecycle.complete"
HTTP_LIFECYCLE_EXCEPTION = "http.lifecycle.exception"
HTTP_LIFECYCLE_HANDLE = "http.lifecycle.handle"
HTTP_LIFECYCLE_READ_BODY = "http.lifecycle.read_body"
HTTP_LIFECYCLE_READ_HEAD = "http.lifecycle.read_head"
HTTP_LIFECYCLE_REQUEST = "http.lifecycle.request"
HTTP_LIFECYCLE_RESPONSE = "http.lifecycle.response"
HTTP_ROUTING_AFTER = "http.routing.after"
HTTP_ROUTING_BEFORE = "http.routing.before"
HTTP_LIFECYCLE_SEND = "http.lifecycle.send"
HTTP_MIDDLEWARE_AFTER = "http.middleware.after"
HTTP_MIDDLEWARE_BEFORE = "http.middleware.before"
RESERVED_NAMESPACES = {
"server": (
# "server.main.start",
# "server.main.stop",
"server.init.before",
"server.init.after",
"server.shutdown.before",
"server.shutdown.after",
Event.SERVER_INIT_AFTER.value,
Event.SERVER_INIT_BEFORE.value,
Event.SERVER_SHUTDOWN_AFTER.value,
Event.SERVER_SHUTDOWN_BEFORE.value,
),
"http": (
"http.lifecycle.begin",
"http.lifecycle.complete",
"http.lifecycle.exception",
"http.lifecycle.handle",
"http.lifecycle.read_body",
"http.lifecycle.read_head",
"http.lifecycle.request",
"http.lifecycle.response",
"http.routing.after",
"http.routing.before",
"http.lifecycle.send",
"http.middleware.after",
"http.middleware.before",
Event.HTTP_LIFECYCLE_BEGIN.value,
Event.HTTP_LIFECYCLE_COMPLETE.value,
Event.HTTP_LIFECYCLE_EXCEPTION.value,
Event.HTTP_LIFECYCLE_HANDLE.value,
Event.HTTP_LIFECYCLE_READ_BODY.value,
Event.HTTP_LIFECYCLE_READ_HEAD.value,
Event.HTTP_LIFECYCLE_REQUEST.value,
Event.HTTP_LIFECYCLE_RESPONSE.value,
Event.HTTP_ROUTING_AFTER.value,
Event.HTTP_ROUTING_BEFORE.value,
Event.HTTP_LIFECYCLE_SEND.value,
Event.HTTP_MIDDLEWARE_AFTER.value,
Event.HTTP_MIDDLEWARE_BEFORE.value,
),
}
@ -113,7 +132,7 @@ class SignalRouter(BaseRouter):
if fail_not_found:
raise e
else:
if self.ctx.app.debug:
if self.ctx.app.debug and self.ctx.app.state.verbosity >= 1:
error_logger.warning(str(e))
return None

196
sanic/tls.py Normal file
View File

@ -0,0 +1,196 @@
import os
import ssl
from typing import Iterable, Optional, Union
from sanic.log import logger
# Only allow secure ciphers, notably leaving out AES-CBC mode
# OpenSSL chooses ECDSA or RSA depending on the cert in use
CIPHERS_TLS12 = [
"ECDHE-ECDSA-CHACHA20-POLY1305",
"ECDHE-ECDSA-AES256-GCM-SHA384",
"ECDHE-ECDSA-AES128-GCM-SHA256",
"ECDHE-RSA-CHACHA20-POLY1305",
"ECDHE-RSA-AES256-GCM-SHA384",
"ECDHE-RSA-AES128-GCM-SHA256",
]
def create_context(
certfile: Optional[str] = None,
keyfile: Optional[str] = None,
password: Optional[str] = None,
) -> ssl.SSLContext:
"""Create a context with secure crypto and HTTP/1.1 in protocols."""
context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
context.minimum_version = ssl.TLSVersion.TLSv1_2
context.set_ciphers(":".join(CIPHERS_TLS12))
context.set_alpn_protocols(["http/1.1"])
context.sni_callback = server_name_callback
if certfile and keyfile:
context.load_cert_chain(certfile, keyfile, password)
return context
def shorthand_to_ctx(
ctxdef: Union[None, ssl.SSLContext, dict, str]
) -> Optional[ssl.SSLContext]:
"""Convert an ssl argument shorthand to an SSLContext object."""
if ctxdef is None or isinstance(ctxdef, ssl.SSLContext):
return ctxdef
if isinstance(ctxdef, str):
return load_cert_dir(ctxdef)
if isinstance(ctxdef, dict):
return CertSimple(**ctxdef)
raise ValueError(
f"Invalid ssl argument {type(ctxdef)}."
" Expecting a list of certdirs, a dict or an SSLContext."
)
def process_to_context(
ssldef: Union[None, ssl.SSLContext, dict, str, list, tuple]
) -> Optional[ssl.SSLContext]:
"""Process app.run ssl argument from easy formats to full SSLContext."""
return (
CertSelector(map(shorthand_to_ctx, ssldef))
if isinstance(ssldef, (list, tuple))
else shorthand_to_ctx(ssldef)
)
def load_cert_dir(p: str) -> ssl.SSLContext:
if os.path.isfile(p):
raise ValueError(f"Certificate folder expected but {p} is a file.")
keyfile = os.path.join(p, "privkey.pem")
certfile = os.path.join(p, "fullchain.pem")
if not os.access(keyfile, os.R_OK):
raise ValueError(
f"Certificate not found or permission denied {keyfile}"
)
if not os.access(certfile, os.R_OK):
raise ValueError(
f"Certificate not found or permission denied {certfile}"
)
return CertSimple(certfile, keyfile)
class CertSimple(ssl.SSLContext):
"""A wrapper for creating SSLContext with a sanic attribute."""
def __new__(cls, cert, key, **kw):
# try common aliases, rename to cert/key
certfile = kw["cert"] = kw.pop("certificate", None) or cert
keyfile = kw["key"] = kw.pop("keyfile", None) or key
password = kw.pop("password", None)
if not certfile or not keyfile:
raise ValueError("SSL dict needs filenames for cert and key.")
subject = {}
if "names" not in kw:
cert = ssl._ssl._test_decode_cert(certfile) # type: ignore
kw["names"] = [
name
for t, name in cert["subjectAltName"]
if t in ["DNS", "IP Address"]
]
subject = {k: v for item in cert["subject"] for k, v in item}
self = create_context(certfile, keyfile, password)
self.__class__ = cls
self.sanic = {**subject, **kw}
return self
def __init__(self, cert, key, **kw):
pass # Do not call super().__init__ because it is already initialized
class CertSelector(ssl.SSLContext):
"""Automatically select SSL certificate based on the hostname that the
client is trying to access, via SSL SNI. Paths to certificate folders
with privkey.pem and fullchain.pem in them should be provided, and
will be matched in the order given whenever there is a new connection.
"""
def __new__(cls, ctxs):
return super().__new__(cls)
def __init__(self, ctxs: Iterable[Optional[ssl.SSLContext]]):
super().__init__()
self.sni_callback = selector_sni_callback # type: ignore
self.sanic_select = []
self.sanic_fallback = None
all_names = []
for i, ctx in enumerate(ctxs):
if not ctx:
continue
names = dict(getattr(ctx, "sanic", {})).get("names", [])
all_names += names
self.sanic_select.append(ctx)
if i == 0:
self.sanic_fallback = ctx
if not all_names:
raise ValueError(
"No certificates with SubjectAlternativeNames found."
)
logger.info(f"Certificate vhosts: {', '.join(all_names)}")
def find_cert(self: CertSelector, server_name: str):
"""Find the first certificate that matches the given SNI.
:raises ssl.CertificateError: No matching certificate found.
:return: A matching ssl.SSLContext object if found."""
if not server_name:
if self.sanic_fallback:
return self.sanic_fallback
raise ValueError(
"The client provided no SNI to match for certificate."
)
for ctx in self.sanic_select:
if match_hostname(ctx, server_name):
return ctx
if self.sanic_fallback:
return self.sanic_fallback
raise ValueError(f"No certificate found matching hostname {server_name!r}")
def match_hostname(
ctx: Union[ssl.SSLContext, CertSelector], hostname: str
) -> bool:
"""Match names from CertSelector against a received hostname."""
# Local certs are considered trusted, so this can be less pedantic
# and thus faster than the deprecated ssl.match_hostname function is.
names = dict(getattr(ctx, "sanic", {})).get("names", [])
hostname = hostname.lower()
for name in names:
if name.startswith("*."):
if hostname.split(".", 1)[-1] == name[2:]:
return True
elif name == hostname:
return True
return False
def selector_sni_callback(
sslobj: ssl.SSLObject, server_name: str, ctx: CertSelector
) -> Optional[int]:
"""Select a certificate matching the SNI."""
# Call server_name_callback to store the SNI on sslobj
server_name_callback(sslobj, server_name, ctx)
# Find a new context matching the hostname
try:
sslobj.context = find_cert(ctx, server_name)
except ValueError as e:
logger.warning(f"Rejecting TLS connection: {e}")
# This would show ERR_SSL_UNRECOGNIZED_NAME_ALERT on client side if
# asyncio/uvloop did proper SSL shutdown. They don't.
return ssl.ALERT_DESCRIPTION_UNRECOGNIZED_NAME
return None # mypy complains without explicit return
def server_name_callback(
sslobj: ssl.SSLObject, server_name: str, ctx: ssl.SSLContext
) -> None:
"""Store the received SNI as sslobj.sanic_server_name."""
sslobj.sanic_server_name = server_name # type: ignore

View File

@ -22,7 +22,9 @@ class OptionalDispatchEvent(BaseScheme):
raw_source = getsource(method)
src = dedent(raw_source)
tree = parse(src)
node = RemoveDispatch(self._registered_events).visit(tree)
node = RemoveDispatch(
self._registered_events, self.app.state.verbosity
).visit(tree)
compiled_src = compile(node, method.__name__, "exec")
exec_locals: Dict[str, Any] = {}
exec(compiled_src, module_globals, exec_locals) # nosec
@ -31,8 +33,9 @@ class OptionalDispatchEvent(BaseScheme):
class RemoveDispatch(NodeTransformer):
def __init__(self, registered_events) -> None:
def __init__(self, registered_events, verbosity: int = 0) -> None:
self._registered_events = registered_events
self._verbosity = verbosity
def visit_Expr(self, node: Expr) -> Any:
call = node.value
@ -49,7 +52,8 @@ class RemoveDispatch(NodeTransformer):
if hasattr(event, "s"):
event_name = getattr(event, "value", event.s)
if self._not_registered(event_name):
logger.debug(f"Disabling event: {event_name}")
if self._verbosity >= 2:
logger.debug(f"Disabling event: {event_name}")
return None
return node

View File

@ -48,7 +48,7 @@ def load_module_from_file_location(
"""Returns loaded module provided as a file path.
:param args:
Coresponds to importlib.util.spec_from_file_location location
Corresponds to importlib.util.spec_from_file_location location
parameters,but with this differences:
- It has to be of a string or bytes type.
- You can also use here environment variables
@ -58,10 +58,10 @@ def load_module_from_file_location(
If location parameter is of a bytes type, then use this encoding
to decode it into string.
:param args:
Coresponds to the rest of importlib.util.spec_from_file_location
Corresponds to the rest of importlib.util.spec_from_file_location
parameters.
:param kwargs:
Coresponds to the rest of importlib.util.spec_from_file_location
Corresponds to the rest of importlib.util.spec_from_file_location
parameters.
For example You can:

View File

@ -310,7 +310,7 @@ if __name__ == "__main__":
cli.add_argument(
"--milestone",
"-ms",
help="Git Release milestone information to include in relase note",
help="Git Release milestone information to include in release note",
required=False,
)
cli.add_argument(

View File

@ -72,6 +72,7 @@ setup_kwargs = {
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
],
"entry_points": {"console_scripts": ["sanic = sanic.__main__:main"]},
}
@ -94,7 +95,7 @@ requirements = [
tests_require = [
"sanic-testing>=0.7.0",
"pytest==5.2.1",
"pytest==6.2.5",
"coverage==5.3",
"gunicorn==20.0.4",
"pytest-cov",
@ -107,7 +108,7 @@ tests_require = [
"black",
"isort>=5.0.0",
"bandit",
"mypy>=0.901",
"mypy>=0.901,<0.910",
"docutils",
"pygments",
"uvicorn<0.15.0",
@ -120,9 +121,11 @@ docs_require = [
"docutils",
"pygments",
"m2r2",
"mistune<2.0.0",
]
dev_require = tests_require + [
"cryptography",
"tox",
"towncrier",
]

113
tests/certs/createcerts.py Normal file
View File

@ -0,0 +1,113 @@
from datetime import datetime, timedelta
from ipaddress import ip_address
from os import path
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec, rsa
from cryptography.x509 import (
BasicConstraints,
CertificateBuilder,
DNSName,
ExtendedKeyUsage,
IPAddress,
KeyUsage,
Name,
NameAttribute,
SubjectAlternativeName,
random_serial_number,
)
from cryptography.x509.oid import ExtendedKeyUsageOID, NameOID
def writefiles(key, cert):
cn = cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value
folder = path.join(path.dirname(__file__), cn)
with open(path.join(folder, "fullchain.pem"), "wb") as f:
f.write(cert.public_bytes(serialization.Encoding.PEM))
with open(path.join(folder, "privkey.pem"), "wb") as f:
f.write(
key.private_bytes(
serialization.Encoding.PEM,
serialization.PrivateFormat.TraditionalOpenSSL,
serialization.NoEncryption(),
)
)
def selfsigned(key, common_name, san):
subject = issuer = Name(
[
NameAttribute(NameOID.COMMON_NAME, common_name),
NameAttribute(NameOID.ORGANIZATION_NAME, "Sanic Org"),
]
)
cert = (
CertificateBuilder()
.subject_name(subject)
.issuer_name(issuer)
.public_key(key.public_key())
.serial_number(random_serial_number())
.not_valid_before(datetime.utcnow())
.not_valid_after(datetime.utcnow() + timedelta(days=365.25 * 8))
.add_extension(
KeyUsage(
True, False, False, False, False, False, False, False, False
),
critical=True,
)
.add_extension(
ExtendedKeyUsage(
[
ExtendedKeyUsageOID.SERVER_AUTH,
ExtendedKeyUsageOID.CLIENT_AUTH,
]
),
critical=False,
)
.add_extension(
BasicConstraints(ca=True, path_length=None),
critical=True,
)
.add_extension(
SubjectAlternativeName(
[
IPAddress(ip_address(n))
if n[0].isdigit() or ":" in n
else DNSName(n)
for n in san
]
),
critical=False,
)
.sign(key, hashes.SHA256())
)
return cert
# Sanic example/test self-signed cert RSA
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
cert = selfsigned(
key,
"sanic.example",
[
"sanic.example",
"www.sanic.example",
"*.sanic.test",
"2001:db8::541c",
],
)
writefiles(key, cert)
# Sanic localhost self-signed cert ECDSA
key = ec.generate_private_key(ec.SECP256R1)
cert = selfsigned(
key,
"localhost",
[
"localhost",
"127.0.0.1",
"::1",
],
)
writefiles(key, cert)

View File

@ -0,0 +1,5 @@
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIFP3fCUob41U1wvVOvei4dGsXrZeSiBUCX/xVu9215bvoAoGCCqGSM49
AwEHoUQDQgAEvBHo/RatEnPRBeiLURXX2sQDBbr9XRb73Fvm8jIOrPyJg8PcvNXH
D1jQah5K60THdjmdkLsY/hamZfqLb24EFQ==
-----END EC PRIVATE KEY-----

View File

@ -0,0 +1,12 @@
-----BEGIN CERTIFICATE-----
MIIBwjCCAWigAwIBAgIUQOCJIPRMiZsOMmvH0uiofxEDFn8wCgYIKoZIzj0EAwIw
KDESMBAGA1UEAwwJbG9jYWxob3N0MRIwEAYDVQQKDAlTYW5pYyBPcmcwHhcNMjEx
MDE5MTcwMTE3WhcNMjkxMDE5MTcwMTE3WjAoMRIwEAYDVQQDDAlsb2NhbGhvc3Qx
EjAQBgNVBAoMCVNhbmljIE9yZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABHf0
SrvRtGF9KIXEtk4+6vsqleNaleuYVvf4d6TD3pX1CbOV/NsZdW6+EhkA1U2pEBnJ
txXqAGVJT4ans8ud3K6jcDBuMA4GA1UdDwEB/wQEAwIHgDAdBgNVHSUEFjAUBggr
BgEFBQcDAQYIKwYBBQUHAwIwDwYDVR0TAQH/BAUwAwEB/zAsBgNVHREEJTAjggls
b2NhbGhvc3SHBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAEwCgYIKoZIzj0EAwIDSAAw
RQIhAJhwopVuiW0S4MKEDCl+Vxwyei5AYobrALcP0pwGpFzIAiAWkxMPeAOMWIjq
LD4t2UZ9h6ma2fS2Jf9pzTon6438Ng==
-----END CERTIFICATE-----

View File

@ -0,0 +1,5 @@
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIDKTs1c2Qo7KMQ8DJrmIuNb29z2fNi4O+TNkJWjvclvsoAoGCCqGSM49
AwEHoUQDQgAEd/RKu9G0YX0ohcS2Tj7q+yqV41qV65hW9/h3pMPelfUJs5X82xl1
br4SGQDVTakQGcm3FeoAZUlPhqezy53crg==
-----END EC PRIVATE KEY-----

View File

@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDdzCCAl+gAwIBAgIUF1H0To9k3mUiMT8mjF6g45A9KgcwDQYJKoZIhvcNAQEL
BQAwLDEWMBQGA1UEAwwNc2FuaWMuZXhhbXBsZTESMBAGA1UECgwJU2FuaWMgT3Jn
MB4XDTIxMTAxOTE3MDExN1oXDTI5MTAxOTE3MDExN1owLDEWMBQGA1UEAwwNc2Fu
aWMuZXhhbXBsZTESMBAGA1UECgwJU2FuaWMgT3JnMIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEAzNeC95zB5LRybz9Wl16+Q4kbOLgXlyUQVKhg9OZD1ChN
3T4Ya/KvChQmPWOWdF814NgkkNS1yHKXlORU2Ljbpqzr+WoOAwGVixbRTknjmI46
glUhCOJlGqxl16RfuYA2BWv0+At9jKBhT1tnrGVhfqldnxsb4FDh0JsFnrZN4/DB
z6x8PY1z0eQMgsyeKAfSTTnGXhkZzAQz6afuQbGZhe8vQIUTvwmnZiU9OdUZ6nLc
b7lSbIQ1edT6/xXUkbn5ixGsEQTf6JWLqEDLkqpo9sbkYmvMMQpj2pCgtaEjx7An
+hQe8Itv+i6h0KD3ARVeCBgdWEgXZTs7zKrmU77xfwIDAQABo4GQMIGNMA4GA1Ud
DwEB/wQEAwIHgDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDwYDVR0T
AQH/BAUwAwEB/zBLBgNVHREERDBCgg1zYW5pYy5leGFtcGxlghF3d3cuc2FuaWMu
ZXhhbXBsZYIMKi5zYW5pYy50ZXN0hxAgAQ24AAAAAAAAAAAAAFQcMA0GCSqGSIb3
DQEBCwUAA4IBAQBLV7xSEI7308Qmm3SyV+ro9jQ/i2ydwUIUyRMtf04EFRS8fHK/
Lln5Yweaba9XP5k3DLSC63Qg1tE50fVqQypbWVA4SMkMW21cK8vEhHEYeGYkHsuC
xCFdwJYhmofqWaQ/j/ErLBrQbaHBdSJ/Nou5RPRtM4HrSU7F2azLGmLczYk6PcZa
wSBvoXdjiEUrRl7XB0iB2ktTga6amuYz4bSJzUvaA8SodJzC4OKhRsduUD83LdDi
2As4KiTcSO/SOCaK2KmbPNBlTKMF4cpqysGMvmnGVWhECOG1PZItJkWNbbBV4XRR
qGmrey2JwDDeTYHFDHaND385/PSJKfSSGLNk
-----END CERTIFICATE-----

View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAzNeC95zB5LRybz9Wl16+Q4kbOLgXlyUQVKhg9OZD1ChN3T4Y
a/KvChQmPWOWdF814NgkkNS1yHKXlORU2Ljbpqzr+WoOAwGVixbRTknjmI46glUh
COJlGqxl16RfuYA2BWv0+At9jKBhT1tnrGVhfqldnxsb4FDh0JsFnrZN4/DBz6x8
PY1z0eQMgsyeKAfSTTnGXhkZzAQz6afuQbGZhe8vQIUTvwmnZiU9OdUZ6nLcb7lS
bIQ1edT6/xXUkbn5ixGsEQTf6JWLqEDLkqpo9sbkYmvMMQpj2pCgtaEjx7An+hQe
8Itv+i6h0KD3ARVeCBgdWEgXZTs7zKrmU77xfwIDAQABAoIBABWKpG89wPY4M8CX
PJf2krOve3lfgruWXj1I58lZXdC13Fpj6VWQ0++PZuYVzwC18oiOsmm4tNU7l81E
pdeUuSSyEq7MBGU0iXFzGNfO1Wx5qJWENlEk3dUMRDmFQ7vSS9wOGljrfGyJgTJD
PofWsYYMcZgF1cylNNonM1QZf990hfd0JDfO6CHCloRe/pKIdVzIxQp+3Ju/3OPk
Gw5V+YnVrG4wdZbhOCW2hPp/TLdgFy/xHvrxkEkGx+2ZHGCw9uFj2LRZJwwuaO9p
LDzbyfbFlPWIHdPamdBvenZ6RNTf28+YsbiqwoOk5C286QYb/VDnT8UnG42hXS1I
p3m//qECgYEA7zXmMSBy1tkMQsuaAakOFfl2HfVL2rrW6/CH6BwcCHUD6Wr8wv6a
kPNhI6pqqnP6Xg8XqJXfyIVZOJYPQMQr69zni2y7b3jPOemVGTBSqN7UE71NZkHF
+HZov55bPuX/KD6qc/WAXCyEcISy9TmcA7cEN7ivmyXmbuSXEoiAjlsCgYEA2zgU
mzL6ObJ2555UOqzGCMx6o2KQqOgA1SGmYLBRX77I3fuvGj+DLo6/iuM0FcVV7alG
U/U6qqrSymtdRgeZXHziSVhLZKY/qobgKG2iO1F3DzqyZ94EK/v0XRS4UyiJma3f
lwVG/BcVnv+FKCYUo2JKGln0R8Wcm6D9Nxp0mq0CgYEAn0Dj+oreyZiAqCuCYV6a
SRjmgTVghcNj+HoPEQE9zIeSziBzHKKCZsQRRLxc/RPveBVWK99zt7zHVHvatcSk
dQeBg3olIyZr1+NhZv6b2V9YE7gwwkZBtZOnUwLrPmnCwJlPw5mLFlJw7bP6rHXp
HzQF887Z4lGOIv++cBE+fQcCgYEArF26BhXdHcSvLYsWW1RCGeT9gL4dVFGnZe2h
bmD0er3+Hlyo35CUyuS+wqvG5l9VIxt4CsfFKzBJsZMdsdSDx28CVf0wuqDlamXG
lsMtTkrNvJHAeV7eFN900kNaczhqiQVnys0BdXGJNI1g26Klk5nS/klAg7ZjXxME
RnFswbkCgYBG5OToLXM8pg3yTM9MHMSXFhnnd2MbBK2AySFah2P1V4xv1rJdklU0
9QRTd/hQmYGHioPIF9deU8YSWlj+FBimyoNfJ51YzFyp2maOSJq4Wxe1nv2DflRK
gh5pkl8FizoDnu8BHu1AjOfRQJ3/tCIi2XZJgBuCxyTjd1b6hVUhyg==
-----END RSA PRIVATE KEY-----

View File

@ -1,22 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIDtTCCAp2gAwIBAgIJAO6wb0FSc/rNMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
BAYTAlVTMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
aWRnaXRzIFB0eSBMdGQwHhcNMTcwMzAzMTUyODAzWhcNMTkxMTI4MTUyODAzWjBF
MQswCQYDVQQGEwJVUzETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
CgKCAQEAsy7Zb3p4yCEnUtPLwqeJrwj9u/ZmcFCrMAktFBx9hG6rY2r7mdB6Bflh
V5cUJXxnsNiDpYcxGhA8kry7pEork1vZ05DyZC9ulVlvxBouVShBcLLwdpaoTGqE
vYtejv6x7ogwMXOjkWWb1WpOv4CVhpeXJ7O/d1uAiYgcUpTpPp4ONG49IAouBHq3
h+o4nVvNfB0J8gaCtTsTZqi1Wt8WYs3XjxGJaKh//ealfRe1kuv40CWQ8gjaC8/1
w9pHdom3Wi/RwfDM3+dVGV6M5lAbPXMB4RK17Hk9P3hlJxJOpKBdgcBJPXtNrTwf
qEWWxk2mB/YVyB84AxjkkNoYyi2ggQIDAQABo4GnMIGkMB0GA1UdDgQWBBRa46Ix
9s9tmMqu+Zz1mocHghm4NTB1BgNVHSMEbjBsgBRa46Ix9s9tmMqu+Zz1mocHghm4
NaFJpEcwRTELMAkGA1UEBhMCVVMxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNV
BAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAO6wb0FSc/rNMAwGA1UdEwQF
MAMBAf8wDQYJKoZIhvcNAQELBQADggEBACdrnM8zb7abxAJsU5WLn1IR0f2+EFA7
ezBEJBM4bn0IZrXuP5ThZ2wieJlshG0C16XN9+zifavHci+AtQwWsB0f/ppHdvWQ
7wt7JN88w+j0DNIYEadRCjWxR3gRAXPgKu3sdyScKFq8MvB49A2EdXRmQSTIM6Fj
teRbE+poxewFT0mhurf3xrtGiSALmv7uAzhRDqpYUzcUlbOGgkyFLYAOOdvZvei+
mfXDi4HKYxgyv53JxBARMdajnCHXM7zQ6Tjc8j1HRtmDQ3XapUB559KfxfODGQq5
zmeoZWU4duxcNXJM0Eiz1CJ39JoWwi8sqaGi/oskuyAh7YKyVTn8xa8=
-----END CERTIFICATE-----

View File

@ -1,27 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAsy7Zb3p4yCEnUtPLwqeJrwj9u/ZmcFCrMAktFBx9hG6rY2r7
mdB6BflhV5cUJXxnsNiDpYcxGhA8kry7pEork1vZ05DyZC9ulVlvxBouVShBcLLw
dpaoTGqEvYtejv6x7ogwMXOjkWWb1WpOv4CVhpeXJ7O/d1uAiYgcUpTpPp4ONG49
IAouBHq3h+o4nVvNfB0J8gaCtTsTZqi1Wt8WYs3XjxGJaKh//ealfRe1kuv40CWQ
8gjaC8/1w9pHdom3Wi/RwfDM3+dVGV6M5lAbPXMB4RK17Hk9P3hlJxJOpKBdgcBJ
PXtNrTwfqEWWxk2mB/YVyB84AxjkkNoYyi2ggQIDAQABAoIBAFgVasxTf3aaXbNo
7JzXMWb7W4iAG2GRNmZZzHA7hTSKFvS7jc3SX3n6WvDtEvlOi8ay2RyRNgEjBDP6
VZ/w2jUJjS5k7dN0Qb9nhPr5B9fS/0CAppcVfsx5/KEVFzniWOPyzQYyW7FJKu8h
4G5hrp/Ie4UH5tKtB6YUZB/wliyyQUkAZdBcoy1hfkOZLAXb1oofArKsiQUHIRA5
th1yyS4cZP8Upngd1EE+d95dFHM2F6iI2lj6DHuu+JxUZ+wKXoNimdG7JniRtIf4
56GoDov83Ey+XbIS6FSQc9nY0ijBDcubl/yP3roCQpE+MZ9BNEo5uj7YmCtAMYLW
TXTNBGUCgYEA4wdkH1NLdub2NcpqwmSA0AtbRvDkt0XTDWWwmuMr/+xPVa4sUKHs
80THQEX/WAZroP6IPbMP6BJhzb53vECukgC65qPxu6M9D1lBGtglxgen4AMu1bKK
gnM8onwARGIo/2ay6qRRZZCxg0TvBky3hbTcIM2zVrnKU6VVyGKHSV8CgYEAygxs
WQYrACv3XN6ZEzyxy08JgjbcnkPWK/m3VPcyHgdEkDu8+nDdUVdbF/js2JWMMx5g
vrPhZ7jVLOXGcLr5mVU4dG5tW5lU0bMy+YYxpEQDiBKlpXgfOsQnakHj7cCZ6bay
mKjJck2oEAQS9bqOJN/Ts5vhOmc8rmhkO7hnAh8CgYEArhVDy9Vl/1WYo6SD+m1w
bJbYtewPpQzwicxZAFuDqKk+KDf3GRkhBWTO2FUUOB4sN3YVaCI+5zf5MPeE/qAm
fCP9LM+3k6bXMkbBamEljdTfACHQruJJ3T+Z1gn5dnZCc5z/QncfRx8NTtfz5MO8
0dTeGnVAuBacs0kLHy2WCUcCgYALNBkl7pOf1NBIlAdE686oCV/rmoMtO3G6yoQB
8BsVUy3YGZfnAy8ifYeNkr3/XHuDsiGHMY5EJBmd/be9NID2oaUZv63MsHnljtw6
vdgu1Z6kgvQwcrK4nXvaBoFPA6kFLp5EnMde0TOKf89VVNzg6pBgmzon9OWGfj9g
mF8N3QKBgQCeoLwxUxpzEA0CPHm7DWF0LefVGllgZ23Eqncdy0QRku5zwwibszbL
sWaR3uDCc3oYcbSGCDVx3cSkvMAJNalc5ZHPfoV9W0+v392/rrExo5iwD8CSoCb2
gFWkeR7PBrD3NzFzFAWyiudzhBKHfRsB0MpCXbJV/WLqTlGIbEypjg==
-----END RSA PRIVATE KEY-----

View File

@ -6,7 +6,8 @@ import string
import sys
import uuid
from typing import Tuple
from logging import LogRecord
from typing import Callable, List, Tuple
import pytest
@ -170,3 +171,16 @@ def run_startup(caplog):
return caplog.record_tuples
return run
@pytest.fixture(scope="function")
def message_in_records():
def msg_in_log(records: List[LogRecord], msg: str):
error_captured = False
for record in records:
if msg in record.message:
error_captured = True
break
return error_captured
return msg_in_log

View File

@ -23,6 +23,7 @@ async def app_info_dump(app: Sanic, _):
"access_log": app.config.ACCESS_LOG,
"auto_reload": app.auto_reload,
"debug": app.debug,
"noisy_exceptions": app.config.NOISY_EXCEPTIONS,
}
logger.info(json.dumps(app_data))

View File

@ -39,41 +39,39 @@ def test_app_loop_running(app):
def test_create_asyncio_server(app):
if not uvloop_installed():
loop = asyncio.get_event_loop()
asyncio_srv_coro = app.create_server(return_asyncio_server=True)
assert isawaitable(asyncio_srv_coro)
srv = loop.run_until_complete(asyncio_srv_coro)
assert srv.is_serving() is True
loop = asyncio.get_event_loop()
asyncio_srv_coro = app.create_server(return_asyncio_server=True)
assert isawaitable(asyncio_srv_coro)
srv = loop.run_until_complete(asyncio_srv_coro)
assert srv.is_serving() is True
def test_asyncio_server_no_start_serving(app):
if not uvloop_installed():
loop = asyncio.get_event_loop()
asyncio_srv_coro = app.create_server(
port=43123,
return_asyncio_server=True,
asyncio_server_kwargs=dict(start_serving=False),
)
srv = loop.run_until_complete(asyncio_srv_coro)
assert srv.is_serving() is False
loop = asyncio.get_event_loop()
asyncio_srv_coro = app.create_server(
port=43123,
return_asyncio_server=True,
asyncio_server_kwargs=dict(start_serving=False),
)
srv = loop.run_until_complete(asyncio_srv_coro)
assert srv.is_serving() is False
def test_asyncio_server_start_serving(app):
if not uvloop_installed():
loop = asyncio.get_event_loop()
asyncio_srv_coro = app.create_server(
port=43124,
return_asyncio_server=True,
asyncio_server_kwargs=dict(start_serving=False),
)
srv = loop.run_until_complete(asyncio_srv_coro)
assert srv.is_serving() is False
loop.run_until_complete(srv.start_serving())
assert srv.is_serving() is True
wait_close = srv.close()
loop.run_until_complete(wait_close)
# Looks like we can't easily test `serve_forever()`
loop = asyncio.get_event_loop()
asyncio_srv_coro = app.create_server(
port=43124,
return_asyncio_server=True,
asyncio_server_kwargs=dict(start_serving=False),
)
srv = loop.run_until_complete(asyncio_srv_coro)
assert srv.is_serving() is False
loop.run_until_complete(srv.startup())
loop.run_until_complete(srv.start_serving())
assert srv.is_serving() is True
wait_close = srv.close()
loop.run_until_complete(wait_close)
# Looks like we can't easily test `serve_forever()`
def test_create_server_main(app, caplog):
@ -90,6 +88,21 @@ def test_create_server_main(app, caplog):
) in caplog.record_tuples
def test_create_server_no_startup(app):
loop = asyncio.get_event_loop()
asyncio_srv_coro = app.create_server(
port=43124,
return_asyncio_server=True,
asyncio_server_kwargs=dict(start_serving=False),
)
srv = loop.run_until_complete(asyncio_srv_coro)
message = (
"Cannot run Sanic server without first running await server.startup()"
)
with pytest.raises(SanicException, match=message):
loop.run_until_complete(srv.start_serving())
def test_create_server_main_convenience(app, caplog):
app.main_process_start(lambda *_: ...)
loop = asyncio.get_event_loop()
@ -104,6 +117,19 @@ def test_create_server_main_convenience(app, caplog):
) in caplog.record_tuples
def test_create_server_init(app, caplog):
loop = asyncio.get_event_loop()
asyncio_srv_coro = app.create_server(return_asyncio_server=True)
server = loop.run_until_complete(asyncio_srv_coro)
message = (
"AsyncioServer.init has been deprecated and will be removed in v22.6. "
"Use Sanic.state.is_started instead."
)
with pytest.warns(DeprecationWarning, match=message):
server.init
def test_app_loop_not_running(app):
with pytest.raises(SanicException) as excinfo:
app.loop
@ -444,3 +470,9 @@ def test_custom_context():
app = Sanic("custom", ctx=ctx)
assert app.ctx == ctx
def test_cannot_run_fast_and_workers(app):
message = "You cannot use both fast=True and workers=X"
with pytest.raises(RuntimeError, match=message):
app.run(fast=True, workers=4)

View File

@ -1,6 +1,4 @@
from copy import deepcopy
from sanic import Blueprint, Sanic, blueprints, response
from sanic import Blueprint, Sanic
from sanic.response import text

View File

@ -1088,3 +1088,31 @@ def test_bp_set_attribute_warning():
"and will be removed in version 21.12. You should change your "
"Blueprint instance to use instance.ctx.foo instead."
)
def test_early_registration(app):
assert len(app.router.routes) == 0
bp = Blueprint("bp")
@bp.get("/one")
async def one(_):
return text("one")
app.blueprint(bp)
assert len(app.router.routes) == 1
@bp.get("/two")
async def two(_):
return text("two")
@bp.get("/three")
async def three(_):
return text("three")
assert len(app.router.routes) == 3
for path in ("one", "two", "three"):
_, response = app.test_client.get(f"/{path}")
assert response.text == path

View File

@ -8,7 +8,6 @@ import pytest
from sanic_routing import __version__ as __routing_version__
from sanic import __version__
from sanic.config import BASE_LOGO
def capture(command):
@ -19,13 +18,20 @@ def capture(command):
cwd=Path(__file__).parent,
)
try:
out, err = proc.communicate(timeout=0.5)
out, err = proc.communicate(timeout=1)
except subprocess.TimeoutExpired:
proc.kill()
out, err = proc.communicate()
return out, err, proc.returncode
def starting_line(lines):
for idx, line in enumerate(lines):
if line.strip().startswith(b"Sanic v"):
return idx
return 0
@pytest.mark.parametrize(
"appname",
(
@ -39,12 +45,62 @@ def test_server_run(appname):
command = ["sanic", appname]
out, err, exitcode = capture(command)
lines = out.split(b"\n")
firstline = lines[6]
firstline = lines[starting_line(lines) + 1]
assert exitcode != 1
assert firstline == b"Goin' Fast @ http://127.0.0.1:8000"
@pytest.mark.parametrize(
"cmd",
(
(
"--cert=certs/sanic.example/fullchain.pem",
"--key=certs/sanic.example/privkey.pem",
),
(
"--tls=certs/sanic.example/",
"--tls=certs/localhost/",
),
(
"--tls=certs/sanic.example/",
"--tls=certs/localhost/",
"--tls-strict-host",
),
),
)
def test_tls_options(cmd):
command = ["sanic", "fake.server.app", *cmd, "-p=9999", "--debug"]
out, err, exitcode = capture(command)
assert exitcode != 1
lines = out.split(b"\n")
firstline = lines[starting_line(lines) + 1]
assert firstline == b"Goin' Fast @ https://127.0.0.1:9999"
@pytest.mark.parametrize(
"cmd",
(
("--cert=certs/sanic.example/fullchain.pem",),
(
"--cert=certs/sanic.example/fullchain.pem",
"--key=certs/sanic.example/privkey.pem",
"--tls=certs/localhost/",
),
("--tls-strict-host",),
),
)
def test_tls_wrong_options(cmd):
command = ["sanic", "fake.server.app", *cmd, "-p=9999", "--debug"]
out, err, exitcode = capture(command)
assert exitcode == 1
assert not out
lines = err.decode().split("\n")
errmsg = lines[8]
assert errmsg == "TLS certificates must be specified by either of:"
@pytest.mark.parametrize(
"cmd",
(
@ -52,16 +108,67 @@ def test_server_run(appname):
("-H", "localhost", "-p", "9999"),
),
)
def test_host_port(cmd):
def test_host_port_localhost(cmd):
command = ["sanic", "fake.server.app", *cmd]
out, err, exitcode = capture(command)
lines = out.split(b"\n")
firstline = lines[6]
firstline = lines[starting_line(lines) + 1]
assert exitcode != 1
assert firstline == b"Goin' Fast @ http://localhost:9999"
@pytest.mark.parametrize(
"cmd",
(
("--host=127.0.0.127", "--port=9999"),
("-H", "127.0.0.127", "-p", "9999"),
),
)
def test_host_port_ipv4(cmd):
command = ["sanic", "fake.server.app", *cmd]
out, err, exitcode = capture(command)
lines = out.split(b"\n")
firstline = lines[starting_line(lines) + 1]
assert exitcode != 1
assert firstline == b"Goin' Fast @ http://127.0.0.127:9999"
@pytest.mark.parametrize(
"cmd",
(
("--host=::", "--port=9999"),
("-H", "::", "-p", "9999"),
),
)
def test_host_port_ipv6_any(cmd):
command = ["sanic", "fake.server.app", *cmd]
out, err, exitcode = capture(command)
lines = out.split(b"\n")
firstline = lines[starting_line(lines) + 1]
assert exitcode != 1
assert firstline == b"Goin' Fast @ http://[::]:9999"
@pytest.mark.parametrize(
"cmd",
(
("--host=::1", "--port=9999"),
("-H", "::1", "-p", "9999"),
),
)
def test_host_port_ipv6_loopback(cmd):
command = ["sanic", "fake.server.app", *cmd]
out, err, exitcode = capture(command)
lines = out.split(b"\n")
firstline = lines[starting_line(lines) + 1]
assert exitcode != 1
assert firstline == b"Goin' Fast @ http://[::1]:9999"
@pytest.mark.parametrize(
"num,cmd",
(
@ -78,9 +185,13 @@ def test_num_workers(num, cmd):
out, err, exitcode = capture(command)
lines = out.split(b"\n")
worker_lines = [line for line in lines if b"worker" in line]
worker_lines = [
line
for line in lines
if b"Starting worker" in line or b"Stopping worker" in line
]
assert exitcode != 1
assert len(worker_lines) == num * 2
assert len(worker_lines) == num * 2, f"Lines found: {lines}"
@pytest.mark.parametrize("cmd", ("--debug", "-d"))
@ -89,10 +200,9 @@ def test_debug(cmd):
out, err, exitcode = capture(command)
lines = out.split(b"\n")
app_info = lines[26]
app_info = lines[starting_line(lines) + 9]
info = json.loads(app_info)
assert (b"\n".join(lines[:6])).decode("utf-8") == BASE_LOGO
assert info["debug"] is True
assert info["auto_reload"] is True
@ -103,7 +213,7 @@ def test_auto_reload(cmd):
out, err, exitcode = capture(command)
lines = out.split(b"\n")
app_info = lines[26]
app_info = lines[starting_line(lines) + 9]
info = json.loads(app_info)
assert info["debug"] is False
@ -118,7 +228,7 @@ def test_access_logs(cmd, expected):
out, err, exitcode = capture(command)
lines = out.split(b"\n")
app_info = lines[26]
app_info = lines[starting_line(lines) + 8]
info = json.loads(app_info)
assert info["access_log"] is expected
@ -131,3 +241,21 @@ def test_version(cmd):
version_string = f"Sanic {__version__}; Routing {__routing_version__}\n"
assert out == version_string.encode("utf-8")
@pytest.mark.parametrize(
"cmd,expected",
(
("--noisy-exceptions", True),
("--no-noisy-exceptions", False),
),
)
def test_noisy_exceptions(cmd, expected):
command = ["sanic", "fake.server.app", cmd]
out, err, exitcode = capture(command)
lines = out.split(b"\n")
app_info = lines[starting_line(lines) + 8]
info = json.loads(app_info)
assert info["noisy_exceptions"] is expected

48
tests/test_coffee.py Normal file
View File

@ -0,0 +1,48 @@
import logging
from unittest.mock import patch
import pytest
from sanic.application.logo import COFFEE_LOGO, get_logo
from sanic.exceptions import SanicException
def has_sugar(value):
if value:
raise SanicException("I said no sugar please")
return False
@pytest.mark.parametrize("sugar", (True, False))
def test_no_sugar(sugar):
if sugar:
with pytest.raises(SanicException):
assert has_sugar(sugar)
else:
assert not has_sugar(sugar)
def test_get_logo_returns_expected_logo():
with patch("sys.stdout.isatty") as isatty:
isatty.return_value = True
logo = get_logo(coffee=True)
assert logo is COFFEE_LOGO
def test_logo_true(app, caplog):
@app.after_server_start
async def shutdown(*_):
app.stop()
with patch("sys.stdout.isatty") as isatty:
isatty.return_value = True
with caplog.at_level(logging.DEBUG):
app.make_coffee()
# Only in the regular logo
assert " ▄███ █████ ██ " not in caplog.text
# Only in the coffee logo
assert " ██ ██▀▀▄ " in caplog.text

View File

@ -3,6 +3,7 @@ from os import environ
from pathlib import Path
from tempfile import TemporaryDirectory
from textwrap import dedent
from unittest.mock import Mock
import pytest
@ -350,3 +351,40 @@ def test_update_from_lowercase_key(app):
d = {"test_setting_value": 1}
app.update_config(d)
assert "test_setting_value" not in app.config
def test_deprecation_notice_when_setting_logo(app):
message = (
"Setting the config.LOGO is deprecated and will no longer be "
"supported starting in v22.6."
)
with pytest.warns(DeprecationWarning, match=message):
app.config.LOGO = "My Custom Logo"
def test_config_set_methods(app, monkeypatch):
post_set = Mock()
monkeypatch.setattr(Config, "_post_set", post_set)
app.config.FOO = 1
post_set.assert_called_once_with("FOO", 1)
post_set.reset_mock()
app.config["FOO"] = 2
post_set.assert_called_once_with("FOO", 2)
post_set.reset_mock()
app.config.update({"FOO": 3})
post_set.assert_called_once_with("FOO", 3)
post_set.reset_mock()
app.config.update([("FOO", 4)])
post_set.assert_called_once_with("FOO", 4)
post_set.reset_mock()
app.config.update(FOO=5)
post_set.assert_called_once_with("FOO", 5)
post_set.reset_mock()
app.config.update_config({"FOO": 6})
post_set.assert_called_once_with("FOO", 6)

View File

@ -1,8 +1,10 @@
import pytest
from sanic import Sanic
from sanic.config import Config
from sanic.errorpages import HTMLRenderer, exception_response
from sanic.exceptions import NotFound, SanicException
from sanic.handlers import ErrorHandler
from sanic.request import Request
from sanic.response import HTTPResponse, html, json, text
@ -271,3 +273,72 @@ def test_combinations_for_auto(fake_request, accept, content_type, expected):
)
assert response.content_type == expected
def test_allow_fallback_error_format_set_main_process_start(app):
@app.main_process_start
async def start(app, _):
app.config.FALLBACK_ERROR_FORMAT = "text"
request, response = app.test_client.get("/error")
assert request.app.error_handler.fallback == "text"
assert response.status == 500
assert response.content_type == "text/plain; charset=utf-8"
def test_setting_fallback_to_non_default_raise_warning(app):
app.error_handler = ErrorHandler(fallback="text")
assert app.error_handler.fallback == "text"
with pytest.warns(
UserWarning,
match=(
"Overriding non-default ErrorHandler fallback value. "
"Changing from text to auto."
),
):
app.config.FALLBACK_ERROR_FORMAT = "auto"
assert app.error_handler.fallback == "auto"
app.config.FALLBACK_ERROR_FORMAT = "text"
with pytest.warns(
UserWarning,
match=(
"Overriding non-default ErrorHandler fallback value. "
"Changing from text to json."
),
):
app.config.FALLBACK_ERROR_FORMAT = "json"
assert app.error_handler.fallback == "json"
def test_allow_fallback_error_format_in_config_injection():
class MyConfig(Config):
FALLBACK_ERROR_FORMAT = "text"
app = Sanic("test", config=MyConfig())
@app.route("/error", methods=["GET", "POST"])
def err(request):
raise Exception("something went wrong")
request, response = app.test_client.get("/error")
assert request.app.error_handler.fallback == "text"
assert response.status == 500
assert response.content_type == "text/plain; charset=utf-8"
def test_allow_fallback_error_format_in_config_replacement(app):
class MyConfig(Config):
FALLBACK_ERROR_FORMAT = "text"
app.config = MyConfig()
request, response = app.test_client.get("/error")
assert request.app.error_handler.fallback == "text"
assert response.status == 500
assert response.content_type == "text/plain; charset=utf-8"

View File

@ -4,7 +4,6 @@ import warnings
import pytest
from bs4 import BeautifulSoup
from websockets.version import version as websockets_version
from sanic import Sanic
from sanic.exceptions import (
@ -19,6 +18,16 @@ from sanic.exceptions import (
from sanic.response import text
def dl_to_dict(soup, css_class):
keys, values = [], []
for dl in soup.find_all("dl", {"class": css_class}):
for dt in dl.find_all("dt"):
keys.append(dt.text.strip())
for dd in dl.find_all("dd"):
values.append(dd.text.strip())
return dict(zip(keys, values))
class SanicExceptionTestException(Exception):
pass
@ -261,14 +270,114 @@ def test_exception_in_ws_logged(caplog):
with caplog.at_level(logging.INFO):
app.test_client.websocket("/feed")
# Websockets v10.0 and above output an additional
# INFO message when a ws connection is accepted
ws_version_parts = websockets_version.split(".")
ws_major = int(ws_version_parts[0])
record_index = 2 if ws_major >= 10 else 1
assert caplog.record_tuples[record_index][0] == "sanic.error"
assert caplog.record_tuples[record_index][1] == logging.ERROR
assert (
"Exception occurred while handling uri:"
in caplog.record_tuples[record_index][2]
)
error_logs = [r for r in caplog.record_tuples if r[0] == "sanic.error"]
assert error_logs[1][1] == logging.ERROR
assert "Exception occurred while handling uri:" in error_logs[1][2]
@pytest.mark.parametrize("debug", (True, False))
def test_contextual_exception_context(debug):
app = Sanic(__name__)
class TeapotError(SanicException):
status_code = 418
message = "Sorry, I cannot brew coffee"
def fail():
raise TeapotError(context={"foo": "bar"})
app.post("/coffee/json", error_format="json")(lambda _: fail())
app.post("/coffee/html", error_format="html")(lambda _: fail())
app.post("/coffee/text", error_format="text")(lambda _: fail())
_, response = app.test_client.post("/coffee/json", debug=debug)
assert response.status == 418
assert response.json["message"] == "Sorry, I cannot brew coffee"
assert response.json["context"] == {"foo": "bar"}
_, response = app.test_client.post("/coffee/html", debug=debug)
soup = BeautifulSoup(response.body, "html.parser")
dl = dl_to_dict(soup, "context")
assert response.status == 418
assert "Sorry, I cannot brew coffee" in soup.find("p").text
assert dl == {"foo": "bar"}
_, response = app.test_client.post("/coffee/text", debug=debug)
lines = list(map(lambda x: x.decode(), response.body.split(b"\n")))
idx = lines.index("Context") + 1
assert response.status == 418
assert lines[2] == "Sorry, I cannot brew coffee"
assert lines[idx] == ' foo: "bar"'
@pytest.mark.parametrize("debug", (True, False))
def test_contextual_exception_extra(debug):
app = Sanic(__name__)
class TeapotError(SanicException):
status_code = 418
@property
def message(self):
return f"Found {self.extra['foo']}"
def fail():
raise TeapotError(extra={"foo": "bar"})
app.post("/coffee/json", error_format="json")(lambda _: fail())
app.post("/coffee/html", error_format="html")(lambda _: fail())
app.post("/coffee/text", error_format="text")(lambda _: fail())
_, response = app.test_client.post("/coffee/json", debug=debug)
assert response.status == 418
assert response.json["message"] == "Found bar"
if debug:
assert response.json["extra"] == {"foo": "bar"}
else:
assert "extra" not in response.json
_, response = app.test_client.post("/coffee/html", debug=debug)
soup = BeautifulSoup(response.body, "html.parser")
dl = dl_to_dict(soup, "extra")
assert response.status == 418
assert "Found bar" in soup.find("p").text
if debug:
assert dl == {"foo": "bar"}
else:
assert not dl
_, response = app.test_client.post("/coffee/text", debug=debug)
lines = list(map(lambda x: x.decode(), response.body.split(b"\n")))
assert response.status == 418
assert lines[2] == "Found bar"
if debug:
idx = lines.index("Extra") + 1
assert lines[idx] == ' foo: "bar"'
else:
assert "Extra" not in lines
@pytest.mark.parametrize("override", (True, False))
def test_contextual_exception_functional_message(override):
app = Sanic(__name__)
class TeapotError(SanicException):
status_code = 418
@property
def message(self):
return f"Received foo={self.context['foo']}"
@app.post("/coffee", error_format="json")
async def make_coffee(_):
error_args = {"context": {"foo": "bar"}}
if override:
error_args["message"] = "override"
raise TeapotError(**error_args)
_, response = app.test_client.post("/coffee", debug=True)
error_message = "override" if override else "Received foo=bar"
assert response.status == 418
assert response.json["message"] == error_message
assert response.json["context"] == {"foo": "bar"}

View File

@ -1,13 +1,18 @@
import asyncio
import logging
from typing import Callable, List
from unittest.mock import Mock
import pytest
from bs4 import BeautifulSoup
from pytest import LogCaptureFixture, MonkeyPatch
from sanic import Sanic
from sanic import Sanic, handlers
from sanic.exceptions import Forbidden, InvalidUsage, NotFound, ServerError
from sanic.handlers import ErrorHandler
from sanic.request import Request
from sanic.response import stream, text
@ -88,35 +93,35 @@ def exception_handler_app():
return exception_handler_app
def test_invalid_usage_exception_handler(exception_handler_app):
def test_invalid_usage_exception_handler(exception_handler_app: Sanic):
request, response = exception_handler_app.test_client.get("/1")
assert response.status == 400
def test_server_error_exception_handler(exception_handler_app):
def test_server_error_exception_handler(exception_handler_app: Sanic):
request, response = exception_handler_app.test_client.get("/2")
assert response.status == 200
assert response.text == "OK"
def test_not_found_exception_handler(exception_handler_app):
def test_not_found_exception_handler(exception_handler_app: Sanic):
request, response = exception_handler_app.test_client.get("/3")
assert response.status == 200
def test_text_exception__handler(exception_handler_app):
def test_text_exception__handler(exception_handler_app: Sanic):
request, response = exception_handler_app.test_client.get("/random")
assert response.status == 200
assert response.text == "Done."
def test_async_exception_handler(exception_handler_app):
def test_async_exception_handler(exception_handler_app: Sanic):
request, response = exception_handler_app.test_client.get("/7")
assert response.status == 200
assert response.text == "foo,bar"
def test_html_traceback_output_in_debug_mode(exception_handler_app):
def test_html_traceback_output_in_debug_mode(exception_handler_app: Sanic):
request, response = exception_handler_app.test_client.get("/4", debug=True)
assert response.status == 500
soup = BeautifulSoup(response.body, "html.parser")
@ -131,12 +136,12 @@ def test_html_traceback_output_in_debug_mode(exception_handler_app):
) == summary_text
def test_inherited_exception_handler(exception_handler_app):
def test_inherited_exception_handler(exception_handler_app: Sanic):
request, response = exception_handler_app.test_client.get("/5")
assert response.status == 200
def test_chained_exception_handler(exception_handler_app):
def test_chained_exception_handler(exception_handler_app: Sanic):
request, response = exception_handler_app.test_client.get(
"/6/0", debug=True
)
@ -155,7 +160,7 @@ def test_chained_exception_handler(exception_handler_app):
) == summary_text
def test_exception_handler_lookup(exception_handler_app):
def test_exception_handler_lookup(exception_handler_app: Sanic):
class CustomError(Exception):
pass
@ -203,27 +208,92 @@ def test_exception_handler_lookup(exception_handler_app):
)
def test_exception_handler_processed_request_middleware(exception_handler_app):
def test_exception_handler_processed_request_middleware(
exception_handler_app: Sanic,
):
request, response = exception_handler_app.test_client.get("/8")
assert response.status == 200
assert response.text == "Done."
def test_single_arg_exception_handler_notice(exception_handler_app, caplog):
def test_single_arg_exception_handler_notice(
exception_handler_app: Sanic, caplog: LogCaptureFixture
):
class CustomErrorHandler(ErrorHandler):
def lookup(self, exception):
return super().lookup(exception, None)
exception_handler_app.error_handler = CustomErrorHandler()
with caplog.at_level(logging.WARNING):
_, response = exception_handler_app.test_client.get("/1")
assert caplog.records[0].message == (
message = (
"You are using a deprecated error handler. The lookup method should "
"accept two positional parameters: (exception, route_name: "
"Optional[str]). Until you upgrade your ErrorHandler.lookup, "
"Blueprint specific exceptions will not work properly. Beginning in "
"v22.3, the legacy style lookup method will not work at all."
)
with pytest.warns(DeprecationWarning) as record:
_, response = exception_handler_app.test_client.get("/1")
assert len(record) == 1
assert record[0].message.args[0] == message
assert response.status == 400
def test_error_handler_noisy_log(
exception_handler_app: Sanic, monkeypatch: MonkeyPatch
):
err_logger = Mock()
monkeypatch.setattr(handlers, "error_logger", err_logger)
exception_handler_app.config["NOISY_EXCEPTIONS"] = False
exception_handler_app.test_client.get("/1")
err_logger.exception.assert_not_called()
exception_handler_app.config["NOISY_EXCEPTIONS"] = True
request, _ = exception_handler_app.test_client.get("/1")
err_logger.exception.assert_called_with(
"Exception occurred while handling uri: %s", repr(request.url)
)
def test_exception_handler_response_was_sent(
app: Sanic,
caplog: LogCaptureFixture,
message_in_records: Callable[[List[logging.LogRecord], str], bool],
):
exception_handler_ran = False
@app.exception(ServerError)
async def exception_handler(request, exception):
nonlocal exception_handler_ran
exception_handler_ran = True
return text("Error")
@app.route("/1")
async def handler1(request: Request):
response = await request.respond()
await response.send("some text")
raise ServerError("Exception")
@app.route("/2")
async def handler2(request: Request):
response = await request.respond()
raise ServerError("Exception")
with caplog.at_level(logging.WARNING):
_, response = app.test_client.get("/1")
assert "some text" in response.text
# Change to assert warning not in the records in the future version.
message_in_records(
caplog.records,
(
"An error occurred while handling the request after at "
"least some part of the response was sent to the client. "
"Therefore, the response from your custom exception "
),
)
_, response = app.test_client.get("/2")
assert "Error" in response.text

View File

@ -38,9 +38,9 @@ def test_no_exceptions_when_cancel_pending_request(app, caplog):
counter = Counter([r[1] for r in caplog.record_tuples])
assert counter[logging.INFO] == 5
assert counter[logging.INFO] == 11
assert logging.ERROR not in counter
assert (
caplog.record_tuples[3][2]
caplog.record_tuples[9][2]
== "Request: GET http://127.0.0.1:8000/ stopped. Transport is closed."
)

View File

@ -1,6 +1,4 @@
import logging
import os
import sys
import uuid
from importlib import reload
@ -9,12 +7,9 @@ from unittest.mock import Mock
import pytest
from sanic_testing.testing import SanicTestClient
import sanic
from sanic import Sanic
from sanic.compat import OS_IS_WINDOWS
from sanic.log import LOGGING_CONFIG_DEFAULTS, logger
from sanic.response import text
@ -155,56 +150,6 @@ async def test_logger(caplog):
assert record in caplog.record_tuples
@pytest.mark.skipif(
OS_IS_WINDOWS and sys.version_info >= (3, 8),
reason="Not testable with current client",
)
def test_logger_static_and_secure(caplog):
# Same as test_logger, except for more coverage:
# - test_client initialised separately for static port
# - using ssl
rand_string = str(uuid.uuid4())
app = Sanic(name=__name__)
@app.get("/")
def log_info(request):
logger.info(rand_string)
return text("hello")
current_dir = os.path.dirname(os.path.realpath(__file__))
ssl_cert = os.path.join(current_dir, "certs/selfsigned.cert")
ssl_key = os.path.join(current_dir, "certs/selfsigned.key")
ssl_dict = {"cert": ssl_cert, "key": ssl_key}
test_client = SanicTestClient(app, port=42101)
with caplog.at_level(logging.INFO):
request, response = test_client.get(
f"https://127.0.0.1:{test_client.port}/",
server_kwargs=dict(ssl=ssl_dict),
)
port = test_client.port
assert caplog.record_tuples[0] == (
"sanic.root",
logging.INFO,
f"Goin' Fast @ https://127.0.0.1:{port}",
)
assert caplog.record_tuples[1] == (
"sanic.root",
logging.INFO,
f"https://127.0.0.1:{port}/",
)
assert caplog.record_tuples[2] == ("sanic.root", logging.INFO, rand_string)
assert caplog.record_tuples[-1] == (
"sanic.root",
logging.INFO,
"Server Stopped",
)
def test_logging_modified_root_logger_config():
# reset_logging()

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