Merge remote-tracking branch 'upstream/master' into bodybytes
This commit is contained in:
commit
7704814019
1
.github/stale.yml
vendored
1
.github/stale.yml
vendored
|
@ -8,6 +8,7 @@ exemptLabels:
|
||||||
- urgent
|
- urgent
|
||||||
- necessary
|
- necessary
|
||||||
- help wanted
|
- help wanted
|
||||||
|
- RFC
|
||||||
# Label to use when marking an issue as stale
|
# Label to use when marking an issue as stale
|
||||||
staleLabel: stale
|
staleLabel: stale
|
||||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||||
|
|
13
.travis.yml
13
.travis.yml
|
@ -43,6 +43,19 @@ matrix:
|
||||||
dist: xenial
|
dist: xenial
|
||||||
sudo: true
|
sudo: true
|
||||||
name: "Python 3.7 Documentation tests"
|
name: "Python 3.7 Documentation tests"
|
||||||
|
- env: TOX_ENV=pyNightly
|
||||||
|
python: 'nightly'
|
||||||
|
name: "Python nightly with Extensions"
|
||||||
|
- env: TOX_ENV=pyNightly-no-ext
|
||||||
|
python: 'nightly'
|
||||||
|
name: "Python nightly Extensions"
|
||||||
|
allow_failures:
|
||||||
|
- env: TOX_ENV=pyNightly
|
||||||
|
python: 'nightly'
|
||||||
|
name: "Python nightly with Extensions"
|
||||||
|
- env: TOX_ENV=pyNightly-no-ext
|
||||||
|
python: 'nightly'
|
||||||
|
name: "Python nightly Extensions"
|
||||||
install:
|
install:
|
||||||
- pip install -U tox
|
- pip install -U tox
|
||||||
- pip install codecov
|
- pip install codecov
|
||||||
|
|
|
@ -1,3 +1,38 @@
|
||||||
|
Version 19.12.0
|
||||||
|
===============
|
||||||
|
|
||||||
|
Bugfixes
|
||||||
|
********
|
||||||
|
|
||||||
|
- Fix blueprint middleware application
|
||||||
|
|
||||||
|
Currently, any blueprint middleware registered, irrespective of which blueprint was used to do so, was
|
||||||
|
being applied to all of the routes created by the :code:`@app` and :code:`@blueprint` alike.
|
||||||
|
|
||||||
|
As part of this change, the blueprint based middleware application is enforced based on where they are
|
||||||
|
registered.
|
||||||
|
|
||||||
|
- If you register a middleware via :code:`@blueprint.middleware` then it will apply only to the routes defined by the blueprint.
|
||||||
|
- If you register a middleware via :code:`@blueprint_group.middleware` then it will apply to all blueprint based routes that are part of the group.
|
||||||
|
- If you define a middleware via :code:`@app.middleware` then it will be applied on all available routes (`#37 <https://github.com/huge-success/sanic/issues/37>`__)
|
||||||
|
- Fix `url_for` behavior with missing SERVER_NAME
|
||||||
|
|
||||||
|
If the `SERVER_NAME` was missing in the `app.config` entity, the `url_for` on the `request` and `app` were failing
|
||||||
|
due to an `AttributeError`. This fix makes the availability of `SERVER_NAME` on our `app.config` an optional behavior. (`#1707 <https://github.com/huge-success/sanic/issues/1707>`__)
|
||||||
|
|
||||||
|
|
||||||
|
Improved Documentation
|
||||||
|
**********************
|
||||||
|
|
||||||
|
- Move docs from RST to MD
|
||||||
|
|
||||||
|
Moved all docs from markdown to restructured text like the rest of the docs to unify the scheme and make it easier in
|
||||||
|
the future to update documentation. (`#1691 <https://github.com/huge-success/sanic/issues/1691>`__)
|
||||||
|
- Fix documentation for `get` and `getlist` of the `request.args`
|
||||||
|
|
||||||
|
Add additional example for showing the usage of `getlist` and fix the documentation string for `request.args` behavior (`#1704 <https://github.com/huge-success/sanic/issues/1704>`__)
|
||||||
|
|
||||||
|
|
||||||
Version 19.6.3
|
Version 19.6.3
|
||||||
==============
|
==============
|
||||||
|
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
Move docs from RST to MD
|
|
||||||
|
|
||||||
Moved all docs from markdown to restructured text like the rest of the docs to unify the scheme and make it easier in
|
|
||||||
the future to update documentation.
|
|
|
@ -1,3 +0,0 @@
|
||||||
Fix documentation for `get` and `getlist` of the `request.args`
|
|
||||||
|
|
||||||
Add additional example for showing the usage of `getlist` and fix the documentation string for `request.args` behavior
|
|
|
@ -1,4 +0,0 @@
|
||||||
Fix `url_for` behavior with missing SERVER_NAME
|
|
||||||
|
|
||||||
If the `SERVER_NAME` was missing in the `app.config` entity, the `url_for` on the `request` and `app` were failing
|
|
||||||
due to an `AttributeError`. This fix makes the availability of `SERVER_NAME` on our `app.config` an optional behavior.
|
|
|
@ -107,6 +107,19 @@ Response without encoding the body
|
||||||
def handle_request(request):
|
def handle_request(request):
|
||||||
return response.raw(b'raw data')
|
return response.raw(b'raw data')
|
||||||
|
|
||||||
|
Empty
|
||||||
|
--------------
|
||||||
|
|
||||||
|
For responding with an empty message as defined by `RFC 2616 <https://tools.ietf.org/search/rfc2616#section-7.2.1>`_
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic import response
|
||||||
|
|
||||||
|
@app.route('/empty')
|
||||||
|
async def handle_request(request):
|
||||||
|
return response.empty()
|
||||||
|
|
||||||
Modify headers or status
|
Modify headers or status
|
||||||
------------------------
|
------------------------
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ Testing
|
||||||
=======
|
=======
|
||||||
|
|
||||||
Sanic endpoints can be tested locally using the `test_client` object, which
|
Sanic endpoints can be tested locally using the `test_client` object, which
|
||||||
depends on the additional `requests-async <https://github.com/encode/requests-async>`_
|
depends on an additional package: `httpx <https://www.encode.io/httpx/>`_
|
||||||
library, which implements an API that mirrors the `requests` library.
|
library, which implements an API that mirrors the `requests` library.
|
||||||
|
|
||||||
The `test_client` exposes `get`, `post`, `put`, `delete`, `patch`, `head` and `options` methods
|
The `test_client` exposes `get`, `post`, `put`, `delete`, `patch`, `head` and `options` methods
|
||||||
|
@ -22,7 +22,7 @@ for you to run against your application. A simple example (using pytest) is like
|
||||||
assert response.status == 405
|
assert response.status == 405
|
||||||
|
|
||||||
Internally, each time you call one of the `test_client` methods, the Sanic app is run at `127.0.0.1:42101` and
|
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 `requests-async`.
|
your test request is executed against your application, using `httpx`.
|
||||||
|
|
||||||
The `test_client` methods accept the following arguments and keyword arguments:
|
The `test_client` methods accept the following arguments and keyword arguments:
|
||||||
|
|
||||||
|
@ -55,8 +55,8 @@ And to supply data to a JSON POST request:
|
||||||
assert request.json.get('key1') == 'value1'
|
assert request.json.get('key1') == 'value1'
|
||||||
|
|
||||||
More information about
|
More information about
|
||||||
the available arguments to `requests-async` can be found
|
the available arguments to `httpx` can be found
|
||||||
[in the documentation for `requests <https://2.python-requests.org/en/master/>`_.
|
[in the documentation for `httpx <https://www.encode.io/httpx/>`_.
|
||||||
|
|
||||||
|
|
||||||
Using a random port
|
Using a random port
|
||||||
|
|
|
@ -13,7 +13,7 @@ dependencies:
|
||||||
- sphinx==1.8.3
|
- sphinx==1.8.3
|
||||||
- sphinx_rtd_theme==0.4.2
|
- sphinx_rtd_theme==0.4.2
|
||||||
- recommonmark==0.5.0
|
- recommonmark==0.5.0
|
||||||
- requests-async==0.5.0
|
- httpx==0.9.3
|
||||||
- sphinxcontrib-asyncio>=0.2.0
|
- sphinxcontrib-asyncio>=0.2.0
|
||||||
- docutils==0.14
|
- docutils==0.14
|
||||||
- pygments==2.3.1
|
- pygments==2.3.1
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
__version__ = "19.9.0"
|
__version__ = "19.12.0"
|
||||||
|
|
80
sanic/app.py
80
sanic/app.py
|
@ -85,7 +85,8 @@ class Sanic:
|
||||||
self.is_request_stream = False
|
self.is_request_stream = False
|
||||||
self.websocket_enabled = False
|
self.websocket_enabled = False
|
||||||
self.websocket_tasks = set()
|
self.websocket_tasks = set()
|
||||||
|
self.named_request_middleware = {}
|
||||||
|
self.named_response_middleware = {}
|
||||||
# Register alternative method names
|
# Register alternative method names
|
||||||
self.go_fast = self.run
|
self.go_fast = self.run
|
||||||
|
|
||||||
|
@ -178,7 +179,7 @@ class Sanic:
|
||||||
:param stream:
|
:param stream:
|
||||||
:param version:
|
:param version:
|
||||||
:param name: user defined route name for url_for
|
:param name: user defined route name for url_for
|
||||||
:return: decorated function
|
:return: tuple of routes, decorated function
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Fix case where the user did not prefix the URL with a /
|
# Fix case where the user did not prefix the URL with a /
|
||||||
|
@ -193,6 +194,12 @@ class Sanic:
|
||||||
strict_slashes = self.strict_slashes
|
strict_slashes = self.strict_slashes
|
||||||
|
|
||||||
def response(handler):
|
def response(handler):
|
||||||
|
if isinstance(handler, tuple):
|
||||||
|
# if a handler fn is already wrapped in a route, the handler
|
||||||
|
# variable will be a tuple of (existing routes, handler fn)
|
||||||
|
routes, handler = handler
|
||||||
|
else:
|
||||||
|
routes = []
|
||||||
args = list(signature(handler).parameters.keys())
|
args = list(signature(handler).parameters.keys())
|
||||||
|
|
||||||
if not args:
|
if not args:
|
||||||
|
@ -204,6 +211,7 @@ class Sanic:
|
||||||
if stream:
|
if stream:
|
||||||
handler.is_stream = stream
|
handler.is_stream = stream
|
||||||
|
|
||||||
|
routes.extend(
|
||||||
self.router.add(
|
self.router.add(
|
||||||
uri=uri,
|
uri=uri,
|
||||||
methods=methods,
|
methods=methods,
|
||||||
|
@ -213,7 +221,8 @@ class Sanic:
|
||||||
version=version,
|
version=version,
|
||||||
name=name,
|
name=name,
|
||||||
)
|
)
|
||||||
return handler
|
)
|
||||||
|
return routes, handler
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@ -462,7 +471,7 @@ class Sanic:
|
||||||
:param subprotocols: optional list of str with supported subprotocols
|
:param subprotocols: optional list of str with supported subprotocols
|
||||||
:param name: A unique name assigned to the URL so that it can
|
:param name: A unique name assigned to the URL so that it can
|
||||||
be used with :func:`url_for`
|
be used with :func:`url_for`
|
||||||
:return: decorated function
|
:return: tuple of routes, decorated function
|
||||||
"""
|
"""
|
||||||
self.enable_websocket()
|
self.enable_websocket()
|
||||||
|
|
||||||
|
@ -475,6 +484,13 @@ class Sanic:
|
||||||
strict_slashes = self.strict_slashes
|
strict_slashes = self.strict_slashes
|
||||||
|
|
||||||
def response(handler):
|
def response(handler):
|
||||||
|
if isinstance(handler, tuple):
|
||||||
|
# if a handler fn is already wrapped in a route, the handler
|
||||||
|
# variable will be a tuple of (existing routes, handler fn)
|
||||||
|
routes, handler = handler
|
||||||
|
else:
|
||||||
|
routes = []
|
||||||
|
|
||||||
async def websocket_handler(request, *args, **kwargs):
|
async def websocket_handler(request, *args, **kwargs):
|
||||||
request.app = self
|
request.app = self
|
||||||
if not getattr(handler, "__blueprintname__", False):
|
if not getattr(handler, "__blueprintname__", False):
|
||||||
|
@ -515,6 +531,7 @@ class Sanic:
|
||||||
self.websocket_tasks.remove(fut)
|
self.websocket_tasks.remove(fut)
|
||||||
await ws.close()
|
await ws.close()
|
||||||
|
|
||||||
|
routes.extend(
|
||||||
self.router.add(
|
self.router.add(
|
||||||
uri=uri,
|
uri=uri,
|
||||||
handler=websocket_handler,
|
handler=websocket_handler,
|
||||||
|
@ -523,7 +540,8 @@ class Sanic:
|
||||||
strict_slashes=strict_slashes,
|
strict_slashes=strict_slashes,
|
||||||
name=name,
|
name=name,
|
||||||
)
|
)
|
||||||
return handler
|
)
|
||||||
|
return routes, handler
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@ -544,6 +562,7 @@ class Sanic:
|
||||||
:param host: Host IP or FQDN details
|
:param host: Host IP or FQDN details
|
||||||
:param uri: URL path that will be mapped to the websocket
|
:param uri: URL path that will be mapped to the websocket
|
||||||
handler
|
handler
|
||||||
|
handler
|
||||||
:param strict_slashes: If the API endpoint needs to terminate
|
:param strict_slashes: If the API endpoint needs to terminate
|
||||||
with a "/" or not
|
with a "/" or not
|
||||||
:param subprotocols: Subprotocols to be used with websocket
|
:param subprotocols: Subprotocols to be used with websocket
|
||||||
|
@ -645,6 +664,22 @@ class Sanic:
|
||||||
self.response_middleware.appendleft(middleware)
|
self.response_middleware.appendleft(middleware)
|
||||||
return middleware
|
return middleware
|
||||||
|
|
||||||
|
def register_named_middleware(
|
||||||
|
self, middleware, route_names, attach_to="request"
|
||||||
|
):
|
||||||
|
if attach_to == "request":
|
||||||
|
for _rn in route_names:
|
||||||
|
if _rn not in self.named_request_middleware:
|
||||||
|
self.named_request_middleware[_rn] = deque()
|
||||||
|
if middleware not in self.named_request_middleware[_rn]:
|
||||||
|
self.named_request_middleware[_rn].append(middleware)
|
||||||
|
if attach_to == "response":
|
||||||
|
for _rn in route_names:
|
||||||
|
if _rn not in self.named_response_middleware:
|
||||||
|
self.named_response_middleware[_rn] = deque()
|
||||||
|
if middleware not in self.named_response_middleware[_rn]:
|
||||||
|
self.named_response_middleware[_rn].append(middleware)
|
||||||
|
|
||||||
# Decorator
|
# Decorator
|
||||||
def middleware(self, middleware_or_request):
|
def middleware(self, middleware_or_request):
|
||||||
"""
|
"""
|
||||||
|
@ -916,20 +951,23 @@ class Sanic:
|
||||||
# allocation before assignment below.
|
# allocation before assignment below.
|
||||||
response = None
|
response = None
|
||||||
cancelled = False
|
cancelled = False
|
||||||
|
name = None
|
||||||
try:
|
try:
|
||||||
|
# Fetch handler from router
|
||||||
|
handler, args, kwargs, uri, name = self.router.get(request)
|
||||||
|
|
||||||
# -------------------------------------------- #
|
# -------------------------------------------- #
|
||||||
# Request Middleware
|
# Request Middleware
|
||||||
# -------------------------------------------- #
|
# -------------------------------------------- #
|
||||||
response = await self._run_request_middleware(request)
|
response = await self._run_request_middleware(
|
||||||
|
request, request_name=name
|
||||||
|
)
|
||||||
# No middleware results
|
# No middleware results
|
||||||
if not response:
|
if not response:
|
||||||
# -------------------------------------------- #
|
# -------------------------------------------- #
|
||||||
# Execute Handler
|
# Execute Handler
|
||||||
# -------------------------------------------- #
|
# -------------------------------------------- #
|
||||||
|
|
||||||
# Fetch handler from router
|
|
||||||
handler, args, kwargs, uri = self.router.get(request)
|
|
||||||
|
|
||||||
request.uri_template = uri
|
request.uri_template = uri
|
||||||
if handler is None:
|
if handler is None:
|
||||||
raise ServerError(
|
raise ServerError(
|
||||||
|
@ -993,7 +1031,7 @@ class Sanic:
|
||||||
if response is not None:
|
if response is not None:
|
||||||
try:
|
try:
|
||||||
response = await self._run_response_middleware(
|
response = await self._run_response_middleware(
|
||||||
request, response
|
request, response, request_name=name
|
||||||
)
|
)
|
||||||
except CancelledError:
|
except CancelledError:
|
||||||
# Response middleware can timeout too, as above.
|
# Response middleware can timeout too, as above.
|
||||||
|
@ -1265,10 +1303,14 @@ class Sanic:
|
||||||
if isawaitable(result):
|
if isawaitable(result):
|
||||||
await result
|
await result
|
||||||
|
|
||||||
async def _run_request_middleware(self, request):
|
async def _run_request_middleware(self, request, request_name=None):
|
||||||
# The if improves speed. I don't know why
|
# The if improves speed. I don't know why
|
||||||
if self.request_middleware:
|
named_middleware = self.named_request_middleware.get(
|
||||||
for middleware in self.request_middleware:
|
request_name, deque()
|
||||||
|
)
|
||||||
|
applicable_middleware = self.request_middleware + named_middleware
|
||||||
|
if applicable_middleware:
|
||||||
|
for middleware in applicable_middleware:
|
||||||
response = middleware(request)
|
response = middleware(request)
|
||||||
if isawaitable(response):
|
if isawaitable(response):
|
||||||
response = await response
|
response = await response
|
||||||
|
@ -1276,9 +1318,15 @@ class Sanic:
|
||||||
return response
|
return response
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def _run_response_middleware(self, request, response):
|
async def _run_response_middleware(
|
||||||
if self.response_middleware:
|
self, request, response, request_name=None
|
||||||
for middleware in self.response_middleware:
|
):
|
||||||
|
named_middleware = self.named_response_middleware.get(
|
||||||
|
request_name, deque()
|
||||||
|
)
|
||||||
|
applicable_middleware = self.response_middleware + named_middleware
|
||||||
|
if applicable_middleware:
|
||||||
|
for middleware in applicable_middleware:
|
||||||
_response = middleware(request, response)
|
_response = middleware(request, response)
|
||||||
if isawaitable(_response):
|
if isawaitable(_response):
|
||||||
_response = await _response
|
_response = await _response
|
||||||
|
|
|
@ -15,8 +15,6 @@ from typing import (
|
||||||
)
|
)
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
|
||||||
from requests_async import ASGISession # type: ignore
|
|
||||||
|
|
||||||
import sanic.app # noqa
|
import sanic.app # noqa
|
||||||
|
|
||||||
from sanic.compat import Header
|
from sanic.compat import Header
|
||||||
|
@ -189,7 +187,7 @@ class Lifespan:
|
||||||
|
|
||||||
|
|
||||||
class ASGIApp:
|
class ASGIApp:
|
||||||
sanic_app: Union[ASGISession, "sanic.app.Sanic"]
|
sanic_app: "sanic.app.Sanic"
|
||||||
request: Request
|
request: Request
|
||||||
transport: MockTransport
|
transport: MockTransport
|
||||||
do_stream: bool
|
do_stream: bool
|
||||||
|
@ -223,8 +221,13 @@ class ASGIApp:
|
||||||
if scope["type"] == "lifespan":
|
if scope["type"] == "lifespan":
|
||||||
await instance.lifespan(scope, receive, send)
|
await instance.lifespan(scope, receive, send)
|
||||||
else:
|
else:
|
||||||
url_bytes = scope.get("root_path", "") + quote(scope["path"])
|
path = (
|
||||||
url_bytes = url_bytes.encode("latin-1")
|
scope["path"][1:]
|
||||||
|
if scope["path"].startswith("/")
|
||||||
|
else scope["path"]
|
||||||
|
)
|
||||||
|
url = "/".join([scope.get("root_path", ""), quote(path)])
|
||||||
|
url_bytes = url.encode("latin-1")
|
||||||
url_bytes += b"?" + scope["query_string"]
|
url_bytes += b"?" + scope["query_string"]
|
||||||
|
|
||||||
if scope["type"] == "http":
|
if scope["type"] == "http":
|
||||||
|
|
|
@ -104,6 +104,8 @@ class Blueprint:
|
||||||
|
|
||||||
url_prefix = options.get("url_prefix", self.url_prefix)
|
url_prefix = options.get("url_prefix", self.url_prefix)
|
||||||
|
|
||||||
|
routes = []
|
||||||
|
|
||||||
# Routes
|
# Routes
|
||||||
for future in self.routes:
|
for future in self.routes:
|
||||||
# attach the blueprint name to the handler so that it can be
|
# attach the blueprint name to the handler so that it can be
|
||||||
|
@ -114,7 +116,7 @@ class Blueprint:
|
||||||
|
|
||||||
version = future.version or self.version
|
version = future.version or self.version
|
||||||
|
|
||||||
app.route(
|
_routes, _ = app.route(
|
||||||
uri=uri[1:] if uri.startswith("//") else uri,
|
uri=uri[1:] if uri.startswith("//") else uri,
|
||||||
methods=future.methods,
|
methods=future.methods,
|
||||||
host=future.host or self.host,
|
host=future.host or self.host,
|
||||||
|
@ -123,6 +125,8 @@ class Blueprint:
|
||||||
version=version,
|
version=version,
|
||||||
name=future.name,
|
name=future.name,
|
||||||
)(future.handler)
|
)(future.handler)
|
||||||
|
if _routes:
|
||||||
|
routes += _routes
|
||||||
|
|
||||||
for future in self.websocket_routes:
|
for future in self.websocket_routes:
|
||||||
# attach the blueprint name to the handler so that it can be
|
# attach the blueprint name to the handler so that it can be
|
||||||
|
@ -130,21 +134,27 @@ class Blueprint:
|
||||||
future.handler.__blueprintname__ = self.name
|
future.handler.__blueprintname__ = self.name
|
||||||
# Prepend the blueprint URI prefix if available
|
# Prepend the blueprint URI prefix if available
|
||||||
uri = url_prefix + future.uri if url_prefix else future.uri
|
uri = url_prefix + future.uri if url_prefix else future.uri
|
||||||
app.websocket(
|
_routes, _ = app.websocket(
|
||||||
uri=uri,
|
uri=uri,
|
||||||
host=future.host or self.host,
|
host=future.host or self.host,
|
||||||
strict_slashes=future.strict_slashes,
|
strict_slashes=future.strict_slashes,
|
||||||
name=future.name,
|
name=future.name,
|
||||||
)(future.handler)
|
)(future.handler)
|
||||||
|
if _routes:
|
||||||
|
routes += _routes
|
||||||
|
|
||||||
|
route_names = [route.name for route in routes]
|
||||||
# Middleware
|
# Middleware
|
||||||
for future in self.middlewares:
|
for future in self.middlewares:
|
||||||
if future.args or future.kwargs:
|
if future.args or future.kwargs:
|
||||||
app.register_middleware(
|
app.register_named_middleware(
|
||||||
future.middleware, *future.args, **future.kwargs
|
future.middleware,
|
||||||
|
route_names,
|
||||||
|
*future.args,
|
||||||
|
**future.kwargs
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
app.register_middleware(future.middleware)
|
app.register_named_middleware(future.middleware, route_names)
|
||||||
|
|
||||||
# Exceptions
|
# Exceptions
|
||||||
for future in self.exceptions:
|
for future in self.exceptions:
|
||||||
|
|
|
@ -5,7 +5,6 @@ from urllib.parse import unquote
|
||||||
|
|
||||||
from sanic.helpers import STATUS_CODES
|
from sanic.helpers import STATUS_CODES
|
||||||
|
|
||||||
|
|
||||||
HeaderIterable = Iterable[Tuple[str, Any]] # Values convertible to str
|
HeaderIterable = Iterable[Tuple[str, Any]] # Values convertible to str
|
||||||
Options = Dict[str, Union[int, str]] # key=value fields in various headers
|
Options = Dict[str, Union[int, str]] # key=value fields in various headers
|
||||||
OptionsIterable = Iterable[Tuple[str, str]] # May contain duplicate keys
|
OptionsIterable = Iterable[Tuple[str, str]] # May contain duplicate keys
|
||||||
|
@ -192,8 +191,6 @@ def format_http1_response(
|
||||||
- If `body` is included, content-length must be specified in headers.
|
- If `body` is included, content-length must be specified in headers.
|
||||||
"""
|
"""
|
||||||
headerbytes = format_http1(headers)
|
headerbytes = format_http1(headers)
|
||||||
if status == 200:
|
|
||||||
return b"HTTP/1.1 200 OK\r\n%b\r\n%b" % (headerbytes, body)
|
|
||||||
return b"HTTP/1.1 %d %b\r\n%b\r\n%b" % (
|
return b"HTTP/1.1 %d %b\r\n%b\r\n%b" % (
|
||||||
status,
|
status,
|
||||||
STATUS_CODES.get(status, b"UNKNOWN"),
|
STATUS_CODES.get(status, b"UNKNOWN"),
|
||||||
|
|
|
@ -98,7 +98,10 @@ class StreamingHTTPResponse(BaseHTTPResponse):
|
||||||
def get_headers(
|
def get_headers(
|
||||||
self, version="1.1", keep_alive=False, keep_alive_timeout=None
|
self, version="1.1", keep_alive=False, keep_alive_timeout=None
|
||||||
):
|
):
|
||||||
if "Content-Type" not in self.headers:
|
assert version == "1.1", "No other versions are currently supported"
|
||||||
|
|
||||||
|
# self.headers get priority over content_type
|
||||||
|
if self.content_type and "Content-Type" not in self.headers:
|
||||||
self.headers["Content-Type"] = self.content_type
|
self.headers["Content-Type"] = self.content_type
|
||||||
|
|
||||||
if keep_alive and keep_alive_timeout is not None:
|
if keep_alive and keep_alive_timeout is not None:
|
||||||
|
@ -119,7 +122,7 @@ class HTTPResponse(BaseHTTPResponse):
|
||||||
body=None,
|
body=None,
|
||||||
status=200,
|
status=200,
|
||||||
headers=None,
|
headers=None,
|
||||||
content_type="text/plain; charset=utf-8",
|
content_type=None,
|
||||||
body_bytes=b"",
|
body_bytes=b"",
|
||||||
):
|
):
|
||||||
self.content_type = content_type
|
self.content_type = content_type
|
||||||
|
@ -129,9 +132,7 @@ class HTTPResponse(BaseHTTPResponse):
|
||||||
self._cookies = None
|
self._cookies = None
|
||||||
|
|
||||||
def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None):
|
def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None):
|
||||||
if "Content-Type" not in self.headers:
|
assert version == "1.1", "No other versions are currently supported"
|
||||||
self.headers["Content-Type"] = self.content_type
|
|
||||||
|
|
||||||
body = b""
|
body = b""
|
||||||
if has_message_body(self.status):
|
if has_message_body(self.status):
|
||||||
body = self.body
|
body = self.body
|
||||||
|
@ -139,15 +140,13 @@ class HTTPResponse(BaseHTTPResponse):
|
||||||
"Content-Length", len(self.body)
|
"Content-Length", len(self.body)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# self.headers get priority over content_type
|
||||||
|
if self.content_type and "Content-Type" not in self.headers:
|
||||||
|
self.headers["Content-Type"] = self.content_type
|
||||||
|
|
||||||
if self.status in (304, 412):
|
if self.status in (304, 412):
|
||||||
self.headers = remove_entity_headers(self.headers)
|
self.headers = remove_entity_headers(self.headers)
|
||||||
|
|
||||||
if keep_alive and keep_alive_timeout is not None:
|
|
||||||
self.headers["Connection"] = "keep-alive"
|
|
||||||
self.headers["Keep-Alive"] = keep_alive_timeout
|
|
||||||
elif not keep_alive:
|
|
||||||
self.headers["Connection"] = "close"
|
|
||||||
|
|
||||||
return format_http1_response(self.status, self.headers.items(), body)
|
return format_http1_response(self.status, self.headers.items(), body)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -157,6 +156,16 @@ class HTTPResponse(BaseHTTPResponse):
|
||||||
return self._cookies
|
return self._cookies
|
||||||
|
|
||||||
|
|
||||||
|
def empty(status=204, headers=None):
|
||||||
|
"""
|
||||||
|
Returns an empty response to the client.
|
||||||
|
|
||||||
|
:param status Response code.
|
||||||
|
:param headers Custom Headers.
|
||||||
|
"""
|
||||||
|
return HTTPResponse(body_bytes=b"", status=status, headers=headers)
|
||||||
|
|
||||||
|
|
||||||
def json(
|
def json(
|
||||||
body,
|
body,
|
||||||
status=200,
|
status=200,
|
||||||
|
|
|
@ -140,21 +140,22 @@ class Router:
|
||||||
docs for further details.
|
docs for further details.
|
||||||
:return: Nothing
|
:return: Nothing
|
||||||
"""
|
"""
|
||||||
|
routes = []
|
||||||
if version is not None:
|
if version is not None:
|
||||||
version = re.escape(str(version).strip("/").lstrip("v"))
|
version = re.escape(str(version).strip("/").lstrip("v"))
|
||||||
uri = "/".join(["/v{}".format(version), uri.lstrip("/")])
|
uri = "/".join(["/v{}".format(version), uri.lstrip("/")])
|
||||||
# add regular version
|
# add regular version
|
||||||
self._add(uri, methods, handler, host, name)
|
routes.append(self._add(uri, methods, handler, host, name))
|
||||||
|
|
||||||
if strict_slashes:
|
if strict_slashes:
|
||||||
return
|
return routes
|
||||||
|
|
||||||
if not isinstance(host, str) and host is not None:
|
if not isinstance(host, str) and host is not None:
|
||||||
# we have gotten back to the top of the recursion tree where the
|
# we have gotten back to the top of the recursion tree where the
|
||||||
# host was originally a list. By now, we've processed the strict
|
# host was originally a list. By now, we've processed the strict
|
||||||
# slashes logic on the leaf nodes (the individual host strings in
|
# slashes logic on the leaf nodes (the individual host strings in
|
||||||
# the list of host)
|
# the list of host)
|
||||||
return
|
return routes
|
||||||
|
|
||||||
# Add versions with and without trailing /
|
# Add versions with and without trailing /
|
||||||
slashed_methods = self.routes_all.get(uri + "/", frozenset({}))
|
slashed_methods = self.routes_all.get(uri + "/", frozenset({}))
|
||||||
|
@ -176,10 +177,12 @@ class Router:
|
||||||
)
|
)
|
||||||
# add version with trailing slash
|
# add version with trailing slash
|
||||||
if slash_is_missing:
|
if slash_is_missing:
|
||||||
self._add(uri + "/", methods, handler, host, name)
|
routes.append(self._add(uri + "/", methods, handler, host, name))
|
||||||
# add version without trailing slash
|
# add version without trailing slash
|
||||||
elif without_slash_is_missing:
|
elif without_slash_is_missing:
|
||||||
self._add(uri[:-1], methods, handler, host, name)
|
routes.append(self._add(uri[:-1], methods, handler, host, name))
|
||||||
|
|
||||||
|
return routes
|
||||||
|
|
||||||
def _add(self, uri, methods, handler, host=None, name=None):
|
def _add(self, uri, methods, handler, host=None, name=None):
|
||||||
"""Add a handler to the route list
|
"""Add a handler to the route list
|
||||||
|
@ -328,6 +331,7 @@ class Router:
|
||||||
self.routes_dynamic[url_hash(uri)].append(route)
|
self.routes_dynamic[url_hash(uri)].append(route)
|
||||||
else:
|
else:
|
||||||
self.routes_static[uri] = route
|
self.routes_static[uri] = route
|
||||||
|
return route
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def check_dynamic_route_exists(pattern, routes_to_check, parameters):
|
def check_dynamic_route_exists(pattern, routes_to_check, parameters):
|
||||||
|
@ -442,6 +446,7 @@ class Router:
|
||||||
method=method,
|
method=method,
|
||||||
allowed_methods=self.get_supported_methods(url),
|
allowed_methods=self.get_supported_methods(url),
|
||||||
)
|
)
|
||||||
|
|
||||||
if route:
|
if route:
|
||||||
if route.methods and method not in route.methods:
|
if route.methods and method not in route.methods:
|
||||||
raise method_not_supported
|
raise method_not_supported
|
||||||
|
@ -476,7 +481,7 @@ class Router:
|
||||||
route_handler = route.handler
|
route_handler = route.handler
|
||||||
if hasattr(route_handler, "handlers"):
|
if hasattr(route_handler, "handlers"):
|
||||||
route_handler = route_handler.handlers[method]
|
route_handler = route_handler.handlers[method]
|
||||||
return route_handler, [], kwargs, route.uri
|
return route_handler, [], kwargs, route.uri, route.name
|
||||||
|
|
||||||
def is_stream_handler(self, request):
|
def is_stream_handler(self, request):
|
||||||
""" Handler for request is stream or not.
|
""" Handler for request is stream or not.
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from collections import deque
|
from collections import deque
|
||||||
|
@ -87,6 +88,7 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
"_header_fragment",
|
"_header_fragment",
|
||||||
"state",
|
"state",
|
||||||
"_debug",
|
"_debug",
|
||||||
|
"_body_chunks",
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -133,6 +135,9 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
self.request_class = request_class or Request
|
self.request_class = request_class or Request
|
||||||
self.is_request_stream = is_request_stream
|
self.is_request_stream = is_request_stream
|
||||||
self._is_stream_handler = False
|
self._is_stream_handler = False
|
||||||
|
if sys.version_info.minor >= 8:
|
||||||
|
self._not_paused = asyncio.Event()
|
||||||
|
else:
|
||||||
self._not_paused = asyncio.Event(loop=loop)
|
self._not_paused = asyncio.Event(loop=loop)
|
||||||
self._total_request_size = 0
|
self._total_request_size = 0
|
||||||
self._request_timeout_handler = None
|
self._request_timeout_handler = None
|
||||||
|
@ -364,6 +369,21 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
else:
|
else:
|
||||||
self.request.body_push(body)
|
self.request.body_push(body)
|
||||||
|
|
||||||
|
async def body_append(self, body):
|
||||||
|
if (
|
||||||
|
self.request is None
|
||||||
|
or self._request_stream_task is None
|
||||||
|
or self._request_stream_task.cancelled()
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.request.stream.is_full():
|
||||||
|
self.transport.pause_reading()
|
||||||
|
await self.request.stream.put(body)
|
||||||
|
self.transport.resume_reading()
|
||||||
|
else:
|
||||||
|
await self.request.stream.put(body)
|
||||||
|
|
||||||
async def stream_append(self):
|
async def stream_append(self):
|
||||||
while self._body_chunks:
|
while self._body_chunks:
|
||||||
body = self._body_chunks.popleft()
|
body = self._body_chunks.popleft()
|
||||||
|
@ -935,7 +955,10 @@ def serve(
|
||||||
else:
|
else:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
if sys.version_info.minor >= 8:
|
||||||
_shutdown = asyncio.gather(*coros, loop=loop)
|
_shutdown = asyncio.gather(*coros, loop=loop)
|
||||||
|
else:
|
||||||
|
_shutdown = asyncio.gather(*coros)
|
||||||
loop.run_until_complete(_shutdown)
|
loop.run_until_complete(_shutdown)
|
||||||
|
|
||||||
trigger_events(after_stop, loop)
|
trigger_events(after_stop, loop)
|
||||||
|
|
270
sanic/testing.py
270
sanic/testing.py
|
@ -1,14 +1,8 @@
|
||||||
import asyncio
|
|
||||||
import types
|
|
||||||
import typing
|
|
||||||
|
|
||||||
from json import JSONDecodeError
|
from json import JSONDecodeError
|
||||||
from socket import socket
|
from socket import socket
|
||||||
from urllib.parse import unquote, urlsplit
|
|
||||||
|
|
||||||
import httpcore # type: ignore
|
import httpx
|
||||||
import requests_async as requests # type: ignore
|
import websockets
|
||||||
import websockets # type: ignore
|
|
||||||
|
|
||||||
from sanic.asgi import ASGIApp
|
from sanic.asgi import ASGIApp
|
||||||
from sanic.exceptions import MethodNotSupported
|
from sanic.exceptions import MethodNotSupported
|
||||||
|
@ -22,13 +16,14 @@ PORT = 42101
|
||||||
|
|
||||||
|
|
||||||
class SanicTestClient:
|
class SanicTestClient:
|
||||||
def __init__(self, app, port=PORT):
|
def __init__(self, app, port=PORT, host=HOST):
|
||||||
"""Use port=None to bind to a random port"""
|
"""Use port=None to bind to a random port"""
|
||||||
self.app = app
|
self.app = app
|
||||||
self.port = port
|
self.port = port
|
||||||
|
self.host = host
|
||||||
|
|
||||||
def get_new_session(self):
|
def get_new_session(self):
|
||||||
return requests.Session()
|
return httpx.Client()
|
||||||
|
|
||||||
async def _local_request(self, method, url, *args, **kwargs):
|
async def _local_request(self, method, url, *args, **kwargs):
|
||||||
logger.info(url)
|
logger.info(url)
|
||||||
|
@ -59,7 +54,8 @@ class SanicTestClient:
|
||||||
|
|
||||||
if raw_cookies:
|
if raw_cookies:
|
||||||
response.raw_cookies = {}
|
response.raw_cookies = {}
|
||||||
for cookie in response.cookies:
|
|
||||||
|
for cookie in response.cookies.jar:
|
||||||
response.raw_cookies[cookie.name] = cookie
|
response.raw_cookies[cookie.name] = cookie
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
@ -71,6 +67,7 @@ class SanicTestClient:
|
||||||
gather_request=True,
|
gather_request=True,
|
||||||
debug=False,
|
debug=False,
|
||||||
server_kwargs={"auto_reload": False},
|
server_kwargs={"auto_reload": False},
|
||||||
|
host=None,
|
||||||
*request_args,
|
*request_args,
|
||||||
**request_kwargs,
|
**request_kwargs,
|
||||||
):
|
):
|
||||||
|
@ -95,11 +92,13 @@ class SanicTestClient:
|
||||||
return self.app.error_handler.default(request, exception)
|
return self.app.error_handler.default(request, exception)
|
||||||
|
|
||||||
if self.port:
|
if self.port:
|
||||||
server_kwargs = dict(host=HOST, port=self.port, **server_kwargs)
|
server_kwargs = dict(
|
||||||
host, port = HOST, self.port
|
host=host or self.host, port=self.port, **server_kwargs
|
||||||
|
)
|
||||||
|
host, port = host or self.host, self.port
|
||||||
else:
|
else:
|
||||||
sock = socket()
|
sock = socket()
|
||||||
sock.bind((HOST, 0))
|
sock.bind((host or self.host, 0))
|
||||||
server_kwargs = dict(sock=sock, **server_kwargs)
|
server_kwargs = dict(sock=sock, **server_kwargs)
|
||||||
host, port = sock.getsockname()
|
host, port = sock.getsockname()
|
||||||
|
|
||||||
|
@ -175,181 +174,6 @@ class SanicTestClient:
|
||||||
return self._sanic_endpoint_test("websocket", *args, **kwargs)
|
return self._sanic_endpoint_test("websocket", *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class SanicASGIAdapter(requests.asgi.ASGIAdapter): # noqa
|
|
||||||
async def send( # type: ignore
|
|
||||||
self,
|
|
||||||
request: requests.PreparedRequest,
|
|
||||||
gather_return: bool = False,
|
|
||||||
*args: typing.Any,
|
|
||||||
**kwargs: typing.Any,
|
|
||||||
) -> requests.Response:
|
|
||||||
"""This method is taken MOSTLY verbatim from requests-asyn. The
|
|
||||||
difference is the capturing of a response on the ASGI call and then
|
|
||||||
returning it on the response object. This is implemented to achieve:
|
|
||||||
|
|
||||||
request, response = await app.asgi_client.get("/")
|
|
||||||
|
|
||||||
You can see the original code here:
|
|
||||||
https://github.com/encode/requests-async/blob/614f40f77f19e6c6da8a212ae799107b0384dbf9/requests_async/asgi.py#L51""" # noqa
|
|
||||||
scheme, netloc, path, query, fragment = urlsplit(
|
|
||||||
request.url
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
default_port = {"http": 80, "ws": 80, "https": 443, "wss": 443}[scheme]
|
|
||||||
|
|
||||||
if ":" in netloc:
|
|
||||||
host, port_string = netloc.split(":", 1)
|
|
||||||
port = int(port_string)
|
|
||||||
else:
|
|
||||||
host = netloc
|
|
||||||
port = default_port
|
|
||||||
|
|
||||||
# Include the 'host' header.
|
|
||||||
if "host" in request.headers:
|
|
||||||
headers = [] # type: typing.List[typing.Tuple[bytes, bytes]]
|
|
||||||
elif port == default_port:
|
|
||||||
headers = [(b"host", host.encode())]
|
|
||||||
else:
|
|
||||||
headers = [(b"host", (f"{host}:{port}").encode())]
|
|
||||||
|
|
||||||
# Include other request headers.
|
|
||||||
headers += [
|
|
||||||
(key.lower().encode(), value.encode())
|
|
||||||
for key, value in request.headers.items()
|
|
||||||
]
|
|
||||||
|
|
||||||
no_response = False
|
|
||||||
if scheme in {"ws", "wss"}:
|
|
||||||
subprotocol = request.headers.get("sec-websocket-protocol", None)
|
|
||||||
if subprotocol is None:
|
|
||||||
subprotocols = [] # type: typing.Sequence[str]
|
|
||||||
else:
|
|
||||||
subprotocols = [
|
|
||||||
value.strip() for value in subprotocol.split(",")
|
|
||||||
]
|
|
||||||
|
|
||||||
scope = {
|
|
||||||
"type": "websocket",
|
|
||||||
"path": unquote(path),
|
|
||||||
"root_path": "",
|
|
||||||
"scheme": scheme,
|
|
||||||
"query_string": query.encode(),
|
|
||||||
"headers": headers,
|
|
||||||
"client": ["testclient", 50000],
|
|
||||||
"server": [host, port],
|
|
||||||
"subprotocols": subprotocols,
|
|
||||||
}
|
|
||||||
no_response = True
|
|
||||||
|
|
||||||
else:
|
|
||||||
scope = {
|
|
||||||
"type": "http",
|
|
||||||
"http_version": "1.1",
|
|
||||||
"method": request.method,
|
|
||||||
"path": unquote(path),
|
|
||||||
"root_path": "",
|
|
||||||
"scheme": scheme,
|
|
||||||
"query_string": query.encode(),
|
|
||||||
"headers": headers,
|
|
||||||
"client": ["testclient", 50000],
|
|
||||||
"server": [host, port],
|
|
||||||
"extensions": {"http.response.template": {}},
|
|
||||||
}
|
|
||||||
|
|
||||||
async def receive():
|
|
||||||
nonlocal request_complete, response_complete
|
|
||||||
|
|
||||||
if request_complete:
|
|
||||||
while not response_complete:
|
|
||||||
await asyncio.sleep(0.0001)
|
|
||||||
return {"type": "http.disconnect"}
|
|
||||||
|
|
||||||
body = request.body
|
|
||||||
if isinstance(body, str):
|
|
||||||
body_bytes = body.encode("utf-8") # type: bytes
|
|
||||||
elif body is None:
|
|
||||||
body_bytes = b""
|
|
||||||
elif isinstance(body, types.GeneratorType):
|
|
||||||
try:
|
|
||||||
chunk = body.send(None)
|
|
||||||
if isinstance(chunk, str):
|
|
||||||
chunk = chunk.encode("utf-8")
|
|
||||||
return {
|
|
||||||
"type": "http.request",
|
|
||||||
"body": chunk,
|
|
||||||
"more_body": True,
|
|
||||||
}
|
|
||||||
except StopIteration:
|
|
||||||
request_complete = True
|
|
||||||
return {"type": "http.request", "body": b""}
|
|
||||||
else:
|
|
||||||
body_bytes = body
|
|
||||||
|
|
||||||
request_complete = True
|
|
||||||
return {"type": "http.request", "body": body_bytes}
|
|
||||||
|
|
||||||
request_complete = False
|
|
||||||
response_started = False
|
|
||||||
response_complete = False
|
|
||||||
raw_kwargs = {"content": b""} # type: typing.Dict[str, typing.Any]
|
|
||||||
template = None
|
|
||||||
context = None
|
|
||||||
return_value = None
|
|
||||||
|
|
||||||
async def send(message) -> None:
|
|
||||||
nonlocal raw_kwargs, response_started, response_complete, template, context # noqa
|
|
||||||
|
|
||||||
if message["type"] == "http.response.start":
|
|
||||||
assert (
|
|
||||||
not response_started
|
|
||||||
), 'Received multiple "http.response.start" messages.'
|
|
||||||
raw_kwargs["status_code"] = message["status"]
|
|
||||||
raw_kwargs["headers"] = message["headers"]
|
|
||||||
response_started = True
|
|
||||||
elif message["type"] == "http.response.body":
|
|
||||||
assert response_started, (
|
|
||||||
'Received "http.response.body" '
|
|
||||||
'without "http.response.start".'
|
|
||||||
)
|
|
||||||
assert (
|
|
||||||
not response_complete
|
|
||||||
), 'Received "http.response.body" after response completed.'
|
|
||||||
body = message.get("body", b"")
|
|
||||||
more_body = message.get("more_body", False)
|
|
||||||
if request.method != "HEAD":
|
|
||||||
raw_kwargs["content"] += body
|
|
||||||
if not more_body:
|
|
||||||
response_complete = True
|
|
||||||
elif message["type"] == "http.response.template":
|
|
||||||
template = message["template"]
|
|
||||||
context = message["context"]
|
|
||||||
|
|
||||||
try:
|
|
||||||
return_value = await self.app(scope, receive, send)
|
|
||||||
except BaseException as exc:
|
|
||||||
if not self.suppress_exceptions:
|
|
||||||
raise exc from None
|
|
||||||
|
|
||||||
if no_response:
|
|
||||||
response_started = True
|
|
||||||
raw_kwargs = {"status_code": 204, "headers": []}
|
|
||||||
|
|
||||||
if not self.suppress_exceptions:
|
|
||||||
assert response_started, "TestClient did not receive any response."
|
|
||||||
elif not response_started:
|
|
||||||
raw_kwargs = {"status_code": 500, "headers": []}
|
|
||||||
|
|
||||||
raw = httpcore.Response(**raw_kwargs)
|
|
||||||
response = self.build_response(request, raw)
|
|
||||||
if template is not None:
|
|
||||||
response.template = template
|
|
||||||
response.context = context
|
|
||||||
|
|
||||||
if gather_return:
|
|
||||||
response.return_value = return_value
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
class TestASGIApp(ASGIApp):
|
class TestASGIApp(ASGIApp):
|
||||||
async def __call__(self):
|
async def __call__(self):
|
||||||
await super().__call__()
|
await super().__call__()
|
||||||
|
@ -361,7 +185,11 @@ async def app_call_with_return(self, scope, receive, send):
|
||||||
return await asgi_app()
|
return await asgi_app()
|
||||||
|
|
||||||
|
|
||||||
class SanicASGITestClient(requests.ASGISession):
|
class SanicASGIDispatch(httpx.dispatch.ASGIDispatch):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SanicASGITestClient(httpx.Client):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
app,
|
app,
|
||||||
|
@ -370,18 +198,18 @@ class SanicASGITestClient(requests.ASGISession):
|
||||||
) -> None:
|
) -> None:
|
||||||
app.__class__.__call__ = app_call_with_return
|
app.__class__.__call__ = app_call_with_return
|
||||||
app.asgi = True
|
app.asgi = True
|
||||||
super().__init__(app)
|
|
||||||
|
|
||||||
adapter = SanicASGIAdapter(
|
|
||||||
app, suppress_exceptions=suppress_exceptions
|
|
||||||
)
|
|
||||||
self.mount("http://", adapter)
|
|
||||||
self.mount("https://", adapter)
|
|
||||||
self.mount("ws://", adapter)
|
|
||||||
self.mount("wss://", adapter)
|
|
||||||
self.headers.update({"user-agent": "testclient"})
|
|
||||||
self.app = app
|
self.app = app
|
||||||
self.base_url = base_url
|
|
||||||
|
dispatch = SanicASGIDispatch(app=app, client=(ASGI_HOST, PORT))
|
||||||
|
super().__init__(dispatch=dispatch, base_url=base_url)
|
||||||
|
|
||||||
|
self.last_request = None
|
||||||
|
|
||||||
|
def _collect_request(request):
|
||||||
|
self.last_request = request
|
||||||
|
|
||||||
|
app.request_middleware.appendleft(_collect_request)
|
||||||
|
|
||||||
async def request(self, method, url, gather_request=True, *args, **kwargs):
|
async def request(self, method, url, gather_request=True, *args, **kwargs):
|
||||||
|
|
||||||
|
@ -391,24 +219,12 @@ class SanicASGITestClient(requests.ASGISession):
|
||||||
response.body = response.content
|
response.body = response.content
|
||||||
response.content_type = response.headers.get("content-type")
|
response.content_type = response.headers.get("content-type")
|
||||||
|
|
||||||
if hasattr(response, "return_value"):
|
return self.last_request, response
|
||||||
request = response.return_value
|
|
||||||
del response.return_value
|
|
||||||
return request, response
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
def merge_environment_settings(self, *args, **kwargs):
|
|
||||||
settings = super().merge_environment_settings(*args, **kwargs)
|
|
||||||
settings.update({"gather_return": self.gather_request})
|
|
||||||
return settings
|
|
||||||
|
|
||||||
async def websocket(self, uri, subprotocols=None, *args, **kwargs):
|
async def websocket(self, uri, subprotocols=None, *args, **kwargs):
|
||||||
if uri.startswith(("ws:", "wss:")):
|
scheme = "ws"
|
||||||
url = uri
|
path = uri
|
||||||
else:
|
root_path = "{}://{}".format(scheme, ASGI_HOST)
|
||||||
uri = uri if uri.startswith("/") else "/{uri}".format(uri=uri)
|
|
||||||
url = "ws://testserver{uri}".format(uri=uri)
|
|
||||||
|
|
||||||
headers = kwargs.get("headers", {})
|
headers = kwargs.get("headers", {})
|
||||||
headers.setdefault("connection", "upgrade")
|
headers.setdefault("connection", "upgrade")
|
||||||
|
@ -418,6 +234,24 @@ class SanicASGITestClient(requests.ASGISession):
|
||||||
headers.setdefault(
|
headers.setdefault(
|
||||||
"sec-websocket-protocol", ", ".join(subprotocols)
|
"sec-websocket-protocol", ", ".join(subprotocols)
|
||||||
)
|
)
|
||||||
kwargs["headers"] = headers
|
|
||||||
|
|
||||||
return await self.request("websocket", url, **kwargs)
|
scope = {
|
||||||
|
"type": "websocket",
|
||||||
|
"asgi": {"version": "3.0"},
|
||||||
|
"http_version": "1.1",
|
||||||
|
"headers": [map(lambda y: y.encode(), x) for x in headers.items()],
|
||||||
|
"scheme": scheme,
|
||||||
|
"root_path": root_path,
|
||||||
|
"path": path,
|
||||||
|
"query_string": b"",
|
||||||
|
}
|
||||||
|
|
||||||
|
async def receive():
|
||||||
|
return {}
|
||||||
|
|
||||||
|
async def send(message):
|
||||||
|
pass
|
||||||
|
|
||||||
|
await self.app(scope, receive, send)
|
||||||
|
|
||||||
|
return None, {}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[tool.towncrier]
|
[tool.towncrier]
|
||||||
package = "sanic"
|
package = "sanic"
|
||||||
package_dir = "."
|
package_dir = ".."
|
||||||
filename = "../CHANGELOG.rst"
|
filename = "../CHANGELOG.rst"
|
||||||
directory = "./changelogs"
|
directory = "./changelogs"
|
||||||
underlines = ["=", "*", "~"]
|
underlines = ["=", "*", "~"]
|
||||||
|
|
|
@ -14,7 +14,7 @@ multi_line_output = 3
|
||||||
not_skip = __init__.py
|
not_skip = __init__.py
|
||||||
|
|
||||||
[version]
|
[version]
|
||||||
current_version = 19.9.0
|
current_version = 19.12.0
|
||||||
files = sanic/__version__.py
|
files = sanic/__version__.py
|
||||||
current_version_pattern = __version__ = "{current_version}"
|
current_version_pattern = __version__ = "{current_version}"
|
||||||
new_version_pattern = __version__ = "{new_version}"
|
new_version_pattern = __version__ = "{new_version}"
|
||||||
|
|
3
setup.py
3
setup.py
|
@ -5,6 +5,7 @@ import codecs
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from distutils.util import strtobool
|
from distutils.util import strtobool
|
||||||
|
|
||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
|
@ -83,7 +84,7 @@ requirements = [
|
||||||
"aiofiles>=0.3.0",
|
"aiofiles>=0.3.0",
|
||||||
"websockets>=7.0,<9.0",
|
"websockets>=7.0,<9.0",
|
||||||
"multidict>=4.0,<5.0",
|
"multidict>=4.0,<5.0",
|
||||||
"requests-async==0.5.0",
|
"httpx==0.9.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
tests_require = [
|
tests_require = [
|
||||||
|
|
|
@ -94,7 +94,7 @@ def test_app_route_raise_value_error(app):
|
||||||
|
|
||||||
def test_app_handle_request_handler_is_none(app, monkeypatch):
|
def test_app_handle_request_handler_is_none(app, monkeypatch):
|
||||||
def mockreturn(*args, **kwargs):
|
def mockreturn(*args, **kwargs):
|
||||||
return None, [], {}, ""
|
return None, [], {}, "", ""
|
||||||
|
|
||||||
# Not sure how to make app.router.get() return None, so use mock here.
|
# Not sure how to make app.router.get() return None, so use mock here.
|
||||||
monkeypatch.setattr(app.router, "get", mockreturn)
|
monkeypatch.setattr(app.router, "get", mockreturn)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from collections import deque
|
from collections import deque, namedtuple
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
@ -245,17 +245,26 @@ async def test_cookie_customization(app):
|
||||||
return response
|
return response
|
||||||
|
|
||||||
_, response = await app.asgi_client.get("/cookie")
|
_, response = await app.asgi_client.get("/cookie")
|
||||||
|
|
||||||
|
CookieDef = namedtuple("CookieDef", ("value", "httponly"))
|
||||||
|
Cookie = namedtuple("Cookie", ("domain", "path", "value", "httponly"))
|
||||||
cookie_map = {
|
cookie_map = {
|
||||||
"test": {"value": "Cookie1", "HttpOnly": True},
|
"test": CookieDef("Cookie1", True),
|
||||||
"c2": {"value": "Cookie2", "HttpOnly": False},
|
"c2": CookieDef("Cookie2", False),
|
||||||
}
|
}
|
||||||
|
|
||||||
for k, v in (
|
cookies = {
|
||||||
response.cookies._cookies.get("mockserver.local").get("/").items()
|
c.name: Cookie(c.domain, c.path, c.value, "HttpOnly" in c._rest.keys())
|
||||||
):
|
for c in response.cookies.jar
|
||||||
assert cookie_map.get(k).get("value") == v.value
|
}
|
||||||
if cookie_map.get(k).get("HttpOnly"):
|
|
||||||
assert "HttpOnly" in v._rest.keys()
|
for name, definition in cookie_map.items():
|
||||||
|
cookie = cookies.get(name)
|
||||||
|
assert cookie
|
||||||
|
assert cookie.value == definition.value
|
||||||
|
assert cookie.domain == "mockserver.local"
|
||||||
|
assert cookie.path == "/"
|
||||||
|
assert cookie.httponly == definition.httponly
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|
|
@ -83,7 +83,7 @@ def test_bp_group_with_additional_route_params(app: Sanic):
|
||||||
_, response = app.test_client.patch("/api/bp2/route/bp2", headers=header)
|
_, response = app.test_client.patch("/api/bp2/route/bp2", headers=header)
|
||||||
assert response.text == "PATCH_bp2"
|
assert response.text == "PATCH_bp2"
|
||||||
|
|
||||||
_, response = app.test_client.get("/v2/api/bp1/request_path")
|
_, response = app.test_client.put("/v2/api/bp1/request_path")
|
||||||
assert response.status == 401
|
assert response.status == 401
|
||||||
|
|
||||||
|
|
||||||
|
@ -141,8 +141,8 @@ def test_bp_group(app: Sanic):
|
||||||
_, response = app.test_client.get("/api/bp3")
|
_, response = app.test_client.get("/api/bp3")
|
||||||
assert response.text == "BP3_OK"
|
assert response.text == "BP3_OK"
|
||||||
|
|
||||||
assert MIDDLEWARE_INVOKE_COUNTER["response"] == 4
|
assert MIDDLEWARE_INVOKE_COUNTER["response"] == 3
|
||||||
assert MIDDLEWARE_INVOKE_COUNTER["request"] == 4
|
assert MIDDLEWARE_INVOKE_COUNTER["request"] == 2
|
||||||
|
|
||||||
|
|
||||||
def test_bp_group_list_operations(app: Sanic):
|
def test_bp_group_list_operations(app: Sanic):
|
||||||
|
|
|
@ -268,7 +268,7 @@ def test_bp_middleware(app):
|
||||||
request, response = app.test_client.get("/")
|
request, response = app.test_client.get("/")
|
||||||
|
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert response.text == "OK"
|
assert response.text == "FAIL"
|
||||||
|
|
||||||
|
|
||||||
def test_bp_exception_handler(app):
|
def test_bp_exception_handler(app):
|
||||||
|
|
|
@ -1,15 +1,9 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import functools
|
|
||||||
import socket
|
|
||||||
|
|
||||||
from asyncio import sleep as aio_sleep
|
from asyncio import sleep as aio_sleep
|
||||||
from http.client import _encode
|
|
||||||
from json import JSONDecodeError
|
from json import JSONDecodeError
|
||||||
|
|
||||||
import httpcore
|
import httpx
|
||||||
import requests_async as requests
|
|
||||||
|
|
||||||
from httpcore import PoolTimeout
|
|
||||||
|
|
||||||
from sanic import Sanic, server
|
from sanic import Sanic, server
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
|
@ -21,24 +15,28 @@ CONFIG_FOR_TESTS = {"KEEP_ALIVE_TIMEOUT": 2, "KEEP_ALIVE": True}
|
||||||
old_conn = None
|
old_conn = None
|
||||||
|
|
||||||
|
|
||||||
class ReusableSanicConnectionPool(httpcore.ConnectionPool):
|
class ReusableSanicConnectionPool(
|
||||||
async def acquire_connection(self, origin):
|
httpx.dispatch.connection_pool.ConnectionPool
|
||||||
|
):
|
||||||
|
async def acquire_connection(self, origin, timeout):
|
||||||
global old_conn
|
global old_conn
|
||||||
connection = self.active_connections.pop_by_origin(
|
connection = self.pop_connection(origin)
|
||||||
origin, http2_only=True
|
|
||||||
)
|
|
||||||
if connection is None:
|
|
||||||
connection = self.keepalive_connections.pop_by_origin(origin)
|
|
||||||
|
|
||||||
if connection is None:
|
if connection is None:
|
||||||
await self.max_connections.acquire()
|
pool_timeout = None if timeout is None else timeout.pool_timeout
|
||||||
connection = httpcore.HTTPConnection(
|
|
||||||
|
await self.max_connections.acquire(timeout=pool_timeout)
|
||||||
|
connection = httpx.dispatch.connection.HTTPConnection(
|
||||||
origin,
|
origin,
|
||||||
ssl=self.ssl,
|
verify=self.verify,
|
||||||
timeout=self.timeout,
|
cert=self.cert,
|
||||||
|
http2=self.http2,
|
||||||
backend=self.backend,
|
backend=self.backend,
|
||||||
release_func=self.release_connection,
|
release_func=self.release_connection,
|
||||||
|
trust_env=self.trust_env,
|
||||||
|
uds=self.uds,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.active_connections.add(connection)
|
self.active_connections.add(connection)
|
||||||
|
|
||||||
if old_conn is not None:
|
if old_conn is not None:
|
||||||
|
@ -51,17 +49,10 @@ class ReusableSanicConnectionPool(httpcore.ConnectionPool):
|
||||||
return connection
|
return connection
|
||||||
|
|
||||||
|
|
||||||
class ReusableSanicAdapter(requests.adapters.HTTPAdapter):
|
class ResusableSanicSession(httpx.Client):
|
||||||
def __init__(self):
|
|
||||||
self.pool = ReusableSanicConnectionPool()
|
|
||||||
|
|
||||||
|
|
||||||
class ResusableSanicSession(requests.Session):
|
|
||||||
def __init__(self, *args, **kwargs) -> None:
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
super().__init__(*args, **kwargs)
|
dispatch = ReusableSanicConnectionPool()
|
||||||
adapter = ReusableSanicAdapter()
|
super().__init__(dispatch=dispatch, *args, **kwargs)
|
||||||
self.mount("http://", adapter)
|
|
||||||
self.mount("https://", adapter)
|
|
||||||
|
|
||||||
|
|
||||||
class ReuseableSanicTestClient(SanicTestClient):
|
class ReuseableSanicTestClient(SanicTestClient):
|
||||||
|
@ -74,6 +65,9 @@ class ReuseableSanicTestClient(SanicTestClient):
|
||||||
self._tcp_connector = None
|
self._tcp_connector = None
|
||||||
self._session = None
|
self._session = None
|
||||||
|
|
||||||
|
def get_new_session(self):
|
||||||
|
return ResusableSanicSession()
|
||||||
|
|
||||||
# Copied from SanicTestClient, but with some changes to reuse the
|
# Copied from SanicTestClient, but with some changes to reuse the
|
||||||
# same loop for the same app.
|
# same loop for the same app.
|
||||||
def _sanic_endpoint_test(
|
def _sanic_endpoint_test(
|
||||||
|
@ -167,7 +161,6 @@ class ReuseableSanicTestClient(SanicTestClient):
|
||||||
self._server.close()
|
self._server.close()
|
||||||
self._loop.run_until_complete(self._server.wait_closed())
|
self._loop.run_until_complete(self._server.wait_closed())
|
||||||
self._server = None
|
self._server = None
|
||||||
self.app.stop()
|
|
||||||
|
|
||||||
if self._session:
|
if self._session:
|
||||||
self._loop.run_until_complete(self._session.close())
|
self._loop.run_until_complete(self._session.close())
|
||||||
|
@ -186,7 +179,7 @@ class ReuseableSanicTestClient(SanicTestClient):
|
||||||
"request_keepalive", CONFIG_FOR_TESTS["KEEP_ALIVE_TIMEOUT"]
|
"request_keepalive", CONFIG_FOR_TESTS["KEEP_ALIVE_TIMEOUT"]
|
||||||
)
|
)
|
||||||
if not self._session:
|
if not self._session:
|
||||||
self._session = ResusableSanicSession()
|
self._session = self.get_new_session()
|
||||||
try:
|
try:
|
||||||
response = await getattr(self._session, method.lower())(
|
response = await getattr(self._session, method.lower())(
|
||||||
url, verify=False, timeout=request_keepalive, *args, **kwargs
|
url, verify=False, timeout=request_keepalive, *args, **kwargs
|
||||||
|
|
|
@ -2,6 +2,7 @@ import io
|
||||||
|
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
|
|
||||||
|
|
||||||
data = "abc" * 10_000_000
|
data = "abc" * 10_000_000
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -332,7 +332,7 @@ def test_request_stream_handle_exception(app):
|
||||||
assert response.text == "Error: Requested URL /in_valid_post not found"
|
assert response.text == "Error: Requested URL /in_valid_post not found"
|
||||||
|
|
||||||
# 405
|
# 405
|
||||||
request, response = app.test_client.get("/post/random_id", data=data)
|
request, response = app.test_client.get("/post/random_id")
|
||||||
assert response.status == 405
|
assert response.status == 405
|
||||||
assert (
|
assert (
|
||||||
response.text == "Error: Method GET not allowed for URL"
|
response.text == "Error: Method GET not allowed for URL"
|
||||||
|
|
|
@ -1,49 +1,70 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
import httpcore
|
import httpx
|
||||||
import requests_async as requests
|
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
from sanic.testing import SanicTestClient
|
from sanic.testing import SanicTestClient
|
||||||
|
|
||||||
|
|
||||||
class DelayableSanicConnectionPool(httpcore.ConnectionPool):
|
class DelayableHTTPConnection(httpx.dispatch.connection.HTTPConnection):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self._request_delay = None
|
||||||
|
if "request_delay" in kwargs:
|
||||||
|
self._request_delay = kwargs.pop("request_delay")
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
async def send(self, request, verify=None, cert=None, timeout=None):
|
||||||
|
if self.h11_connection is None and self.h2_connection is None:
|
||||||
|
await self.connect(verify=verify, cert=cert, timeout=timeout)
|
||||||
|
|
||||||
|
if self._request_delay:
|
||||||
|
await asyncio.sleep(self._request_delay)
|
||||||
|
|
||||||
|
if self.h2_connection is not None:
|
||||||
|
response = await self.h2_connection.send(request, timeout=timeout)
|
||||||
|
else:
|
||||||
|
assert self.h11_connection is not None
|
||||||
|
response = await self.h11_connection.send(request, timeout=timeout)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class DelayableSanicConnectionPool(
|
||||||
|
httpx.dispatch.connection_pool.ConnectionPool
|
||||||
|
):
|
||||||
def __init__(self, request_delay=None, *args, **kwargs):
|
def __init__(self, request_delay=None, *args, **kwargs):
|
||||||
self._request_delay = request_delay
|
self._request_delay = request_delay
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
async def send(self, request, stream=False, ssl=None, timeout=None):
|
async def acquire_connection(self, origin, timeout=None):
|
||||||
connection = await self.acquire_connection(request.url.origin)
|
connection = self.pop_connection(origin)
|
||||||
if (
|
|
||||||
connection.h11_connection is None
|
if connection is None:
|
||||||
and connection.h2_connection is None
|
pool_timeout = None if timeout is None else timeout.pool_timeout
|
||||||
):
|
|
||||||
await connection.connect(ssl=ssl, timeout=timeout)
|
await self.max_connections.acquire(timeout=pool_timeout)
|
||||||
if self._request_delay:
|
connection = DelayableHTTPConnection(
|
||||||
await asyncio.sleep(self._request_delay)
|
origin,
|
||||||
try:
|
verify=self.verify,
|
||||||
response = await connection.send(
|
cert=self.cert,
|
||||||
request, stream=stream, ssl=ssl, timeout=timeout
|
http2=self.http2,
|
||||||
|
backend=self.backend,
|
||||||
|
release_func=self.release_connection,
|
||||||
|
trust_env=self.trust_env,
|
||||||
|
uds=self.uds,
|
||||||
|
request_delay=self._request_delay,
|
||||||
)
|
)
|
||||||
except BaseException as exc:
|
|
||||||
self.active_connections.remove(connection)
|
self.active_connections.add(connection)
|
||||||
self.max_connections.release()
|
|
||||||
raise exc
|
return connection
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
class DelayableSanicAdapter(requests.adapters.HTTPAdapter):
|
class DelayableSanicSession(httpx.Client):
|
||||||
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:
|
def __init__(self, request_delay=None, *args, **kwargs) -> None:
|
||||||
super().__init__(*args, **kwargs)
|
dispatch = DelayableSanicConnectionPool(request_delay=request_delay)
|
||||||
adapter = DelayableSanicAdapter(request_delay=request_delay)
|
super().__init__(dispatch=dispatch, *args, **kwargs)
|
||||||
self.mount("http://", adapter)
|
|
||||||
self.mount("https://", adapter)
|
|
||||||
|
|
||||||
|
|
||||||
class DelayableSanicTestClient(SanicTestClient):
|
class DelayableSanicTestClient(SanicTestClient):
|
||||||
|
|
|
@ -55,11 +55,11 @@ def test_ip(app):
|
||||||
async def test_ip_asgi(app):
|
async def test_ip_asgi(app):
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def handler(request):
|
def handler(request):
|
||||||
return text("{}".format(request.ip))
|
return text("{}".format(request.url))
|
||||||
|
|
||||||
request, response = await app.asgi_client.get("/")
|
request, response = await app.asgi_client.get("/")
|
||||||
|
|
||||||
assert response.text == "mockserver"
|
assert response.text == "http://mockserver/"
|
||||||
|
|
||||||
|
|
||||||
def test_text(app):
|
def test_text(app):
|
||||||
|
@ -242,24 +242,24 @@ async def test_empty_json_asgi(app):
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_json(app):
|
def test_invalid_json(app):
|
||||||
@app.route("/")
|
@app.post("/")
|
||||||
async def handler(request):
|
async def handler(request):
|
||||||
return json(request.json)
|
return json(request.json)
|
||||||
|
|
||||||
data = "I am not json"
|
data = "I am not json"
|
||||||
request, response = app.test_client.get("/", data=data)
|
request, response = app.test_client.post("/", data=data)
|
||||||
|
|
||||||
assert response.status == 400
|
assert response.status == 400
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_invalid_json_asgi(app):
|
async def test_invalid_json_asgi(app):
|
||||||
@app.route("/")
|
@app.post("/")
|
||||||
async def handler(request):
|
async def handler(request):
|
||||||
return json(request.json)
|
return json(request.json)
|
||||||
|
|
||||||
data = "I am not json"
|
data = "I am not json"
|
||||||
request, response = await app.asgi_client.get("/", data=data)
|
request, response = await app.asgi_client.post("/", data=data)
|
||||||
|
|
||||||
assert response.status == 400
|
assert response.status == 400
|
||||||
|
|
||||||
|
@ -1842,26 +1842,6 @@ def test_request_port(app):
|
||||||
assert hasattr(request, "_port")
|
assert hasattr(request, "_port")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_request_port_asgi(app):
|
|
||||||
@app.get("/")
|
|
||||||
def handler(request):
|
|
||||||
return text("OK")
|
|
||||||
|
|
||||||
request, response = await app.asgi_client.get("/")
|
|
||||||
|
|
||||||
port = request.port
|
|
||||||
assert isinstance(port, int)
|
|
||||||
|
|
||||||
delattr(request, "_socket")
|
|
||||||
delattr(request, "_port")
|
|
||||||
|
|
||||||
port = request.port
|
|
||||||
assert isinstance(port, int)
|
|
||||||
assert hasattr(request, "_socket")
|
|
||||||
assert hasattr(request, "_port")
|
|
||||||
|
|
||||||
|
|
||||||
def test_request_socket(app):
|
def test_request_socket(app):
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
def handler(request):
|
def handler(request):
|
||||||
|
|
|
@ -22,6 +22,7 @@ from sanic.response import (
|
||||||
stream,
|
stream,
|
||||||
text,
|
text,
|
||||||
)
|
)
|
||||||
|
from sanic.response import empty
|
||||||
from sanic.server import HttpProtocol
|
from sanic.server import HttpProtocol
|
||||||
from sanic.testing import HOST, PORT
|
from sanic.testing import HOST, PORT
|
||||||
|
|
||||||
|
@ -592,3 +593,13 @@ def test_raw_response(app):
|
||||||
request, response = app.test_client.get("/test")
|
request, response = app.test_client.get("/test")
|
||||||
assert response.content_type == "application/octet-stream"
|
assert response.content_type == "application/octet-stream"
|
||||||
assert response.body == b"raw_response"
|
assert response.body == b"raw_response"
|
||||||
|
|
||||||
|
|
||||||
|
def test_empty_response(app):
|
||||||
|
@app.get("/test")
|
||||||
|
def handler(request):
|
||||||
|
return empty()
|
||||||
|
|
||||||
|
request, response = app.test_client.get("/test")
|
||||||
|
assert response.content_type is None
|
||||||
|
assert response.body == b""
|
||||||
|
|
|
@ -551,6 +551,35 @@ def test_route_duplicate(app):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_double_stack_route(app):
|
||||||
|
@app.route("/test/1")
|
||||||
|
@app.route("/test/2")
|
||||||
|
async def handler1(request):
|
||||||
|
return text("OK")
|
||||||
|
|
||||||
|
request, response = app.test_client.get("/test/1")
|
||||||
|
assert response.status == 200
|
||||||
|
request, response = app.test_client.get("/test/2")
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_websocket_route_asgi(app):
|
||||||
|
ev = asyncio.Event()
|
||||||
|
|
||||||
|
@app.websocket("/test/1")
|
||||||
|
@app.websocket("/test/2")
|
||||||
|
async def handler(request, ws):
|
||||||
|
ev.set()
|
||||||
|
|
||||||
|
request, response = await app.asgi_client.websocket("/test/1")
|
||||||
|
first_set = ev.is_set()
|
||||||
|
ev.clear()
|
||||||
|
request, response = await app.asgi_client.websocket("/test/1")
|
||||||
|
second_set = ev.is_set()
|
||||||
|
assert(first_set and second_set)
|
||||||
|
|
||||||
|
|
||||||
def test_method_not_allowed(app):
|
def test_method_not_allowed(app):
|
||||||
@app.route("/test", methods=["GET"])
|
@app.route("/test", methods=["GET"])
|
||||||
async def handler(request):
|
async def handler(request):
|
||||||
|
|
|
@ -37,14 +37,14 @@ def skip_test_utf8_route(app):
|
||||||
|
|
||||||
|
|
||||||
def test_utf8_post_json(app):
|
def test_utf8_post_json(app):
|
||||||
@app.route("/")
|
@app.post("/")
|
||||||
async def handler(request):
|
async def handler(request):
|
||||||
return text("OK")
|
return text("OK")
|
||||||
|
|
||||||
payload = {"test": "✓"}
|
payload = {"test": "✓"}
|
||||||
headers = {"content-type": "application/json"}
|
headers = {"content-type": "application/json"}
|
||||||
|
|
||||||
request, response = app.test_client.get(
|
request, response = app.test_client.post(
|
||||||
"/", data=json_dumps(payload), headers=headers
|
"/", data=json_dumps(payload), headers=headers
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
8
tox.ini
8
tox.ini
|
@ -1,11 +1,11 @@
|
||||||
[tox]
|
[tox]
|
||||||
envlist = py36, py37, {py36,py37}-no-ext, lint, check, security, docs
|
envlist = py36, py37, pyNightly, {py36,py37,pyNightly}-no-ext, lint, check, security, docs
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
usedevelop = True
|
usedevelop = True
|
||||||
setenv =
|
setenv =
|
||||||
{py36,py37}-no-ext: SANIC_NO_UJSON=1
|
{py36,py37,pyNightly}-no-ext: SANIC_NO_UJSON=1
|
||||||
{py36,py37}-no-ext: SANIC_NO_UVLOOP=1
|
{py36,py37,pyNightly}-no-ext: SANIC_NO_UVLOOP=1
|
||||||
deps =
|
deps =
|
||||||
coverage
|
coverage
|
||||||
pytest==5.2.1
|
pytest==5.2.1
|
||||||
|
@ -13,7 +13,7 @@ deps =
|
||||||
pytest-sanic
|
pytest-sanic
|
||||||
pytest-sugar
|
pytest-sugar
|
||||||
httpcore==0.3.0
|
httpcore==0.3.0
|
||||||
requests-async==0.5.0
|
httpx==0.9.3
|
||||||
chardet<=2.3.0
|
chardet<=2.3.0
|
||||||
beautifulsoup4
|
beautifulsoup4
|
||||||
gunicorn
|
gunicorn
|
||||||
|
|
Loading…
Reference in New Issue
Block a user