diff --git a/.appveyor.yml b/.appveyor.yml index 368270c5..afc50f13 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -2,11 +2,6 @@ version: "{branch}.{build}" environment: matrix: - - TOXENV: py35-no-ext - PYTHON: "C:\\Python35-x64" - PYTHON_VERSION: "3.5.x" - PYTHON_ARCH: "64" - - TOXENV: py36-no-ext PYTHON: "C:\\Python36-x64" PYTHON_VERSION: "3.6.x" diff --git a/pyproject.toml b/.black.toml similarity index 100% rename from pyproject.toml rename to .black.toml diff --git a/.travis.yml b/.travis.yml index e40e3124..a2a0a128 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,10 +5,6 @@ cache: - $HOME/.cache/pip matrix: include: - - env: TOX_ENV=py35 - python: 3.5 - - env: TOX_ENV=py35-no-ext - python: 3.5 - env: TOX_ENV=py36 python: 3.6 - env: TOX_ENV=py36-no-ext diff --git a/CHANGELOG.md b/CHANGELOG.md index dd866835..ea90d8bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +Version 19.6 +------------ +19.6.0 + - Changes: + - Remove `aiohttp` dependencey and create new `SanicTestClient` based upon + [`requests-async`](https://github.com/encode/requests-async). + + - Deprecation: + - Support for Python 3.5 + +Note: Sanic will not support Python 3.5 from version 19.6 and forward. However, +version 18.12LTS will have its support period extended thru December 2020, and +therefore passing Python's official support version 3.5, which is set to expire +in September 2020. + Version 19.3 ------------- 19.3.1 diff --git a/LICENSE b/LICENSE index 74ee7987..35740e3d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2016-present Channel Cat +Copyright (c) 2016-present Sanic Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile index de806fd6..a95326b2 100644 --- a/Makefile +++ b/Makefile @@ -47,12 +47,12 @@ ifdef include_tests isort -rc sanic tests else $(info Sorting Imports) - isort -rc sanic + isort -rc sanic tests endif endif black: - black --config ./pyproject.toml sanic tests + black --config ./.black.toml sanic tests fix-import: black - isort -rc sanic + isort -rc sanic tests diff --git a/README.rst b/README.rst index 20dbaa1b..f63d9707 100644 --- a/README.rst +++ b/README.rst @@ -17,6 +17,8 @@ Sanic | Build fast. Run fast. - | |PyPI| |PyPI version| |Wheel| |Supported implementations| |Code style black| * - Support - | |Forums| |Join the chat at https://gitter.im/sanic-python/Lobby| + * - Stats + - | |Downloads| .. |Forums| image:: https://img.shields.io/badge/forums-community-ff0068.svg :target: https://community.sanicframework.org/ @@ -42,10 +44,13 @@ Sanic | Build fast. Run fast. .. |Supported implementations| image:: https://img.shields.io/pypi/implementation/sanic.svg :alt: Supported implementations :target: https://pypi.python.org/pypi/sanic +.. |Downloads| image:: https://pepy.tech/badge/sanic/month + :alt: Downloads + :target: https://pepy.tech/project/sanic .. end-badges -Sanic is a Python web server and web framework that's written to go fast. It allows the usage of the ``async/await`` syntax added in Python 3.5, which makes your code non-blocking and speedy. +Sanic is a **Python 3.6+** web server and web framework that's written to go fast. It allows the usage of the ``async/await`` syntax added in Python 3.5, which makes your code non-blocking and speedy. `Source code on GitHub `_ | `Help and discussion board `_. diff --git a/docs/sanic/changelog.md b/docs/sanic/changelog.md index dd866835..ea90d8bb 100644 --- a/docs/sanic/changelog.md +++ b/docs/sanic/changelog.md @@ -1,3 +1,18 @@ +Version 19.6 +------------ +19.6.0 + - Changes: + - Remove `aiohttp` dependencey and create new `SanicTestClient` based upon + [`requests-async`](https://github.com/encode/requests-async). + + - Deprecation: + - Support for Python 3.5 + +Note: Sanic will not support Python 3.5 from version 19.6 and forward. However, +version 18.12LTS will have its support period extended thru December 2020, and +therefore passing Python's official support version 3.5, which is set to expire +in September 2020. + Version 19.3 ------------- 19.3.1 diff --git a/docs/sanic/testing.md b/docs/sanic/testing.md index 28caac12..64cdef4f 100644 --- a/docs/sanic/testing.md +++ b/docs/sanic/testing.md @@ -1,8 +1,8 @@ # Testing Sanic endpoints can be tested locally using the `test_client` object, which -depends on the additional [aiohttp](https://aiohttp.readthedocs.io/en/stable/) -library. +depends on the additional [`requests-async`](https://github.com/encode/requests-async) +library, which implements an API that mirrors the `requests` library. The `test_client` exposes `get`, `post`, `put`, `delete`, `patch`, `head` and `options` methods for you to run against your application. A simple example (using pytest) is like follows: @@ -21,7 +21,7 @@ def test_index_put_not_allowed(): ``` Internally, each time you call one of the `test_client` methods, the Sanic app is run at `127.0.0.1:42101` and -your test request is executed against your application, using `aiohttp`. +your test request is executed against your application, using `requests-async`. The `test_client` methods accept the following arguments and keyword arguments: @@ -33,7 +33,7 @@ The `test_client` methods accept the following arguments and keyword arguments: - `server_kwargs` *(default `{}`) a dict of additional arguments to pass into `app.run` before the test request is run. - `debug` *(default `False`)* A boolean which determines whether to run the server in debug mode. -The function further takes the `*request_args` and `**request_kwargs`, which are passed directly to the aiohttp ClientSession request. +The function further takes the `*request_args` and `**request_kwargs`, which are passed directly to the request. For example, to supply data to a GET request, you would do the following: @@ -55,8 +55,8 @@ def test_post_json_request_includes_data(): More information about -the available arguments to aiohttp can be found -[in the documentation for ClientSession](https://aiohttp.readthedocs.io/en/stable/client_reference.html#client-session). +the available arguments to `requests-async` can be found +[in the documentation for `requests`](https://2.python-requests.org/en/master/). ## Using a random port diff --git a/sanic/testing.py b/sanic/testing.py index 2ed52bbb..7aab4736 100644 --- a/sanic/testing.py +++ b/sanic/testing.py @@ -1,6 +1,9 @@ from json import JSONDecodeError from socket import socket +import requests_async as requests +import websockets + from sanic.exceptions import MethodNotSupported from sanic.log import logger from sanic.response import text @@ -16,32 +19,41 @@ class SanicTestClient: self.app = app self.port = port - async def _local_request(self, method, url, cookies=None, *args, **kwargs): - import aiohttp + def get_new_session(self): + return requests.Session() + async def _local_request(self, method, url, *args, **kwargs): logger.info(url) - conn = aiohttp.TCPConnector(ssl=False) - async with aiohttp.ClientSession( - cookies=cookies, connector=conn - ) as session: - async with getattr(session, method.lower())( - url, *args, **kwargs - ) as response: - try: - response.text = await response.text() - except UnicodeDecodeError: - response.text = None + raw_cookies = kwargs.pop("raw_cookies", None) + + if method == "websocket": + async with websockets.connect(url, *args, **kwargs) as websocket: + websocket.opened = websocket.open + return websocket + else: + async with self.get_new_session() as session: try: - response.json = await response.json() - except ( - JSONDecodeError, - UnicodeDecodeError, - aiohttp.ClientResponseError, - ): + response = await getattr(session, method.lower())( + url, verify=False, *args, **kwargs + ) + except NameError: + raise Exception(response.status_code) + + try: + response.json = response.json() + except (JSONDecodeError, UnicodeDecodeError): response.json = None response.body = await response.read() + response.status = response.status_code + response.content_type = response.headers.get("content-type") + + if raw_cookies: + response.raw_cookies = {} + for cookie in response.cookies: + response.raw_cookies[cookie.name] = cookie + return response def _sanic_endpoint_test( @@ -83,11 +95,15 @@ class SanicTestClient: server_kwargs = dict(sock=sock, **server_kwargs) host, port = sock.getsockname() - if uri.startswith(("http:", "https:", "ftp:", "ftps://", "//")): + if uri.startswith( + ("http:", "https:", "ftp:", "ftps://", "//", "ws:", "wss:") + ): url = uri else: - url = "http://{host}:{port}{uri}".format( - host=host, port=port, uri=uri + uri = uri if uri.startswith("/") else "/{uri}".format(uri=uri) + scheme = "ws" if method == "websocket" else "http" + url = "{scheme}://{host}:{port}{uri}".format( + scheme=scheme, host=host, port=port, uri=uri ) @self.app.listener("after_server_start") @@ -146,3 +162,6 @@ class SanicTestClient: def head(self, *args, **kwargs): return self._sanic_endpoint_test("head", *args, **kwargs) + + def websocket(self, *args, **kwargs): + return self._sanic_endpoint_test("websocket", *args, **kwargs) diff --git a/setup.cfg b/setup.cfg index b3572c85..ae329e77 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,7 +14,7 @@ multi_line_output = 3 not_skip = __init__.py [version] -current_version = 0.8.3 +current_version = 19.3.1 file = sanic/__init__.py current_version_pattern = __version__ = "{current_version}" new_version_pattern = __version__ = "{new_version}" diff --git a/setup.py b/setup.py index 4a682151..2cd973b7 100644 --- a/setup.py +++ b/setup.py @@ -50,12 +50,12 @@ with open_local(["README.rst"]) as rm: setup_kwargs = { "name": "sanic", "version": version, - "url": "http://github.com/channelcat/sanic/", + "url": "http://github.com/huge-success/sanic/", "license": "MIT", - "author": "Channel Cat", - "author_email": "channelcat@gmail.com", + "author": "Sanic Community", + "author_email": "admhpkns@gmail.com", "description": ( - "A microframework based on uvloop, httptools, and learnings of flask" + "A web server and web framework that's written to go fast. Build fast. Run fast." ), "long_description": long_description, "packages": ["sanic"], @@ -64,7 +64,6 @@ setup_kwargs = { "Development Status :: 4 - Beta", "Environment :: Web Environment", "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", ], @@ -90,7 +89,8 @@ tests_require = [ "multidict>=4.0,<5.0", "gunicorn", "pytest-cov", - "aiohttp>=2.3.0,<=3.2.1", + "httpcore==0.1.1", + "requests-async==0.4.0", "beautifulsoup4", uvloop, ujson, @@ -119,7 +119,7 @@ extras_require = { "recommonmark", "sphinxcontrib-asyncio", "docutils", - "pygments" + "pygments", ], } diff --git a/tests/benchmark/test_route_resolution_benchmark.py b/tests/benchmark/test_route_resolution_benchmark.py index d0df69a1..d9354c4b 100644 --- a/tests/benchmark/test_route_resolution_benchmark.py +++ b/tests/benchmark/test_route_resolution_benchmark.py @@ -1,8 +1,10 @@ from random import choice, seed + from pytest import mark import sanic.router + seed("Pack my box with five dozen liquor jugs.") # Disable Caching for testing purpose diff --git a/tests/conftest.py b/tests/conftest.py index 1748b46c..d720f3be 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,6 +9,7 @@ import pytest from sanic import Sanic from sanic.router import RouteExists, Router + random.seed("Pack my box with five dozen liquor jugs.") if sys.platform in ["win32", "cygwin"]: diff --git a/tests/performance/aiohttp/simple_server.py b/tests/performance/aiohttp/simple_server.py index c781f070..9a57f459 100644 --- a/tests/performance/aiohttp/simple_server.py +++ b/tests/performance/aiohttp/simple_server.py @@ -1,10 +1,13 @@ # Run with python3 simple_server.py PORT -from aiohttp import web import asyncio import sys -import uvloop + import ujson as json +import uvloop + +from aiohttp import web + loop = uvloop.new_event_loop() asyncio.set_event_loop(loop) diff --git a/tests/performance/bottle/simple_server.py b/tests/performance/bottle/simple_server.py index 58605fae..43a8f019 100644 --- a/tests/performance/bottle/simple_server.py +++ b/tests/performance/bottle/simple_server.py @@ -1,8 +1,9 @@ # Run with: gunicorn --workers=1 --worker-class=meinheld.gmeinheld.MeinheldWorker -b :8000 simple_server:app import bottle -from bottle import route, run import ujson +from bottle import route, run + @route("/") def index(): diff --git a/tests/performance/kyoukai/simple_server.py b/tests/performance/kyoukai/simple_server.py index 4b901978..9fd7d2b0 100644 --- a/tests/performance/kyoukai/simple_server.py +++ b/tests/performance/kyoukai/simple_server.py @@ -1,10 +1,13 @@ # Run with: python3 -O simple_server.py import asyncio -from kyoukai import Kyoukai, HTTPRequestContext import logging + import ujson import uvloop +from kyoukai import HTTPRequestContext, Kyoukai + + loop = uvloop.new_event_loop() asyncio.set_event_loop(loop) diff --git a/tests/performance/sanic/http_response.py b/tests/performance/sanic/http_response.py index 8d864f53..52596e5e 100644 --- a/tests/performance/sanic/http_response.py +++ b/tests/performance/sanic/http_response.py @@ -1,16 +1,18 @@ -import asyncpg -import sys -import os import inspect +import os +import sys +import timeit + +import asyncpg + +from sanic.response import json + currentdir = os.path.dirname( os.path.abspath(inspect.getfile(inspect.currentframe())) ) sys.path.insert(0, currentdir + "/../../../") -import timeit - -from sanic.response import json print(json({"test": True}).output()) diff --git a/tests/performance/sanic/simple_server.py b/tests/performance/sanic/simple_server.py index 33d1e52a..60ebd819 100644 --- a/tests/performance/sanic/simple_server.py +++ b/tests/performance/sanic/simple_server.py @@ -1,14 +1,16 @@ -import sys -import os import inspect +import os +import sys + +from sanic import Sanic +from sanic.response import json + currentdir = os.path.dirname( os.path.abspath(inspect.getfile(inspect.currentframe())) ) sys.path.insert(0, currentdir + "/../../../") -from sanic import Sanic -from sanic.response import json app = Sanic("test") diff --git a/tests/performance/sanic/varied_server.py b/tests/performance/sanic/varied_server.py index af919693..9b3f16da 100644 --- a/tests/performance/sanic/varied_server.py +++ b/tests/performance/sanic/varied_server.py @@ -1,15 +1,17 @@ -import sys -import os import inspect +import os +import sys + +from sanic import Sanic +from sanic.exceptions import ServerError +from sanic.response import json, text + currentdir = os.path.dirname( os.path.abspath(inspect.getfile(inspect.currentframe())) ) sys.path.insert(0, currentdir + "/../../../") -from sanic import Sanic -from sanic.response import json, text -from sanic.exceptions import ServerError app = Sanic("test") @@ -56,8 +58,6 @@ def query_string(request): ) -import sys - app.run(host="0.0.0.0", port=sys.argv[1]) diff --git a/tests/performance/tornado/simple_server.py b/tests/performance/tornado/simple_server.py index a326eeaf..1e69a293 100644 --- a/tests/performance/tornado/simple_server.py +++ b/tests/performance/tornado/simple_server.py @@ -1,5 +1,6 @@ # Run with: python simple_server.py import ujson + from tornado import ioloop, web diff --git a/tests/performance/wheezy/simple_server.py b/tests/performance/wheezy/simple_server.py index cbeeee6a..70a6338a 100644 --- a/tests/performance/wheezy/simple_server.py +++ b/tests/performance/wheezy/simple_server.py @@ -2,15 +2,16 @@ """ Minimal helloworld application. """ -from wheezy.http import HTTPResponse -from wheezy.http import WSGIApplication +import ujson + +from wheezy.http import HTTPResponse, WSGIApplication from wheezy.http.response import json_response from wheezy.routing import url from wheezy.web.handlers import BaseHandler -from wheezy.web.middleware import bootstrap_defaults -from wheezy.web.middleware import path_routing_middleware_factory - -import ujson +from wheezy.web.middleware import ( + bootstrap_defaults, + path_routing_middleware_factory, +) class WelcomeHandler(BaseHandler): diff --git a/tests/test_app.py b/tests/test_app.py index 8d90641f..5ddae42d 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -3,6 +3,7 @@ import logging import sys from inspect import isawaitable + import pytest from sanic.exceptions import SanicException @@ -11,7 +12,7 @@ from sanic.response import text def uvloop_installed(): try: - import uvloop + import uvloop # noqa return True except ImportError: diff --git a/tests/test_blueprint_group.py b/tests/test_blueprint_group.py index fe1db7a3..32729a49 100644 --- a/tests/test_blueprint_group.py +++ b/tests/test_blueprint_group.py @@ -3,7 +3,8 @@ from pytest import raises from sanic.app import Sanic from sanic.blueprints import Blueprint from sanic.request import Request -from sanic.response import text, HTTPResponse +from sanic.response import HTTPResponse, text + MIDDLEWARE_INVOKE_COUNTER = {"request": 0, "response": 0} diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index 397216d0..f0a67bd7 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -7,9 +7,9 @@ import pytest from sanic.app import Sanic from sanic.blueprints import Blueprint from sanic.constants import HTTP_METHODS -from sanic.exceptions import NotFound, ServerError, InvalidUsage +from sanic.exceptions import InvalidUsage, NotFound, ServerError from sanic.request import Request -from sanic.response import text, json +from sanic.response import json, text from sanic.views import CompositionView @@ -467,16 +467,8 @@ def test_bp_shorthand(app): request, response = app.test_client.get("/delete") assert response.status == 405 - request, response = app.test_client.get( - "/ws/", - headers={ - "Upgrade": "websocket", - "Connection": "upgrade", - "Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==", - "Sec-WebSocket-Version": "13", - }, - ) - assert response.status == 101 + request, response = app.test_client.websocket("/ws/") + assert response.opened is True assert ev.is_set() @@ -595,14 +587,13 @@ def test_blueprint_middleware_with_args(app: Sanic): "/wa", headers={"content-type": "plain/text"} ) assert response.json.get("test") == "value" - d = {} @pytest.mark.parametrize("file_name", ["test.file"]) def test_static_blueprint_name(app: Sanic, static_file_directory, file_name): current_file = inspect.getfile(inspect.currentframe()) with open(current_file, "rb") as file: - current_file_contents = file.read() + file.read() bp = Blueprint(name="static", url_prefix="/static", strict_slashes=False) @@ -662,16 +653,8 @@ def test_websocket_route(app: Sanic): app.blueprint(bp) - _, response = app.test_client.get( - "/ws/test", - headers={ - "Upgrade": "websocket", - "Connection": "upgrade", - "Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==", - "Sec-WebSocket-Version": "13", - }, - ) - assert response.status == 101 + _, response = app.test_client.websocket("/ws/test") + assert response.opened is True assert event.is_set() diff --git a/tests/test_config.py b/tests/test_config.py index c2da4bdd..7b203311 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,12 +1,13 @@ +from contextlib import contextmanager from os import environ from pathlib import Path -from contextlib import contextmanager from tempfile import TemporaryDirectory from textwrap import dedent + import pytest from sanic import Sanic -from sanic.config import Config, DEFAULT_CONFIG +from sanic.config import DEFAULT_CONFIG, Config from sanic.exceptions import PyFileError diff --git a/tests/test_cookies.py b/tests/test_cookies.py index b573b845..a77fda2f 100644 --- a/tests/test_cookies.py +++ b/tests/test_cookies.py @@ -1,8 +1,11 @@ from datetime import datetime, timedelta from http.cookies import SimpleCookie -from sanic.response import text + import pytest -from sanic.cookies import Cookie, DEFAULT_MAX_AGE + +from sanic.cookies import Cookie +from sanic.response import text + # ------------------------------------------------------------ # # GET @@ -100,7 +103,7 @@ def test_cookie_deletion(app): assert int(response_cookies["i_want_to_die"]["max-age"]) == 0 with pytest.raises(KeyError): - _ = response.cookies["i_never_existed"] + response.cookies["i_never_existed"] def test_cookie_reserved_cookie(): @@ -135,7 +138,7 @@ def test_cookie_set_same_key(app): request, response = app.test_client.get("/", cookies=cookies) assert response.status == 200 - assert response.cookies["test"].value == "pass" + assert response.cookies["test"] == "pass" @pytest.mark.parametrize("max_age", ["0", 30, 30.0, 30.1, "30", "test"]) @@ -149,19 +152,42 @@ def test_cookie_max_age(app, max_age): response.cookies["test"]["max-age"] = max_age return response - request, response = app.test_client.get("/", cookies=cookies) + request, response = app.test_client.get( + "/", cookies=cookies, raw_cookies=True + ) assert response.status == 200 - assert response.cookies["test"].value == "pass" + cookie = response.cookies.get("test") + if ( + str(max_age).isdigit() + and int(max_age) == float(max_age) + and int(max_age) != 0 + ): + cookie_expires = datetime.utcfromtimestamp( + response.raw_cookies["test"].expires + ).replace(microsecond=0) - if str(max_age).isdigit() and int(max_age) == float(max_age): - assert response.cookies["test"]["max-age"] == str(max_age) + # Grabbing utcnow after the response may lead to it being off slightly. + # Therefore, we 0 out the microseconds, and accept the test if there + # is a 1 second difference. + expires = datetime.utcnow().replace(microsecond=0) + timedelta( + seconds=int(max_age) + ) + + assert cookie == "pass" + assert ( + cookie_expires == expires + or cookie_expires == expires + timedelta(seconds=-1) + ) else: - assert response.cookies["test"]["max-age"] == str(DEFAULT_MAX_AGE) + assert cookie is None -@pytest.mark.parametrize("expires", [datetime.now() + timedelta(seconds=60)]) +@pytest.mark.parametrize( + "expires", [datetime.utcnow() + timedelta(seconds=60)] +) def test_cookie_expires(app, expires): + expires = expires.replace(microsecond=0) cookies = {"test": "wait"} @app.get("/") @@ -171,15 +197,16 @@ def test_cookie_expires(app, expires): response.cookies["test"]["expires"] = expires return response - request, response = app.test_client.get("/", cookies=cookies) + request, response = app.test_client.get( + "/", cookies=cookies, raw_cookies=True + ) + cookie_expires = datetime.utcfromtimestamp( + response.raw_cookies["test"].expires + ).replace(microsecond=0) + assert response.status == 200 - - assert response.cookies["test"].value == "pass" - - if isinstance(expires, datetime): - expires = expires.strftime("%a, %d-%b-%Y %T GMT") - - assert response.cookies["test"]["expires"] == expires + assert response.cookies["test"] == "pass" + assert cookie_expires == expires @pytest.mark.parametrize("expires", ["Fri, 21-Dec-2018 15:30:00 GMT"]) diff --git a/tests/test_create_task.py b/tests/test_create_task.py index b1c0710a..4ea5c845 100644 --- a/tests/test_create_task.py +++ b/tests/test_create_task.py @@ -1,7 +1,9 @@ -from sanic.response import text -from threading import Event import asyncio + from queue import Queue +from threading import Event + +from sanic.response import text def test_create_task(app): diff --git a/tests/test_custom_protocol.py b/tests/test_custom_protocol.py index e1e7d589..8984c8df 100644 --- a/tests/test_custom_protocol.py +++ b/tests/test_custom_protocol.py @@ -1,5 +1,5 @@ -from sanic.server import HttpProtocol from sanic.response import text +from sanic.server import HttpProtocol class CustomHttpProtocol(HttpProtocol): diff --git a/tests/test_dynamic_routes.py b/tests/test_dynamic_routes.py index 6a5c57c6..ee3e11b4 100644 --- a/tests/test_dynamic_routes.py +++ b/tests/test_dynamic_routes.py @@ -1,6 +1,7 @@ +import pytest + from sanic.response import text from sanic.router import RouteExists -import pytest @pytest.mark.parametrize( diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index a02a7064..7e1b78b9 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -1,10 +1,17 @@ import pytest + from bs4 import BeautifulSoup from sanic import Sanic +from sanic.exceptions import ( + Forbidden, + InvalidUsage, + NotFound, + ServerError, + Unauthorized, + abort, +) from sanic.response import text -from sanic.exceptions import InvalidUsage, ServerError, NotFound, Unauthorized -from sanic.exceptions import Forbidden, abort class SanicExceptionTestException(Exception): diff --git a/tests/test_exceptions_handler.py b/tests/test_exceptions_handler.py index aae99bf9..446f6240 100644 --- a/tests/test_exceptions_handler.py +++ b/tests/test_exceptions_handler.py @@ -1,9 +1,11 @@ -from sanic import Sanic -from sanic.response import text -from sanic.exceptions import InvalidUsage, ServerError, NotFound -from sanic.handlers import ErrorHandler from bs4 import BeautifulSoup +from sanic import Sanic +from sanic.exceptions import InvalidUsage, NotFound, ServerError +from sanic.handlers import ErrorHandler +from sanic.response import text + + exception_handler_app = Sanic("test_exception_handler") diff --git a/tests/test_keep_alive_timeout.py b/tests/test_keep_alive_timeout.py index 566edae0..1d6de63e 100644 --- a/tests/test_keep_alive_timeout.py +++ b/tests/test_keep_alive_timeout.py @@ -1,39 +1,143 @@ -from json import JSONDecodeError -from sanic import Sanic import asyncio +import functools +import socket + from asyncio import sleep as aio_sleep +from http.client import _encode +from json import JSONDecodeError + +import httpcore +import requests_async as requests + +from httpcore import PoolTimeout + +from sanic import Sanic, server from sanic.response import text -from sanic import server -import aiohttp -from aiohttp import TCPConnector -from sanic.testing import SanicTestClient, HOST, PORT +from sanic.testing import HOST, PORT, SanicTestClient + + +# import traceback + + + + CONFIG_FOR_TESTS = {"KEEP_ALIVE_TIMEOUT": 2, "KEEP_ALIVE": True} +old_conn = None -class ReuseableTCPConnector(TCPConnector): - def __init__(self, *args, **kwargs): - super(ReuseableTCPConnector, self).__init__(*args, **kwargs) - self.old_proto = None - async def connect(self, req, *args, **kwargs): - new_conn = await super(ReuseableTCPConnector, self).connect( - req, *args, **kwargs - ) - if self.old_proto is not None: - if self.old_proto != new_conn._protocol: +class ReusableSanicConnectionPool(httpcore.ConnectionPool): + async def acquire_connection(self, url, ssl, timeout): + global old_conn + if timeout.connect_timeout and not timeout.pool_timeout: + timeout.pool_timeout = timeout.connect_timeout + key = (url.scheme, url.hostname, url.port, ssl, timeout) + try: + connection = self._keepalive_connections[key].pop() + if not self._keepalive_connections[key]: + del self._keepalive_connections[key] + self.num_keepalive_connections -= 1 + self.num_active_connections += 1 + + except (KeyError, IndexError): + ssl_context = await self.get_ssl_context(url, ssl) + try: + await asyncio.wait_for( + self._max_connections.acquire(), timeout.pool_timeout + ) + except asyncio.TimeoutError: + raise PoolTimeout() + release = functools.partial(self.release_connection, key=key) + connection = httpcore.connections.Connection( + timeout=timeout, on_release=release + ) + self.num_active_connections += 1 + await connection.open(url.hostname, url.port, ssl=ssl_context) + + if old_conn is not None: + if old_conn != connection: raise RuntimeError( "We got a new connection, wanted the same one!" ) - print(new_conn.__dict__) - self.old_proto = new_conn._protocol - return new_conn + old_conn = connection + + return connection + + +class ReusableSanicAdapter(requests.adapters.HTTPAdapter): + def __init__(self): + self.pool = ReusableSanicConnectionPool() + + async def send( + self, + request, + stream=False, + timeout=None, + verify=True, + cert=None, + proxies=None, + ): + + method = request.method + url = request.url + headers = [ + (_encode(k), _encode(v)) for k, v in request.headers.items() + ] + + if not request.body: + body = b"" + elif isinstance(request.body, str): + body = _encode(request.body) + else: + body = request.body + + if isinstance(timeout, tuple): + timeout_kwargs = { + "connect_timeout": timeout[0], + "read_timeout": timeout[1], + } + else: + timeout_kwargs = { + "connect_timeout": timeout, + "read_timeout": timeout, + } + + ssl = httpcore.SSLConfig(cert=cert, verify=verify) + timeout = httpcore.TimeoutConfig(**timeout_kwargs) + + try: + response = await self.pool.request( + method, + url, + headers=headers, + body=body, + stream=stream, + ssl=ssl, + timeout=timeout, + ) + except (httpcore.BadResponse, socket.error) as err: + raise ConnectionError(err, request=request) + except httpcore.ConnectTimeout as err: + raise requests.exceptions.ConnectTimeout(err, request=request) + except httpcore.ReadTimeout as err: + raise requests.exceptions.ReadTimeout(err, request=request) + + return self.build_response(request, response) + + +class ResusableSanicSession(requests.Session): + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + adapter = ReusableSanicAdapter() + self.mount("http://", adapter) + self.mount("https://", adapter) class ReuseableSanicTestClient(SanicTestClient): def __init__(self, app, loop=None): - super(ReuseableSanicTestClient, self).__init__(app) + super().__init__(app) if loop is None: loop = asyncio.get_event_loop() self._loop = loop @@ -51,12 +155,11 @@ class ReuseableSanicTestClient(SanicTestClient): debug=False, server_kwargs={"return_asyncio_server": True}, *request_args, - **request_kwargs + **request_kwargs, ): loop = self._loop results = [None, None] exceptions = [] - do_kill_server = request_kwargs.pop("end_server", False) if gather_request: def _collect_request(request): @@ -65,21 +168,27 @@ class ReuseableSanicTestClient(SanicTestClient): self.app.request_middleware.appendleft(_collect_request) + if uri.startswith( + ("http:", "https:", "ftp:", "ftps://", "//", "ws:", "wss:") + ): + url = uri + else: + uri = uri if uri.startswith("/") else "/{uri}".format(uri=uri) + scheme = "http" + url = "{scheme}://{host}:{port}{uri}".format( + scheme=scheme, host=HOST, port=PORT, uri=uri + ) + @self.app.listener("after_server_start") async def _collect_response(loop): try: - if do_kill_server: - request_kwargs["end_session"] = True response = await self._local_request( - method, uri, *request_args, **request_kwargs + method, url, *request_args, **request_kwargs ) results[-1] = response except Exception as e2: - import traceback - - traceback.print_tb(e2.__traceback__) + # traceback.print_tb(e2.__traceback__) exceptions.append(e2) - # Don't stop here! self.app.stop() if self._server is not None: _server = self._server @@ -94,27 +203,14 @@ class ReuseableSanicTestClient(SanicTestClient): try: loop._stopping = False - http_server = loop.run_until_complete(_server_co) + _server = loop.run_until_complete(_server_co) except Exception as e1: - import traceback - - traceback.print_tb(e1.__traceback__) + # traceback.print_tb(e1.__traceback__) raise e1 - self._server = _server = http_server + self._server = _server server.trigger_events(self.app.listeners["after_server_start"], loop) self.app.listeners["after_server_start"].pop() - if do_kill_server: - try: - _server.close() - self._server = None - loop.run_until_complete(_server.wait_closed()) - self.app.stop() - except Exception as e3: - import traceback - - traceback.print_tb(e3.__traceback__) - exceptions.append(e3) if exceptions: raise ValueError("Exception during request: {}".format(exceptions)) @@ -137,59 +233,61 @@ class ReuseableSanicTestClient(SanicTestClient): "Request object expected, got ({})".format(results) ) + def kill_server(self): + try: + if self._server: + self._server.close() + self._loop.run_until_complete(self._server.wait_closed()) + self._server = None + self.app.stop() + + if self._session: + self._loop.run_until_complete(self._session.close()) + self._session = None + + except Exception as e3: + raise e3 + # Copied from SanicTestClient, but with some changes to reuse the # same TCPConnection and the sane ClientSession more than once. # Note, you cannot use the same session if you are in a _different_ # loop, so the changes above are required too. - async def _local_request(self, method, uri, cookies=None, *args, **kwargs): + async def _local_request(self, method, url, *args, **kwargs): + raw_cookies = kwargs.pop("raw_cookies", None) request_keepalive = kwargs.pop( "request_keepalive", CONFIG_FOR_TESTS["KEEP_ALIVE_TIMEOUT"] ) - if uri.startswith(("http:", "https:", "ftp:", "ftps://" "//")): - url = uri - else: - url = "http://{host}:{port}{uri}".format( - host=HOST, port=self.port, uri=uri - ) - do_kill_session = kwargs.pop("end_session", False) if self._session: - session = self._session + _session = self._session else: - if self._tcp_connector: - conn = self._tcp_connector - else: - conn = ReuseableTCPConnector( - ssl=False, - loop=self._loop, - keepalive_timeout=request_keepalive, + _session = ResusableSanicSession() + self._session = _session + async with _session as session: + try: + response = await getattr(session, method.lower())( + url, + verify=False, + timeout=request_keepalive, + *args, + **kwargs, ) - self._tcp_connector = conn - session = aiohttp.ClientSession( - cookies=cookies, connector=conn, loop=self._loop - ) - self._session = session - - async with getattr(session, method.lower())( - url, *args, **kwargs - ) as response: - try: - response.text = await response.text() - except UnicodeDecodeError: - response.text = None + except NameError: + raise Exception(response.status_code) try: - response.json = await response.json() - except ( - JSONDecodeError, - UnicodeDecodeError, - aiohttp.ClientResponseError, - ): + response.json = response.json() + except (JSONDecodeError, UnicodeDecodeError): response.json = None response.body = await response.read() - if do_kill_session: - await session.close() - self._session = None + response.status = response.status_code + response.content_type = response.headers.get("content-type") + + if raw_cookies: + response.raw_cookies = {} + for cookie in response.cookies: + response.raw_cookies[cookie.name] = cookie + return response @@ -229,9 +327,10 @@ def test_keep_alive_timeout_reuse(): assert response.status == 200 assert response.text == "OK" loop.run_until_complete(aio_sleep(1)) - request, response = client.get("/1", end_server=True) + request, response = client.get("/1") assert response.status == 200 assert response.text == "OK" + client.kill_server() def test_keep_alive_client_timeout(): @@ -241,20 +340,21 @@ def test_keep_alive_client_timeout(): asyncio.set_event_loop(loop) client = ReuseableSanicTestClient(keep_alive_app_client_timeout, loop) headers = {"Connection": "keep-alive"} - request, response = client.get("/1", headers=headers, request_keepalive=1) - assert response.status == 200 - assert response.text == "OK" - loop.run_until_complete(aio_sleep(2)) - exception = None try: request, response = client.get( - "/1", end_server=True, request_keepalive=1 + "/1", headers=headers, request_keepalive=1 ) + assert response.status == 200 + assert response.text == "OK" + loop.run_until_complete(aio_sleep(2)) + exception = None + request, response = client.get("/1", request_keepalive=1) except ValueError as e: exception = e assert exception is not None assert isinstance(exception, ValueError) assert "got a new connection" in exception.args[0] + client.kill_server() def test_keep_alive_server_timeout(): @@ -266,15 +366,15 @@ def test_keep_alive_server_timeout(): asyncio.set_event_loop(loop) client = ReuseableSanicTestClient(keep_alive_app_server_timeout, loop) headers = {"Connection": "keep-alive"} - request, response = client.get("/1", headers=headers, request_keepalive=60) - assert response.status == 200 - assert response.text == "OK" - loop.run_until_complete(aio_sleep(3)) - exception = None try: request, response = client.get( - "/1", request_keepalive=60, end_server=True + "/1", headers=headers, request_keepalive=60 ) + assert response.status == 200 + assert response.text == "OK" + loop.run_until_complete(aio_sleep(3)) + exception = None + request, response = client.get("/1", request_keepalive=60) except ValueError as e: exception = e assert exception is not None @@ -283,3 +383,4 @@ def test_keep_alive_server_timeout(): "Connection reset" in exception.args[0] or "got a new connection" in exception.args[0] ) + client.kill_server() diff --git a/tests/test_logging.py b/tests/test_logging.py index b13532b2..5a54b75a 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -1,17 +1,17 @@ -import uuid import logging +import uuid -from io import StringIO from importlib import reload - -import pytest +from io import StringIO from unittest.mock import Mock +import pytest + import sanic -from sanic.response import text -from sanic.log import LOGGING_CONFIG_DEFAULTS + from sanic import Sanic -from sanic.log import logger +from sanic.log import LOGGING_CONFIG_DEFAULTS, logger +from sanic.response import text logging_format = """module: %(module)s; \ diff --git a/tests/test_logo.py b/tests/test_logo.py index eb54bccf..d99e117f 100644 --- a/tests/test_logo.py +++ b/tests/test_logo.py @@ -1,8 +1,9 @@ -import logging import asyncio +import logging from sanic.config import BASE_LOGO + try: import uvloop # noqa diff --git a/tests/test_middleware.py b/tests/test_middleware.py index 6e94b5c8..26f0d751 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -1,10 +1,12 @@ import logging + from asyncio import CancelledError from sanic.exceptions import NotFound from sanic.request import Request from sanic.response import HTTPResponse, text + # ------------------------------------------------------------ # # GET # ------------------------------------------------------------ # diff --git a/tests/test_multiprocessing.py b/tests/test_multiprocessing.py index b4b69caf..763db085 100644 --- a/tests/test_multiprocessing.py +++ b/tests/test_multiprocessing.py @@ -1,11 +1,12 @@ import multiprocessing +import pickle import random import signal -import pickle + import pytest -from sanic.testing import HOST, PORT from sanic.response import text +from sanic.testing import HOST, PORT @pytest.mark.skipif( diff --git a/tests/test_named_routes.py b/tests/test_named_routes.py index 05e3890e..7783e454 100644 --- a/tests/test_named_routes.py +++ b/tests/test_named_routes.py @@ -2,12 +2,13 @@ # -*- coding: utf-8 -*- import asyncio + import pytest from sanic.blueprints import Blueprint -from sanic.response import text -from sanic.exceptions import URLBuildError from sanic.constants import HTTP_METHODS +from sanic.exceptions import URLBuildError +from sanic.response import text # ------------------------------------------------------------ # diff --git a/tests/test_redirect.py b/tests/test_redirect.py index c2c54744..86c4ace3 100644 --- a/tests/test_redirect.py +++ b/tests/test_redirect.py @@ -1,7 +1,8 @@ -import pytest from urllib.parse import quote -from sanic.response import text, redirect +import pytest + +from sanic.response import redirect, text @pytest.fixture diff --git a/tests/test_request_cancel.py b/tests/test_request_cancel.py index d6b53079..e9499f6d 100644 --- a/tests/test_request_cancel.py +++ b/tests/test_request_cancel.py @@ -1,7 +1,7 @@ import asyncio import contextlib -from sanic.response import text, stream +from sanic.response import stream, text async def test_request_cancel_when_connection_lost(loop, app, test_client): diff --git a/tests/test_request_data.py b/tests/test_request_data.py index dad112ad..061653bd 100644 --- a/tests/test_request_data.py +++ b/tests/test_request_data.py @@ -2,6 +2,7 @@ import random from sanic.response import json + try: from ujson import loads except ImportError: diff --git a/tests/test_request_stream.py b/tests/test_request_stream.py index c8978608..c1457c8f 100644 --- a/tests/test_request_stream.py +++ b/tests/test_request_stream.py @@ -1,10 +1,8 @@ -import asyncio from sanic.blueprints import Blueprint -from sanic.views import CompositionView -from sanic.views import HTTPMethodView -from sanic.views import stream as stream_decorator -from sanic.response import stream, text from sanic.request import StreamBuffer +from sanic.response import stream, text +from sanic.views import CompositionView, HTTPMethodView +from sanic.views import stream as stream_decorator data = "abc" * 10000000 diff --git a/tests/test_request_timeout.py b/tests/test_request_timeout.py index 1e7db05d..e59f2d2f 100644 --- a/tests/test_request_timeout.py +++ b/tests/test_request_timeout.py @@ -1,183 +1,73 @@ -from json import JSONDecodeError +import asyncio + +import httpcore +import requests_async as requests from sanic import Sanic -import asyncio from sanic.response import text -import aiohttp -from aiohttp import TCPConnector -from sanic.testing import SanicTestClient, HOST - -try: - try: - # direct use - import packaging - - version = packaging.version - except (ImportError, AttributeError): - # setuptools v39.0 and above. - try: - from setuptools.extern import packaging - except ImportError: - # Before setuptools v39.0 - from pkg_resources.extern import packaging - version = packaging.version -except ImportError: - raise RuntimeError("The 'packaging' library is missing.") +from sanic.testing import SanicTestClient -aiohttp_version = version.parse(aiohttp.__version__) +class DelayableSanicConnectionPool(httpcore.ConnectionPool): + def __init__(self, request_delay=None, *args, **kwargs): + self._request_delay = request_delay + super().__init__(*args, **kwargs) + async def request( + self, + method, + url, + headers=(), + body=b"", + stream=False, + ssl=None, + timeout=None, + ): + if ssl is None: + ssl = self.ssl_config + if timeout is None: + timeout = self.timeout -class DelayableTCPConnector(TCPConnector): - class RequestContextManager(object): - def __new__(cls, req, delay): - cls = super( - DelayableTCPConnector.RequestContextManager, cls - ).__new__(cls) - cls.req = req - cls.send_task = None - cls.resp = None - cls.orig_send = getattr(req, "send") - cls.orig_start = None - cls.delay = delay - cls._acting_as = req - return cls - - def __getattr__(self, item): - acting_as = self._acting_as - return getattr(acting_as, item) - - async def start(self, connection, read_until_eof=False): - if self.send_task is None: - raise RuntimeError("do a send() before you do a start()") - resp = await self.send_task - self.send_task = None - self.resp = resp - self._acting_as = self.resp - self.orig_start = getattr(resp, "start") - - try: - if aiohttp_version >= version.parse("3.3.0"): - ret = await self.orig_start(connection) - else: - ret = await self.orig_start(connection, read_until_eof) - except Exception as e: - raise e - return ret - - def close(self): - if self.resp is not None: - self.resp.close() - if self.send_task is not None: - self.send_task.cancel() - - async def delayed_send(self, *args, **kwargs): - req = self.req - if self.delay and self.delay > 0: - # sync_sleep(self.delay) - await asyncio.sleep(self.delay) - t = req.loop.time() - print("sending at {}".format(t), flush=True) - next(iter(args)) # first arg is connection - - try: - return await self.orig_send(*args, **kwargs) - except Exception as e: - if aiohttp_version < version.parse("3.1.0"): - return aiohttp.ClientResponse(req.method, req.url) - kw = dict( - writer=None, - continue100=None, - timer=None, - request_info=None, - traces=[], - loop=req.loop, - session=None, - ) - if aiohttp_version < version.parse("3.3.0"): - kw["auto_decompress"] = None - return aiohttp.ClientResponse(req.method, req.url, **kw) - - def _send(self, *args, **kwargs): - gen = self.delayed_send(*args, **kwargs) - task = self.req.loop.create_task(gen) - self.send_task = task - self._acting_as = task - return self - - if aiohttp_version >= version.parse("3.1.0"): - # aiohttp changed the request.send method to async - async def send(self, *args, **kwargs): - return self._send(*args, **kwargs) - - else: - send = _send - - def __init__(self, *args, **kwargs): - _post_connect_delay = kwargs.pop("post_connect_delay", 0) - _pre_request_delay = kwargs.pop("pre_request_delay", 0) - super(DelayableTCPConnector, self).__init__(*args, **kwargs) - self._post_connect_delay = _post_connect_delay - self._pre_request_delay = _pre_request_delay - - async def connect(self, req, *args, **kwargs): - d_req = DelayableTCPConnector.RequestContextManager( - req, self._pre_request_delay + parsed_url = httpcore.URL(url) + request = httpcore.Request( + method, parsed_url, headers=headers, body=body ) - conn = await super(DelayableTCPConnector, self).connect( - req, *args, **kwargs + connection = await self.acquire_connection( + parsed_url, ssl=ssl, timeout=timeout ) - if self._post_connect_delay and self._post_connect_delay > 0: - await asyncio.sleep(self._post_connect_delay, loop=self._loop) - req.send = d_req.send - t = req.loop.time() - print("Connected at {}".format(t), flush=True) - return conn + if self._request_delay: + print(f"\t>> Sleeping ({self._request_delay})") + await asyncio.sleep(self._request_delay) + response = await connection.send(request) + if not stream: + try: + await response.read() + finally: + await response.close() + return response + + +class DelayableSanicAdapter(requests.adapters.HTTPAdapter): + def __init__(self, request_delay=None): + self.pool = DelayableSanicConnectionPool(request_delay=request_delay) + + +class DelayableSanicSession(requests.Session): + def __init__(self, request_delay=None, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + adapter = DelayableSanicAdapter(request_delay=request_delay) + self.mount("http://", adapter) + self.mount("https://", adapter) class DelayableSanicTestClient(SanicTestClient): - def __init__(self, app, loop, request_delay=1): - super(DelayableSanicTestClient, self).__init__(app) + def __init__(self, app, request_delay=None): + super().__init__(app) self._request_delay = request_delay self._loop = None - async def _local_request(self, method, uri, cookies=None, *args, **kwargs): - if self._loop is None: - self._loop = asyncio.get_event_loop() - if uri.startswith(("http:", "https:", "ftp:", "ftps://" "//")): - url = uri - else: - url = "http://{host}:{port}{uri}".format( - host=HOST, port=self.port, uri=uri - ) - conn = DelayableTCPConnector( - pre_request_delay=self._request_delay, ssl=False, loop=self._loop - ) - async with aiohttp.ClientSession( - cookies=cookies, connector=conn, loop=self._loop - ) as session: - # Insert a delay after creating the connection - # But before sending the request. - - async with getattr(session, method.lower())( - url, *args, **kwargs - ) as response: - try: - response.text = await response.text() - except UnicodeDecodeError: - response.text = None - - try: - response.json = await response.json() - except ( - JSONDecodeError, - UnicodeDecodeError, - aiohttp.ClientResponseError, - ): - response.json = None - - response.body = await response.read() - return response + def get_new_session(self): + return DelayableSanicSession(request_delay=self._request_delay) request_timeout_default_app = Sanic("test_request_timeout_default") @@ -202,14 +92,14 @@ async def ws_handler1(request, ws): def test_default_server_error_request_timeout(): - client = DelayableSanicTestClient(request_timeout_default_app, None, 2) + client = DelayableSanicTestClient(request_timeout_default_app, 2) request, response = client.get("/1") assert response.status == 408 assert response.text == "Error: Request Timeout" def test_default_server_error_request_dont_timeout(): - client = DelayableSanicTestClient(request_no_timeout_app, None, 0.2) + client = DelayableSanicTestClient(request_no_timeout_app, 0.2) request, response = client.get("/1") assert response.status == 200 assert response.text == "OK" @@ -224,7 +114,7 @@ def test_default_server_error_websocket_request_timeout(): "Sec-WebSocket-Version": "13", } - client = DelayableSanicTestClient(request_timeout_default_app, None, 2) + client = DelayableSanicTestClient(request_timeout_default_app, 2) request, response = client.get("/ws1", headers=headers) assert response.status == 408 diff --git a/tests/test_requests.py b/tests/test_requests.py index 9e634fd8..2d854a73 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -1,19 +1,20 @@ import logging import os import ssl + from json import dumps as json_dumps from json import loads as json_loads from urllib.parse import urlparse import pytest -from sanic import Sanic -from sanic import Blueprint +from sanic import Blueprint, Sanic from sanic.exceptions import ServerError from sanic.request import DEFAULT_HTTP_CONTENT_TYPE, RequestParameters from sanic.response import json, text from sanic.testing import HOST, PORT + # ------------------------------------------------------------ # # GET # ------------------------------------------------------------ # @@ -529,36 +530,54 @@ def test_request_string_representation(app): @pytest.mark.parametrize( "payload,filename", [ - ("------sanic\r\n" - 'Content-Disposition: form-data; filename="filename"; name="test"\r\n' - "\r\n" - "OK\r\n" - "------sanic--\r\n", "filename"), - ("------sanic\r\n" - 'content-disposition: form-data; filename="filename"; name="test"\r\n' - "\r\n" - 'content-type: application/json; {"field": "value"}\r\n' - "------sanic--\r\n", "filename"), - ("------sanic\r\n" - 'Content-Disposition: form-data; filename=""; name="test"\r\n' - "\r\n" - "OK\r\n" - "------sanic--\r\n", ""), - ("------sanic\r\n" - 'content-disposition: form-data; filename=""; name="test"\r\n' - "\r\n" - 'content-type: application/json; {"field": "value"}\r\n' - "------sanic--\r\n", ""), - ("------sanic\r\n" - 'Content-Disposition: form-data; filename*="utf-8\'\'filename_%C2%A0_test"; name="test"\r\n' - "\r\n" - "OK\r\n" - "------sanic--\r\n", "filename_\u00A0_test"), - ("------sanic\r\n" - 'content-disposition: form-data; filename*="utf-8\'\'filename_%C2%A0_test"; name="test"\r\n' - "\r\n" - 'content-type: application/json; {"field": "value"}\r\n' - "------sanic--\r\n", "filename_\u00A0_test"), + ( + "------sanic\r\n" + 'Content-Disposition: form-data; filename="filename"; name="test"\r\n' + "\r\n" + "OK\r\n" + "------sanic--\r\n", + "filename", + ), + ( + "------sanic\r\n" + 'content-disposition: form-data; filename="filename"; name="test"\r\n' + "\r\n" + 'content-type: application/json; {"field": "value"}\r\n' + "------sanic--\r\n", + "filename", + ), + ( + "------sanic\r\n" + 'Content-Disposition: form-data; filename=""; name="test"\r\n' + "\r\n" + "OK\r\n" + "------sanic--\r\n", + "", + ), + ( + "------sanic\r\n" + 'content-disposition: form-data; filename=""; name="test"\r\n' + "\r\n" + 'content-type: application/json; {"field": "value"}\r\n' + "------sanic--\r\n", + "", + ), + ( + "------sanic\r\n" + 'Content-Disposition: form-data; filename*="utf-8\'\'filename_%C2%A0_test"; name="test"\r\n' + "\r\n" + "OK\r\n" + "------sanic--\r\n", + "filename_\u00A0_test", + ), + ( + "------sanic\r\n" + 'content-disposition: form-data; filename*="utf-8\'\'filename_%C2%A0_test"; name="test"\r\n' + "\r\n" + 'content-type: application/json; {"field": "value"}\r\n' + "------sanic--\r\n", + "filename_\u00A0_test", + ), ], ) def test_request_multipart_files(app, payload, filename): @@ -743,7 +762,7 @@ def test_request_raw_args(app): def test_request_query_args(app): # test multiple params with the same key - params = [('test', 'value1'), ('test', 'value2')] + params = [("test", "value1"), ("test", "value2")] @app.get("/") def handler(request): @@ -754,7 +773,10 @@ def test_request_query_args(app): assert request.query_args == params # test cached value - assert request.parsed_not_grouped_args[(False, False, "utf-8", "replace")] == request.query_args + assert ( + request.parsed_not_grouped_args[(False, False, "utf-8", "replace")] + == request.query_args + ) # test params directly in the url request, response = app.test_client.get("/?test=value1&test=value2") @@ -762,7 +784,7 @@ def test_request_query_args(app): assert request.query_args == params # test unique params - params = [('test1', 'value1'), ('test2', 'value2')] + params = [("test1", "value1"), ("test2", "value2")] request, response = app.test_client.get("/", params=params) @@ -779,25 +801,22 @@ def test_request_query_args_custom_parsing(app): def handler(request): return text("pass") - request, response = app.test_client.get("/?test1=value1&test2=&test3=value3") + request, response = app.test_client.get( + "/?test1=value1&test2=&test3=value3" + ) - assert request.get_query_args( - keep_blank_values=True - ) == [ - ('test1', 'value1'), ('test2', ''), ('test3', 'value3') + assert request.get_query_args(keep_blank_values=True) == [ + ("test1", "value1"), + ("test2", ""), + ("test3", "value3"), ] - assert request.query_args == [ - ('test1', 'value1'), ('test3', 'value3') - ] - assert request.get_query_args( - keep_blank_values=False - ) == [ - ('test1', 'value1'), ('test3', 'value3') + assert request.query_args == [("test1", "value1"), ("test3", "value3")] + assert request.get_query_args(keep_blank_values=False) == [ + ("test1", "value1"), + ("test3", "value3"), ] - assert request.get_args( - keep_blank_values=True - ) == RequestParameters( + assert request.get_args(keep_blank_values=True) == RequestParameters( {"test1": ["value1"], "test2": [""], "test3": ["value3"]} ) @@ -805,9 +824,7 @@ def test_request_query_args_custom_parsing(app): {"test1": ["value1"], "test3": ["value3"]} ) - assert request.get_args( - keep_blank_values=False - ) == RequestParameters( + assert request.get_args(keep_blank_values=False) == RequestParameters( {"test1": ["value1"], "test3": ["value3"]} ) diff --git a/tests/test_response.py b/tests/test_response.py index 256a37c6..e290c777 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -1,6 +1,7 @@ import asyncio import inspect import os + from collections import namedtuple from mimetypes import guess_type from random import choice @@ -8,6 +9,7 @@ from unittest.mock import MagicMock from urllib.parse import unquote import pytest + from aiofiles import os as async_os from sanic.response import ( @@ -18,11 +20,11 @@ from sanic.response import ( json, raw, stream, - text, ) from sanic.server import HttpProtocol from sanic.testing import HOST, PORT + JSON_DATA = {"ok": True} @@ -77,10 +79,10 @@ def test_response_header(app): request, response = app.test_client.get("/") assert dict(response.headers) == { - "Connection": "keep-alive", - "Keep-Alive": str(app.config.KEEP_ALIVE_TIMEOUT), - "Content-Length": "11", - "Content-Type": "application/json", + "connection": "keep-alive", + "keep-alive": str(app.config.KEEP_ALIVE_TIMEOUT), + "content-length": "11", + "content-type": "application/json", } @@ -276,7 +278,7 @@ def test_stream_response_with_cookies(app): return response request, response = app.test_client.get("/") - assert response.cookies["test"].value == "pass" + assert response.cookies["test"] == "pass" def test_stream_response_without_cookies(app): diff --git a/tests/test_response_timeout.py b/tests/test_response_timeout.py index bae0daa0..95a77a2d 100644 --- a/tests/test_response_timeout.py +++ b/tests/test_response_timeout.py @@ -1,7 +1,9 @@ -from sanic import Sanic import asyncio -from sanic.response import text + +from sanic import Sanic from sanic.exceptions import ServiceUnavailable +from sanic.response import text + response_timeout_app = Sanic("test_response_timeout") response_timeout_default_app = Sanic("test_response_timeout_default") diff --git a/tests/test_routes.py b/tests/test_routes.py index 3ccef135..4617803e 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -7,6 +7,7 @@ from sanic.constants import HTTP_METHODS from sanic.response import json, text from sanic.router import ParameterNameConflicts, RouteDoesNotExist, RouteExists + # ------------------------------------------------------------ # # UTF-8 # ------------------------------------------------------------ # @@ -468,16 +469,8 @@ def test_websocket_route(app, url): assert ws.subprotocol is None ev.set() - request, response = app.test_client.get( - "/ws", - headers={ - "Upgrade": "websocket", - "Connection": "upgrade", - "Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==", - "Sec-WebSocket-Version": "13", - }, - ) - assert response.status == 101 + request, response = app.test_client.websocket(url) + assert response.opened is True assert ev.is_set() @@ -487,54 +480,24 @@ def test_websocket_route_with_subprotocols(app): @app.websocket("/ws", subprotocols=["foo", "bar"]) async def handler(request, ws): results.append(ws.subprotocol) + assert ws.subprotocol is not None - request, response = app.test_client.get( - "/ws", - headers={ - "Upgrade": "websocket", - "Connection": "upgrade", - "Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==", - "Sec-WebSocket-Version": "13", - "Sec-WebSocket-Protocol": "bar", - }, + request, response = app.test_client.websocket("/ws", subprotocols=["bar"]) + assert response.opened is True + assert results == ["bar"] + + request, response = app.test_client.websocket( + "/ws", subprotocols=["bar", "foo"] ) - assert response.status == 101 + assert response.opened is True + assert results == ["bar", "bar"] - request, response = app.test_client.get( - "/ws", - headers={ - "Upgrade": "websocket", - "Connection": "upgrade", - "Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==", - "Sec-WebSocket-Version": "13", - "Sec-WebSocket-Protocol": "bar, foo", - }, - ) - assert response.status == 101 - - request, response = app.test_client.get( - "/ws", - headers={ - "Upgrade": "websocket", - "Connection": "upgrade", - "Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==", - "Sec-WebSocket-Version": "13", - "Sec-WebSocket-Protocol": "baz", - }, - ) - assert response.status == 101 - - request, response = app.test_client.get( - "/ws", - headers={ - "Upgrade": "websocket", - "Connection": "upgrade", - "Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==", - "Sec-WebSocket-Version": "13", - }, - ) - assert response.status == 101 + request, response = app.test_client.websocket("/ws", subprotocols=["baz"]) + assert response.opened is True + assert results == ["bar", "bar", None] + request, response = app.test_client.websocket("/ws") + assert response.opened is True assert results == ["bar", "bar", None, None] @@ -547,16 +510,8 @@ def test_add_webscoket_route(app, strict_slashes): ev.set() app.add_websocket_route(handler, "/ws", strict_slashes=strict_slashes) - request, response = app.test_client.get( - "/ws", - headers={ - "Upgrade": "websocket", - "Connection": "upgrade", - "Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==", - "Sec-WebSocket-Version": "13", - }, - ) - assert response.status == 101 + request, response = app.test_client.websocket("/ws") + assert response.opened is True assert ev.is_set() diff --git a/tests/test_server_events.py b/tests/test_server_events.py index c15f9ed9..be17e801 100644 --- a/tests/test_server_events.py +++ b/tests/test_server_events.py @@ -4,6 +4,7 @@ import pytest from sanic.testing import HOST, PORT + AVAILABLE_LISTENERS = [ "before_server_start", "after_server_start", diff --git a/tests/test_signal_handlers.py b/tests/test_signal_handlers.py index 7a49a348..262f41cb 100644 --- a/tests/test_signal_handlers.py +++ b/tests/test_signal_handlers.py @@ -1,8 +1,10 @@ +import asyncio + +from queue import Queue +from unittest.mock import MagicMock + from sanic.response import HTTPResponse from sanic.testing import HOST, PORT -from unittest.mock import MagicMock -import asyncio -from queue import Queue async def stop(app, loop): diff --git a/tests/test_static.py b/tests/test_static.py index 43078a9d..ae66cc68 100644 --- a/tests/test_static.py +++ b/tests/test_static.py @@ -1,5 +1,6 @@ import inspect import os + from time import gmtime, strftime import pytest diff --git a/tests/test_test_client_port.py b/tests/test_test_client_port.py index a49d9f81..231ec4b5 100644 --- a/tests/test_test_client_port.py +++ b/tests/test_test_client_port.py @@ -1,7 +1,8 @@ import socket -from sanic.testing import PORT, SanicTestClient from sanic.response import json, text +from sanic.testing import PORT, SanicTestClient + # ------------------------------------------------------------ # # UTF-8 @@ -9,26 +10,26 @@ from sanic.response import json, text def test_test_client_port_none(app): - @app.get('/get') + @app.get("/get") def handler(request): - return text('OK') + return text("OK") test_client = SanicTestClient(app, port=None) - request, response = test_client.get('/get') - assert response.text == 'OK' + request, response = test_client.get("/get") + assert response.text == "OK" - request, response = test_client.post('/get') + request, response = test_client.post("/get") assert response.status == 405 def test_test_client_port_default(app): - @app.get('/get') + @app.get("/get") def handler(request): - return json(request.transport.get_extra_info('sockname')[1]) + return json(request.transport.get_extra_info("sockname")[1]) test_client = SanicTestClient(app) assert test_client.port == PORT - request, response = test_client.get('/get') + request, response = test_client.get("/get") assert response.json == PORT diff --git a/tests/test_url_building.py b/tests/test_url_building.py index a246aabc..816ce997 100644 --- a/tests/test_url_building.py +++ b/tests/test_url_building.py @@ -1,14 +1,17 @@ -import pytest as pytest -from urllib.parse import urlsplit, parse_qsl - -from sanic.response import text -from sanic.views import HTTPMethodView -from sanic.blueprints import Blueprint -from sanic.testing import PORT as test_port, HOST as test_host -from sanic.exceptions import URLBuildError - import string +from urllib.parse import parse_qsl, urlsplit + +import pytest as pytest + +from sanic.blueprints import Blueprint +from sanic.exceptions import URLBuildError +from sanic.response import text +from sanic.testing import HOST as test_host +from sanic.testing import PORT as test_port +from sanic.views import HTTPMethodView + + URL_FOR_ARGS1 = dict(arg1=["v1", "v2"]) URL_FOR_VALUE1 = "/myurl?arg1=v1&arg1=v2" URL_FOR_ARGS2 = dict(arg1=["v1", "v2"], _anchor="anchor") @@ -170,7 +173,7 @@ def test_fails_with_int_message(app): expected_error = ( r'Value "not_int" for parameter `foo` ' - r'does not match pattern for type `int`: -?\d+' + r"does not match pattern for type `int`: -?\d+" ) assert str(e.value) == expected_error diff --git a/tests/test_utf8.py b/tests/test_utf8.py index d6bcdd3e..8fd072a4 100644 --- a/tests/test_utf8.py +++ b/tests/test_utf8.py @@ -1,4 +1,5 @@ from json import dumps as json_dumps + from sanic.response import text diff --git a/tests/test_views.py b/tests/test_views.py index d0f35d3a..feef325e 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -1,11 +1,11 @@ import pytest as pytest -from sanic.exceptions import InvalidUsage -from sanic.response import text, HTTPResponse -from sanic.views import HTTPMethodView, CompositionView from sanic.blueprints import Blueprint -from sanic.request import Request from sanic.constants import HTTP_METHODS +from sanic.exceptions import InvalidUsage +from sanic.request import Request +from sanic.response import HTTPResponse, text +from sanic.views import CompositionView, HTTPMethodView @pytest.mark.parametrize("method", HTTP_METHODS) diff --git a/tests/test_worker.py b/tests/test_worker.py index 7000cccf..3e83fa13 100644 --- a/tests/test_worker.py +++ b/tests/test_worker.py @@ -1,14 +1,17 @@ -import time +import asyncio import json import shlex import subprocess +import time import urllib.request + from unittest import mock -from sanic.worker import GunicornWorker -from sanic.app import Sanic -import asyncio + import pytest +from sanic.app import Sanic +from sanic.worker import GunicornWorker + @pytest.fixture(scope="module") def gunicorn_worker(): diff --git a/tox.ini b/tox.ini index 502eea81..c825f0de 100644 --- a/tox.ini +++ b/tox.ini @@ -1,24 +1,25 @@ [tox] -envlist = py35, py36, py37, {py35,py36,py37}-no-ext, lint, check +envlist = py36, py37, {py36,py37}-no-ext, lint, check [testenv] usedevelop = True setenv = - {py35,py36,py37}-no-ext: SANIC_NO_UJSON=1 - {py35,py36,py37}-no-ext: SANIC_NO_UVLOOP=1 + {py36,py37}-no-ext: SANIC_NO_UJSON=1 + {py36,py37}-no-ext: SANIC_NO_UVLOOP=1 deps = coverage pytest==4.1.0 pytest-cov pytest-sanic pytest-sugar - aiohttp>=2.3,<=3.2.1 + httpcore==0.1.1 + requests-async==0.4.0 chardet<=2.3.0 beautifulsoup4 gunicorn pytest-benchmark commands = - pytest tests --cov sanic --cov-report= {posargs} + pytest {posargs:tests --cov sanic} - coverage combine --append coverage report -m coverage html -i @@ -31,7 +32,7 @@ deps = commands = flake8 sanic - black --check --verbose sanic + black --config ./.black.toml --check --verbose sanic isort --check-only --recursive sanic [testenv:check]