From ccd4c9615c8277e5cae17b8af06ad49cc7d77ecf Mon Sep 17 00:00:00 2001 From: Adam Hopkins Date: Wed, 24 Apr 2019 00:44:42 +0300 Subject: [PATCH] Create requests-async based TestClient, remove aiohttp dependency, drop Python 3.5 Update all tests to be compatible with requests-async Cleanup testing client changes with black and isort Remove Python 3.5 and other meta doc cleanup rename pyproject and fix pep517 error Add black config to tox.ini Cleanup tests and remove aiohttp tox.ini change for easier development commands Remove aiohttp from changelog and requirements Cleanup imports and Makefile --- .appveyor.yml | 5 - pyproject.toml => .black.toml | 0 .travis.yml | 4 - CHANGELOG.md | 15 + LICENSE | 2 +- Makefile | 6 +- README.rst | 7 +- docs/sanic/changelog.md | 15 + docs/sanic/testing.md | 12 +- sanic/testing.py | 61 ++-- setup.cfg | 2 +- setup.py | 14 +- .../test_route_resolution_benchmark.py | 2 + tests/conftest.py | 1 + tests/performance/aiohttp/simple_server.py | 7 +- tests/performance/bottle/simple_server.py | 3 +- tests/performance/kyoukai/simple_server.py | 5 +- tests/performance/sanic/http_response.py | 14 +- tests/performance/sanic/simple_server.py | 10 +- tests/performance/sanic/varied_server.py | 14 +- tests/performance/tornado/simple_server.py | 1 + tests/performance/wheezy/simple_server.py | 13 +- tests/test_app.py | 3 +- tests/test_blueprint_group.py | 3 +- tests/test_blueprints.py | 31 +- tests/test_config.py | 5 +- tests/test_cookies.py | 63 ++-- tests/test_create_task.py | 6 +- tests/test_custom_protocol.py | 2 +- tests/test_dynamic_routes.py | 3 +- tests/test_exceptions.py | 11 +- tests/test_exceptions_handler.py | 8 +- tests/test_keep_alive_timeout.py | 293 ++++++++++++------ tests/test_logging.py | 12 +- tests/test_logo.py | 3 +- tests/test_middleware.py | 2 + tests/test_multiprocessing.py | 5 +- tests/test_named_routes.py | 5 +- tests/test_redirect.py | 5 +- tests/test_request_cancel.py | 2 +- tests/test_request_data.py | 1 + tests/test_request_stream.py | 8 +- tests/test_request_timeout.py | 230 ++++---------- tests/test_requests.py | 123 ++++---- tests/test_response.py | 14 +- tests/test_response_timeout.py | 6 +- tests/test_routes.py | 85 ++--- tests/test_server_events.py | 1 + tests/test_signal_handlers.py | 8 +- tests/test_static.py | 1 + tests/test_test_client_port.py | 19 +- tests/test_url_building.py | 15 +- tests/test_utf8.py | 1 + tests/test_views.py | 8 +- tests/test_worker.py | 11 +- tox.ini | 13 +- 56 files changed, 645 insertions(+), 574 deletions(-) rename pyproject.toml => .black.toml (100%) diff --git a/.appveyor.yml b/.appveyor.yml index 368270c5f4..afc50f1362 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 e40e3124ce..a2a0a12802 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 dd86683596..ea90d8bb6b 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 74ee79874a..35740e3da6 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 de806fd69a..a95326b294 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 20dbaa1bc2..f63d9707c5 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 dd86683596..ea90d8bb6b 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 28caac1229..64cdef4fd7 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 2ed52bbbc8..7aab47368e 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 @@ def __init__(self, app, port=PORT): 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: + 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.text = await response.text() - except UnicodeDecodeError: - response.text = None + response = await getattr(session, method.lower())( + url, verify=False, *args, **kwargs + ) + 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() + 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 @@ async def error_handler(request, exception): 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 @@ def options(self, *args, **kwargs): 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 b3572c85ab..ae329e7780 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 4a682151de..2cd973b74b 100644 --- a/setup.py +++ b/setup.py @@ -50,12 +50,12 @@ def open_local(paths, mode="r", encoding="utf8"): 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 @@ def open_local(paths, mode="r", encoding="utf8"): "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 @@ def open_local(paths, mode="r", encoding="utf8"): "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 @@ def open_local(paths, mode="r", encoding="utf8"): "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 d0df69a14c..d9354c4bb4 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 1748b46c40..d720f3be0b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,6 +9,7 @@ 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 c781f070d3..9a57f459f5 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 58605fae16..43a8f01987 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 4b901978f4..9fd7d2b03c 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 8d864f5369..52596e5e18 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 33d1e52aa8..60ebd8193d 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 af9196931a..9b3f16da74 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 a326eeaf75..1e69a29382 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 cbeeee6a43..70a6338a0a 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 8d90641f81..5ddae42d79 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -3,6 +3,7 @@ import sys from inspect import isawaitable + import pytest from sanic.exceptions import SanicException @@ -11,7 +12,7 @@ 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 fe1db7a38e..32729a4903 100644 --- a/tests/test_blueprint_group.py +++ b/tests/test_blueprint_group.py @@ -3,7 +3,8 @@ 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 397216d0f1..f0a67bd743 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -7,9 +7,9 @@ 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 @@ async def websocket_handler(request, ws): 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 default_route(request): "/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 @@ async def websocket_handler(request, ws): 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 c2da4bdd3f..7b2033110d 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 b573b845e3..a77fda2fb7 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 handler(request): 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 handler(request): 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 handler(request): 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) + + # 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) + ) - if str(max_age).isdigit() and int(max_age) == float(max_age): - assert response.cookies["test"]["max-age"] == str(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 handler(request): response.cookies["test"]["expires"] = expires return response - request, response = app.test_client.get("/", cookies=cookies) - assert response.status == 200 - - assert response.cookies["test"].value == "pass" - - if isinstance(expires, datetime): - expires = expires.strftime("%a, %d-%b-%Y %T GMT") + 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.cookies["test"]["expires"] == expires + assert response.status == 200 + 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 b1c0710a09..4ea5c8455f 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 e1e7d58961..8984c8df2e 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 6a5c57c68c..ee3e11b413 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 a02a706432..7e1b78b97e 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 aae99bf9bd..446f624007 100644 --- a/tests/test_exceptions_handler.py +++ b/tests/test_exceptions_handler.py @@ -1,8 +1,10 @@ +from bs4 import BeautifulSoup + from sanic import Sanic -from sanic.response import text -from sanic.exceptions import InvalidUsage, ServerError, NotFound +from sanic.exceptions import InvalidUsage, NotFound, ServerError from sanic.handlers import ErrorHandler -from bs4 import BeautifulSoup +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 566edae0bf..1d6de63ee1 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 @@ def _sanic_endpoint_test( 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 @@ def _collect_request(request): 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 @@ async def _collect_response(loop): 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 @@ async def _collect_response(loop): "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, - ) - 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: + _session = ResusableSanicSession() + self._session = _session + async with _session as session: try: - response.text = await response.text() - except UnicodeDecodeError: - response.text = None + response = await getattr(session, method.lower())( + url, + verify=False, + timeout=request_keepalive, + *args, + **kwargs, + ) + 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 b13532b2f2..5a54b75ad8 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 +from io import StringIO +from unittest.mock import Mock import pytest -from unittest.mock import Mock 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 eb54bccfa3..d99e117f5d 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 6e94b5c835..26f0d75108 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 b4b69caf36..763db085e3 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 05e3890ef9..7783e454e4 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 c2c5474463..86c4ace3fe 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 d6b5307947..e9499f6d78 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 dad112ad26..061653bd66 100644 --- a/tests/test_request_data.py +++ b/tests/test_request_data.py @@ -2,6 +2,7 @@ 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 c897860889..c1457c8f7d 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 1e7db05d44..e59f2d2f12 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.") - - -aiohttp_version = version.parse(aiohttp.__version__) - - -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") +from sanic.testing import SanicTestClient - 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 +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 + + 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 9e634fd825..2d854a73a7 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 @@ async def get(request): @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 handler(request): 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 handler(request): 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 handler(request): 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.query_args == [ - ('test1', 'value1'), ('test3', 'value3') + assert request.get_query_args(keep_blank_values=True) == [ + ("test1", "value1"), + ("test2", ""), + ("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 handler(request): {"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 256a37c6f7..e290c77769 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 urllib.parse import unquote import pytest + from aiofiles import os as async_os from sanic.response import ( @@ -18,11 +20,11 @@ json, raw, stream, - text, ) from sanic.server import HttpProtocol from sanic.testing import HOST, PORT + JSON_DATA = {"ok": True} @@ -77,10 +79,10 @@ async def test(request): 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 @@ async def test(request): 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 bae0daa06c..95a77a2d67 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 3ccef1359c..4617803e1e 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -7,6 +7,7 @@ from sanic.response import json, text from sanic.router import ParameterNameConflicts, RouteDoesNotExist, RouteExists + # ------------------------------------------------------------ # # UTF-8 # ------------------------------------------------------------ # @@ -468,16 +469,8 @@ async def handler(request, ws): 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", - }, - ) - 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": "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", - }, + 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.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 @@ async def handler(request, ws): 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 c15f9ed96a..be17e80186 100644 --- a/tests/test_server_events.py +++ b/tests/test_server_events.py @@ -4,6 +4,7 @@ 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 7a49a34822..262f41cba9 100644 --- a/tests/test_signal_handlers.py +++ b/tests/test_signal_handlers.py @@ -1,8 +1,10 @@ -from sanic.response import HTTPResponse -from sanic.testing import HOST, PORT -from unittest.mock import MagicMock import asyncio + from queue import Queue +from unittest.mock import MagicMock + +from sanic.response import HTTPResponse +from sanic.testing import HOST, PORT async def stop(app, loop): diff --git a/tests/test_static.py b/tests/test_static.py index 43078a9d7e..ae66cc6805 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 a49d9f81c2..231ec4b5cf 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 @@ 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 a246aabc42..816ce99731 100644 --- a/tests/test_url_building.py +++ b/tests/test_url_building.py @@ -1,13 +1,16 @@ +import string + +from urllib.parse import parse_qsl, urlsplit + 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 +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 -import string URL_FOR_ARGS1 = dict(arg1=["v1", "v2"]) URL_FOR_VALUE1 = "/myurl?arg1=v1&arg1=v2" @@ -170,7 +173,7 @@ def fail(request): 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 d6bcdd3e54..8fd072a40b 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 d0f35d3a8a..feef325eb3 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 7000cccf9d..3e83fa13e7 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 502eea8158..c825f0de35 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]