diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 18415b68..5108c247 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -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 diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 56a98398..c478a961 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -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: diff --git a/.github/workflows/pr-bandit.yml b/.github/workflows/pr-bandit.yml index c90514e8..ca91312a 100644 --- a/.github/workflows/pr-bandit.yml +++ b/.github/workflows/pr-bandit.yml @@ -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: diff --git a/.github/workflows/pr-docs.yml b/.github/workflows/pr-docs.yml index 1a6871c2..7b3c2f6e 100644 --- a/.github/workflows/pr-docs.yml +++ b/.github/workflows/pr-docs.yml @@ -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: diff --git a/.github/workflows/pr-linter.yml b/.github/workflows/pr-linter.yml index 6165a988..9ed45d0a 100644 --- a/.github/workflows/pr-linter.yml +++ b/.github/workflows/pr-linter.yml @@ -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: diff --git a/.github/workflows/pr-python310.yml b/.github/workflows/pr-python310.yml index 713f6495..f3f7c607 100644 --- a/.github/workflows/pr-python310.yml +++ b/.github/workflows/pr-python310.yml @@ -3,15 +3,11 @@ on: pull_request: branches: - main - push: - branches: - - main - paths: - - sanic/* - - tests/* + types: [opened, synchronize, reopened, ready_for_review] jobs: - testPy39: + testPy310: + if: github.event.pull_request.draft == false name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: diff --git a/.github/workflows/pr-python37.yml b/.github/workflows/pr-python37.yml index 485f2592..50f79c6e 100644 --- a/.github/workflows/pr-python37.yml +++ b/.github/workflows/pr-python37.yml @@ -3,15 +3,11 @@ 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: diff --git a/.github/workflows/pr-python38.yml b/.github/workflows/pr-python38.yml index 8c46b86b..1e0b8050 100644 --- a/.github/workflows/pr-python38.yml +++ b/.github/workflows/pr-python38.yml @@ -3,15 +3,11 @@ 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: diff --git a/.github/workflows/pr-python39.yml b/.github/workflows/pr-python39.yml index cdccf0d2..1abd6bcb 100644 --- a/.github/workflows/pr-python39.yml +++ b/.github/workflows/pr-python39.yml @@ -3,15 +3,11 @@ 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: diff --git a/.github/workflows/pr-type-check.yml b/.github/workflows/pr-type-check.yml index 80d8d9fa..2fae03be 100644 --- a/.github/workflows/pr-type-check.yml +++ b/.github/workflows/pr-type-check.yml @@ -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: diff --git a/.github/workflows/pr-windows.yml b/.github/workflows/pr-windows.yml index 73e29d4f..9721b5b5 100644 --- a/.github/workflows/pr-windows.yml +++ b/.github/workflows/pr-windows.yml @@ -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: diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a9940da1..5f09fd51 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -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) diff --git a/README.rst b/README.rst index c6616f16..2a11ba8d 100644 --- a/README.rst +++ b/README.rst @@ -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 diff --git a/examples/add_task_sanic.py b/examples/add_task_sanic.py index 52b4e6bb..ece26433 100644 --- a/examples/add_task_sanic.py +++ b/examples/add_task_sanic.py @@ -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()) diff --git a/examples/amending_request_object.py b/examples/amending_request_object.py index 55d889f7..366dd67d 100644 --- a/examples/amending_request_object.py +++ b/examples/amending_request_object.py @@ -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) diff --git a/examples/authorized_sanic.py b/examples/authorized_sanic.py index 7b5b7501..33e54a4b 100644 --- a/examples/authorized_sanic.py +++ b/examples/authorized_sanic.py @@ -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) diff --git a/examples/blueprint_middlware_execution_order.py b/examples/blueprint_middlware_execution_order.py index 38fc4cb1..e179c36d 100644 --- a/examples/blueprint_middlware_execution_order.py +++ b/examples/blueprint_middlware_execution_order.py @@ -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 + + +""" +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) diff --git a/examples/blueprints.py b/examples/blueprints.py index 643093f6..62340a0d 100644 --- a/examples/blueprints.py +++ b/examples/blueprints.py @@ -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") diff --git a/examples/delayed_response.py b/examples/delayed_response.py index 4105edba..5923d10a 100644 --- a/examples/delayed_response.py +++ b/examples/delayed_response.py @@ -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/") 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) diff --git a/examples/exception_monitoring.py b/examples/exception_monitoring.py index 02a13e7d..3d853d32 100644 --- a/examples/exception_monitoring.py +++ b/examples/exception_monitoring.py @@ -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) diff --git a/examples/simple_server.py b/examples/hello_world.py similarity index 100% rename from examples/simple_server.py rename to examples/hello_world.py diff --git a/examples/http_redirect.py b/examples/http_redirect.py index 2e38eb92..50a79d81 100644 --- a/examples/http_redirect.py +++ b/examples/http_redirect.py @@ -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) diff --git a/examples/limit_concurrency.py b/examples/limit_concurrency.py index f6b4b01a..429a312b 100644 --- a/examples/limit_concurrency.py +++ b/examples/limit_concurrency.py @@ -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) diff --git a/examples/log_request_id.py b/examples/log_request_id.py index 27d987bc..c0d2d6f9 100644 --- a/examples/log_request_id.py +++ b/examples/log_request_id.py @@ -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__": diff --git a/examples/logdna_example.py b/examples/logdna_example.py index da38f404..01236d98 100644 --- a/examples/logdna_example.py +++ b/examples/logdna_example.py @@ -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)) diff --git a/examples/run_asgi.py b/examples/run_asgi.py index d4351c17..c29c5fbb 100644 --- a/examples/run_asgi.py +++ b/examples/run_asgi.py @@ -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") diff --git a/examples/run_async.py b/examples/run_async.py index c35da8b1..a30417d7 100644 --- a/examples/run_async.py +++ b/examples/run_async.py @@ -1,9 +1,12 @@ -from sanic import Sanic -from sanic import response -from signal import signal, SIGINT import asyncio + +from signal import SIGINT, signal + import uvloop +from sanic import Sanic, response + + app = Sanic(__name__) @@ -11,12 +14,18 @@ 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) +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) +server = loop.run_until_complete(task) +loop.run_until_complete(server.startup()) signal(SIGINT, lambda s, f: loop.stop()) + try: loop.run_forever() -except: +finally: loop.stop() diff --git a/examples/run_async_advanced.py b/examples/run_async_advanced.py index 27f86f3f..7ea30dd7 100644 --- a/examples/run_async_advanced.py +++ b/examples/run_async_advanced.py @@ -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()) diff --git a/examples/simple_async_view.py b/examples/simple_async_view.py index 990aa21a..4e73967c 100644 --- a/examples/simple_async_view.py +++ b/examples/simple_async_view.py @@ -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) diff --git a/examples/try_everything.py b/examples/try_everything.py index a775704d..8e4a8e09 100644 --- a/examples/try_everything.py +++ b/examples/try_everything.py @@ -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) diff --git a/examples/unix_socket.py b/examples/unix_socket.py index 08e89445..a64b205d 100644 --- a/examples/unix_socket.py +++ b/examples/unix_socket.py @@ -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) diff --git a/examples/url_for_example.py b/examples/url_for_example.py index cb895b0c..f0d3614b 100644 --- a/examples/url_for_example.py +++ b/examples/url_for_example.py @@ -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/') +@app.route("/posts/") async def post_handler(request, post_id): - return response.text('Post - {}'.format(post_id)) - -if __name__ == '__main__': + return response.text("Post - {}".format(post_id)) + + +if __name__ == "__main__": app.run(host="0.0.0.0", port=8000, debug=True) diff --git a/examples/versioned_blueprint_group.py b/examples/versioned_blueprint_group.py index 77360f5d..56715acc 100644 --- a/examples/versioned_blueprint_group.py +++ b/examples/versioned_blueprint_group.py @@ -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) diff --git a/examples/websocket.py b/examples/websocket.py index 92f71375..7bcd2cd1 100644 --- a/examples/websocket.py +++ b/examples/websocket.py @@ -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) - diff --git a/sanic/app.py b/sanic/app.py index 88ae1c6e..da54d5dd 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -337,7 +337,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): """ 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 @@ -775,6 +775,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( diff --git a/sanic/application/motd.py b/sanic/application/motd.py index 27c36663..32825b12 100644 --- a/sanic/application/motd.py +++ b/sanic/application/motd.py @@ -64,8 +64,10 @@ class MOTDTTY(MOTD): self.set_variables() def set_variables(self): # no cov - fallback = (80, 24) - terminal_width = min(get_terminal_size(fallback=fallback).columns, 108) + 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 diff --git a/sanic/blueprints.py b/sanic/blueprints.py index 290773fa..6a6c2e82 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -79,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 */* @@ -112,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", diff --git a/sanic/config.py b/sanic/config.py index e08b3f60..3b6cf8f8 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -176,11 +176,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`` @@ -188,19 +188,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]): """ diff --git a/sanic/headers.py b/sanic/headers.py index dbb8720f..b744974c 100644 --- a/sanic/headers.py +++ b/sanic/headers.py @@ -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 diff --git a/sanic/helpers.py b/sanic/helpers.py index 87d51b53..c5c10ccc 100644 --- a/sanic/helpers.py +++ b/sanic/helpers.py @@ -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 diff --git a/sanic/mixins/routes.py b/sanic/mixins/routes.py index 7139cd3c..01911e66 100644 --- a/sanic/mixins/routes.py +++ b/sanic/mixins/routes.py @@ -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, diff --git a/sanic/models/futures.py b/sanic/models/futures.py index 74ee92b9..21f9c674 100644 --- a/sanic/models/futures.py +++ b/sanic/models/futures.py @@ -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] diff --git a/sanic/response.py b/sanic/response.py index 1f1d7fbe..1da4486a 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -101,7 +101,7 @@ class BaseHTTPResponse: async def send( self, - data: Optional[Union[AnyStr]] = None, + data: Optional[AnyStr] = None, end_stream: Optional[bool] = None, ) -> None: """ diff --git a/sanic/router.py b/sanic/router.py index b15c2a3e..bad471c6 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -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 diff --git a/sanic/tls.py b/sanic/tls.py index e0f9151a..be30f4a2 100644 --- a/sanic/tls.py +++ b/sanic/tls.py @@ -175,7 +175,7 @@ def match_hostname( def selector_sni_callback( sslobj: ssl.SSLObject, server_name: str, ctx: CertSelector ) -> Optional[int]: - """Select a certificate mathing the SNI.""" + """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 diff --git a/sanic/utils.py b/sanic/utils.py index ef91ec9d..51d94d08 100644 --- a/sanic/utils.py +++ b/sanic/utils.py @@ -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: diff --git a/scripts/release.py b/scripts/release.py index 488ebe2b..e2b9b887 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -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( diff --git a/setup.py b/setup.py index 3bc11f8e..36de0c4f 100644 --- a/setup.py +++ b/setup.py @@ -121,6 +121,7 @@ docs_require = [ "docutils", "pygments", "m2r2", + "mistune<2.0.0", ] dev_require = tests_require + [ diff --git a/tests/test_request_data.py b/tests/test_request_data.py index f5bfabda..a1b78e95 100644 --- a/tests/test_request_data.py +++ b/tests/test_request_data.py @@ -17,7 +17,7 @@ def test_custom_context(app): @app.route("/") def handler(request): - # Accessing non-existant key should fail with AttributeError + # Accessing non-existent key should fail with AttributeError try: invalid = request.ctx.missing except AttributeError as e: diff --git a/tests/test_worker.py b/tests/test_worker.py index 1fec3b54..cdc30a05 100644 --- a/tests/test_worker.py +++ b/tests/test_worker.py @@ -21,7 +21,7 @@ def gunicorn_worker(): "gunicorn " f"--bind 127.0.0.1:{PORT} " "--worker-class sanic.worker.GunicornWorker " - "examples.simple_server:app" + "examples.hello_world:app" ) worker = subprocess.Popen(shlex.split(command)) time.sleep(2) @@ -35,7 +35,7 @@ def gunicorn_worker_with_access_logs(): "gunicorn " f"--bind 127.0.0.1:{PORT + 1} " "--worker-class sanic.worker.GunicornWorker " - "examples.simple_server:app" + "examples.hello_world:app" ) worker = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE) time.sleep(2) @@ -50,7 +50,7 @@ def gunicorn_worker_with_env_var(): f"--bind 127.0.0.1:{PORT + 2} " "--worker-class sanic.worker.GunicornWorker " "--log-level info " - "examples.simple_server:app" + "examples.hello_world:app" ) worker = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE) time.sleep(2)