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 | 63 ++-- 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 | 10 +- tests/test_keep_alive_timeout.py | 293 ++++++++++++------ tests/test_logging.py | 14 +- 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 | 226 ++++---------- tests/test_requests.py | 123 ++++---- tests/test_response.py | 14 +- tests/test_response_timeout.py | 6 +- tests/test_routes.py | 83 ++--- 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 | 23 +- tests/test_utf8.py | 1 + tests/test_views.py | 8 +- tests/test_worker.py | 11 +- tox.ini | 13 +- 56 files changed, 649 insertions(+), 578 deletions(-) rename pyproject.toml => .black.toml (100%) 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]