Compare commits
15 Commits
flaky-test
...
feat-2394-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92bfeaefd5 | ||
|
|
5a650f5758 | ||
|
|
850b1f432c | ||
|
|
df8480156d | ||
|
|
d0cfb69f43 | ||
|
|
19eee8bfb2 | ||
|
|
625cffb1b9 | ||
|
|
c5ac28cbcd | ||
|
|
e57bea28f7 | ||
|
|
28665e31ce | ||
|
|
9f41936861 | ||
|
|
7835492b09 | ||
|
|
ca3bea3425 | ||
|
|
88ca56dda3 | ||
|
|
d2ab46d70b |
1
.github/workflows/pr-python310.yml
vendored
1
.github/workflows/pr-python310.yml
vendored
@@ -8,6 +8,7 @@ on:
|
||||
|
||||
jobs:
|
||||
testPy310:
|
||||
if: github.event.pull_request.draft == false
|
||||
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
|
||||
@@ -140,7 +140,6 @@ To maintain the code consistency, Sanic uses following tools.
|
||||
#. `isort <https://github.com/timothycrosley/isort>`_
|
||||
#. `black <https://github.com/python/black>`_
|
||||
#. `flake8 <https://github.com/PyCQA/flake8>`_
|
||||
#. `slotscheck <https://github.com/ariebovenberg/slotscheck>`_
|
||||
|
||||
isort
|
||||
*****
|
||||
@@ -168,13 +167,7 @@ flake8
|
||||
#. pycodestyle
|
||||
#. Ned Batchelder's McCabe script
|
||||
|
||||
slotscheck
|
||||
**********
|
||||
|
||||
``slotscheck`` ensures that there are no problems with ``__slots__``
|
||||
(e.g. overlaps, or missing slots in base classes).
|
||||
|
||||
``isort``\ , ``black``\ , ``flake8`` and ``slotscheck`` checks are performed during ``tox`` lint checks.
|
||||
``isort``\ , ``black`` and ``flake8`` checks are performed during ``tox`` lint checks.
|
||||
|
||||
The **easiest** way to make your code conform is to run the following before committing.
|
||||
|
||||
|
||||
@@ -6,5 +6,5 @@ data = ""
|
||||
for i in range(1, 250000):
|
||||
data += str(i)
|
||||
|
||||
r = requests.post('http://0.0.0.0:8000/stream', data=data)
|
||||
r = requests.post("http://0.0.0.0:8000/stream", data=data)
|
||||
print(r.text)
|
||||
|
||||
13
sanic/app.py
13
sanic/app.py
@@ -44,8 +44,11 @@ from typing import (
|
||||
from urllib.parse import urlencode, urlunparse
|
||||
from warnings import filterwarnings
|
||||
|
||||
from sanic_routing.exceptions import FinalizationError, NotFound
|
||||
from sanic_routing.route import Route
|
||||
from sanic_routing.exceptions import ( # type: ignore
|
||||
FinalizationError,
|
||||
NotFound,
|
||||
)
|
||||
from sanic_routing.route import Route # type: ignore
|
||||
|
||||
from sanic.application.ext import setup_ext
|
||||
from sanic.application.state import ApplicationState, Mode, ServerStage
|
||||
@@ -139,6 +142,7 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
||||
"error_handler",
|
||||
"go_fast",
|
||||
"listeners",
|
||||
"name",
|
||||
"named_request_middleware",
|
||||
"named_response_middleware",
|
||||
"request_class",
|
||||
@@ -1264,9 +1268,10 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
||||
...
|
||||
|
||||
def purge_tasks(self):
|
||||
for key, task in self._task_registry.items():
|
||||
for task in self.tasks:
|
||||
if task.done() or task.cancelled():
|
||||
self._task_registry[key] = None
|
||||
name = task.get_name()
|
||||
self._task_registry[name] = None
|
||||
|
||||
self._task_registry = {
|
||||
k: v for k, v in self._task_registry.items() if v is not None
|
||||
|
||||
@@ -16,9 +16,9 @@ class MiddlewareMixin(metaclass=SanicMeta):
|
||||
self, middleware_or_request, attach_to="request", apply=True
|
||||
):
|
||||
"""
|
||||
Decorate and register middleware to be called before a request
|
||||
is handled or after a response is created. Can either be called as
|
||||
*@app.middleware* or *@app.middleware('request')*.
|
||||
Decorate and register middleware to be called before a request.
|
||||
Can either be called as *@app.middleware* or
|
||||
*@app.middleware('request')*
|
||||
|
||||
`See user guide re: middleware
|
||||
<https://sanicframework.org/guide/basics/middleware.html>`__
|
||||
@@ -47,25 +47,12 @@ class MiddlewareMixin(metaclass=SanicMeta):
|
||||
)
|
||||
|
||||
def on_request(self, middleware=None):
|
||||
"""Register a middleware to be called before a request is handled.
|
||||
|
||||
This is the same as *@app.middleware('request')*.
|
||||
|
||||
:param: middleware: A callable that takes in request.
|
||||
"""
|
||||
if callable(middleware):
|
||||
return self.middleware(middleware, "request")
|
||||
else:
|
||||
return partial(self.middleware, attach_to="request")
|
||||
|
||||
def on_response(self, middleware=None):
|
||||
"""Register a middleware to be called after a response is created.
|
||||
|
||||
This is the same as *@app.middleware('response')*.
|
||||
|
||||
:param: middleware:
|
||||
A callable that takes in a request and its response.
|
||||
"""
|
||||
if callable(middleware):
|
||||
return self.middleware(middleware, "response")
|
||||
else:
|
||||
|
||||
@@ -30,6 +30,7 @@ from types import SimpleNamespace
|
||||
from urllib.parse import parse_qs, parse_qsl, unquote, urlunparse
|
||||
|
||||
from httptools import parse_url # type: ignore
|
||||
from httptools.parser.errors import HttpParserInvalidURLError # type: ignore
|
||||
|
||||
from sanic.compat import CancelledErrors, Header
|
||||
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE
|
||||
@@ -130,7 +131,12 @@ class Request:
|
||||
|
||||
self.raw_url = url_bytes
|
||||
# TODO: Content-Encoding detection
|
||||
self._parsed_url = parse_url(url_bytes)
|
||||
try:
|
||||
self._parsed_url = parse_url(url_bytes)
|
||||
except HttpParserInvalidURLError as InvalidURLError:
|
||||
raise InvalidUsage(
|
||||
"URL is invalid or malformed"
|
||||
) from InvalidURLError
|
||||
self._id: Optional[Union[uuid.UUID, str, int]] = None
|
||||
self._name: Optional[str] = None
|
||||
self.app = app
|
||||
|
||||
@@ -50,16 +50,6 @@ class BaseHTTPResponse:
|
||||
The base class for all HTTP Responses
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"asgi",
|
||||
"body",
|
||||
"content_type",
|
||||
"stream",
|
||||
"status",
|
||||
"headers",
|
||||
"_cookies",
|
||||
)
|
||||
|
||||
_dumps = json_dumps
|
||||
|
||||
def __init__(self):
|
||||
@@ -166,7 +156,7 @@ class HTTPResponse(BaseHTTPResponse):
|
||||
:type content_type: Optional[str]
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
__slots__ = ("body", "status", "content_type", "headers", "_cookies")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@@ -1,12 +1,26 @@
|
||||
import asyncio
|
||||
|
||||
from distutils.util import strtobool
|
||||
from os import getenv
|
||||
|
||||
from sanic.compat import OS_IS_WINDOWS
|
||||
from sanic.log import error_logger
|
||||
|
||||
|
||||
def strtobool(query: str) -> bool:
|
||||
"""
|
||||
reimplement strtobool per PEP 632 and python 3.12 deprecation
|
||||
|
||||
True values are y, yes, t, true, on and 1; false values are n, no, f,
|
||||
false, off and 0. Raises ValueError if val is anything else.
|
||||
"""
|
||||
if query.lower() in ["y", "yes", "t", "true", "on", "1"]:
|
||||
return True
|
||||
elif query.lower() in ["n", "no", "f", "false", "off", "0"]:
|
||||
return False
|
||||
else:
|
||||
raise ValueError(f"String value {query} cannot be converted to bool")
|
||||
|
||||
|
||||
def try_use_uvloop() -> None:
|
||||
"""
|
||||
Use uvloop instead of the default asyncio loop.
|
||||
|
||||
@@ -11,35 +11,18 @@ from sanic.helpers import import_string
|
||||
|
||||
|
||||
def str_to_bool(val: str) -> bool:
|
||||
"""Takes string and tries to turn it into bool as human would do.
|
||||
"""
|
||||
reimplement strtobool per PEP 632 and python 3.12 deprecation
|
||||
|
||||
If val is in case insensitive (
|
||||
"y", "yes", "yep", "yup", "t",
|
||||
"true", "on", "enable", "enabled", "1"
|
||||
) returns True.
|
||||
If val is in case insensitive (
|
||||
"n", "no", "f", "false", "off", "disable", "disabled", "0"
|
||||
) returns False.
|
||||
Else Raise ValueError."""
|
||||
|
||||
val = val.lower()
|
||||
if val in {
|
||||
"y",
|
||||
"yes",
|
||||
"yep",
|
||||
"yup",
|
||||
"t",
|
||||
"true",
|
||||
"on",
|
||||
"enable",
|
||||
"enabled",
|
||||
"1",
|
||||
}:
|
||||
True values are y, yes, t, true, on and 1; false values are n, no, f,
|
||||
false, off and 0. Raises ValueError if val is anything else.
|
||||
"""
|
||||
if val.lower() in ["y", "yes", "t", "true", "on", "1"]:
|
||||
return True
|
||||
elif val in {"n", "no", "f", "false", "off", "disable", "disabled", "0"}:
|
||||
elif val.lower() in ["n", "no", "f", "false", "off", "0"]:
|
||||
return False
|
||||
else:
|
||||
raise ValueError(f"Invalid truth value {val}")
|
||||
raise ValueError(f"String value {val} cannot be converted to bool")
|
||||
|
||||
|
||||
def load_module_from_file_location(
|
||||
|
||||
22
setup.py
22
setup.py
@@ -6,8 +6,6 @@ import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
from distutils.util import strtobool
|
||||
|
||||
from setuptools import find_packages, setup
|
||||
from setuptools.command.test import test as TestCommand
|
||||
|
||||
@@ -61,7 +59,7 @@ setup_kwargs = {
|
||||
"Build fast. Run fast."
|
||||
),
|
||||
"long_description": long_description,
|
||||
"packages": find_packages(),
|
||||
"packages": find_packages(include=[]),
|
||||
"package_data": {"sanic": ["py.typed"]},
|
||||
"platforms": "any",
|
||||
"python_requires": ">=3.7",
|
||||
@@ -112,7 +110,6 @@ tests_require = [
|
||||
"docutils",
|
||||
"pygments",
|
||||
"uvicorn<0.15.0",
|
||||
"slotscheck>=0.8.0,<1",
|
||||
types_ujson,
|
||||
]
|
||||
|
||||
@@ -133,6 +130,23 @@ dev_require = tests_require + [
|
||||
|
||||
all_require = list(set(dev_require + docs_require))
|
||||
|
||||
# trying to self-refernce this from within sanic prior to install is
|
||||
# problematic
|
||||
def strtobool(val: str) -> bool:
|
||||
"""
|
||||
reimplement strtobool per PEP 632 and python 3.12 deprecation
|
||||
|
||||
True values are y, yes, t, true, on and 1; false values are n, no, f,
|
||||
false, off and 0. Raises ValueError if val is anything else.
|
||||
"""
|
||||
if val.lower() in ["y", "yes", "t", "true", "on", "1"]:
|
||||
return True
|
||||
elif val.lower() in ["n", "no", "f", "false", "off", "0"]:
|
||||
return False
|
||||
else:
|
||||
raise ValueError(f"String value {val} cannot be converted to bool")
|
||||
|
||||
|
||||
if strtobool(os.environ.get("SANIC_NO_UJSON", "no")):
|
||||
print("Installing without uJSON")
|
||||
requirements.remove(ujson)
|
||||
|
||||
@@ -21,3 +21,25 @@ def test_bad_request_response(app):
|
||||
app.run(host="127.0.0.1", port=42101, debug=False)
|
||||
assert lines[0] == b"HTTP/1.1 400 Bad Request\r\n"
|
||||
assert b"Bad Request" in lines[-2]
|
||||
|
||||
|
||||
def test_malformed_uri_bad_request(app):
|
||||
lines = []
|
||||
|
||||
app.get("/")(lambda x: ...)
|
||||
|
||||
@app.listener("after_server_start")
|
||||
async def _request(sanic, loop):
|
||||
connect = asyncio.open_connection("127.0.0.1", 42101)
|
||||
reader, writer = await connect
|
||||
writer.write(b"GET /\r\nHost: ---.com\r\n\r\n")
|
||||
while True:
|
||||
line = await reader.readline()
|
||||
if not line:
|
||||
break
|
||||
lines.append(line)
|
||||
app.stop()
|
||||
|
||||
app.run(host="127.0.0.1", port=42101, debug=False)
|
||||
assert lines[0] == b"HTTP/1.1 400 Bad Request\r\n"
|
||||
assert b"Bad Request" in lines[-2]
|
||||
@@ -5,7 +5,6 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from pyparsing import line
|
||||
from sanic_routing import __version__ as __routing_version__
|
||||
|
||||
from sanic import __version__
|
||||
@@ -53,10 +52,8 @@ def test_server_run(appname):
|
||||
out, err, exitcode = capture(command)
|
||||
lines = out.split(b"\n")
|
||||
firstline = lines[starting_line(lines) + 1]
|
||||
error_message = f"Lines found: {lines}\nErr output: {err}"
|
||||
|
||||
assert exitcode != 1
|
||||
assert lines, error_message
|
||||
assert firstline == b"Goin' Fast @ http://127.0.0.1:8000"
|
||||
|
||||
|
||||
@@ -83,9 +80,6 @@ def test_tls_options(cmd):
|
||||
out, err, exitcode = capture(command)
|
||||
assert exitcode != 1
|
||||
lines = out.split(b"\n")
|
||||
error_message = f"Lines found: {lines}\nErr output: {err}"
|
||||
|
||||
assert lines, error_message
|
||||
firstline = lines[starting_line(lines) + 1]
|
||||
assert firstline == b"Goin' Fast @ https://127.0.0.1:9999"
|
||||
|
||||
@@ -108,9 +102,7 @@ def test_tls_wrong_options(cmd):
|
||||
assert exitcode == 1
|
||||
assert not out
|
||||
lines = err.decode().split("\n")
|
||||
error_message = f"Lines found: {lines}\nErr output: {err}"
|
||||
|
||||
assert lines, error_message
|
||||
errmsg = lines[6]
|
||||
assert errmsg == "TLS certificates must be specified by either of:"
|
||||
|
||||
@@ -127,11 +119,9 @@ def test_host_port_localhost(cmd):
|
||||
out, err, exitcode = capture(command)
|
||||
lines = out.split(b"\n")
|
||||
expected = b"Goin' Fast @ http://localhost:9999"
|
||||
error_message = f"Lines found: {lines}\nErr output: {err}"
|
||||
|
||||
assert exitcode != 1
|
||||
assert lines, error_message
|
||||
assert expected in lines, error_message
|
||||
assert expected in lines, f"Lines found: {lines}\nErr output: {err}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -146,11 +136,9 @@ def test_host_port_ipv4(cmd):
|
||||
out, err, exitcode = capture(command)
|
||||
lines = out.split(b"\n")
|
||||
expected = b"Goin' Fast @ http://127.0.0.127:9999"
|
||||
error_message = f"Lines found: {lines}\nErr output: {err}"
|
||||
|
||||
assert exitcode != 1
|
||||
assert lines, error_message
|
||||
assert expected in lines, error_message
|
||||
assert expected in lines, f"Lines found: {lines}\nErr output: {err}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -165,11 +153,9 @@ def test_host_port_ipv6_any(cmd):
|
||||
out, err, exitcode = capture(command)
|
||||
lines = out.split(b"\n")
|
||||
expected = b"Goin' Fast @ http://[::]:9999"
|
||||
error_message = f"Lines found: {lines}\nErr output: {err}"
|
||||
|
||||
assert exitcode != 1
|
||||
assert lines, error_message
|
||||
assert expected in lines, error_message
|
||||
assert expected in lines, f"Lines found: {lines}\nErr output: {err}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -184,11 +170,9 @@ def test_host_port_ipv6_loopback(cmd):
|
||||
out, err, exitcode = capture(command)
|
||||
lines = out.split(b"\n")
|
||||
expected = b"Goin' Fast @ http://[::1]:9999"
|
||||
error_message = f"Lines found: {lines}\nErr output: {err}"
|
||||
|
||||
assert exitcode != 1
|
||||
assert lines, error_message
|
||||
assert expected in lines, error_message
|
||||
assert expected in lines, f"Lines found: {lines}\nErr output: {err}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -222,12 +206,12 @@ def test_debug(cmd):
|
||||
out, err, exitcode = capture(command)
|
||||
lines = out.split(b"\n")
|
||||
info = read_app_info(lines)
|
||||
error_message = f"Lines found: {lines}\nErr output: {err}"
|
||||
|
||||
assert info, error_message
|
||||
assert info["debug"] is True, error_message
|
||||
assert info["auto_reload"] is False, error_message
|
||||
assert "dev" not in info, error_message
|
||||
assert info["debug"] is True, f"Lines found: {lines}\nErr output: {err}"
|
||||
assert (
|
||||
info["auto_reload"] is False
|
||||
), f"Lines found: {lines}\nErr output: {err}"
|
||||
assert "dev" not in info, f"Lines found: {lines}\nErr output: {err}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("cmd", ("--dev", "-d"))
|
||||
@@ -236,11 +220,11 @@ def test_dev(cmd):
|
||||
out, err, exitcode = capture(command)
|
||||
lines = out.split(b"\n")
|
||||
info = read_app_info(lines)
|
||||
error_message = f"Lines found: {lines}\nErr output: {err}"
|
||||
|
||||
assert info, error_message
|
||||
assert info["debug"] is True, error_message
|
||||
assert info["auto_reload"] is True, error_message
|
||||
assert info["debug"] is True, f"Lines found: {lines}\nErr output: {err}"
|
||||
assert (
|
||||
info["auto_reload"] is True
|
||||
), f"Lines found: {lines}\nErr output: {err}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("cmd", ("--auto-reload", "-r"))
|
||||
@@ -249,12 +233,12 @@ def test_auto_reload(cmd):
|
||||
out, err, exitcode = capture(command)
|
||||
lines = out.split(b"\n")
|
||||
info = read_app_info(lines)
|
||||
error_message = f"Lines found: {lines}\nErr output: {err}"
|
||||
|
||||
assert info, error_message
|
||||
assert info["debug"] is False, error_message
|
||||
assert info["auto_reload"] is True, error_message
|
||||
assert "dev" not in info, error_message
|
||||
assert info["debug"] is False, f"Lines found: {lines}\nErr output: {err}"
|
||||
assert (
|
||||
info["auto_reload"] is True
|
||||
), f"Lines found: {lines}\nErr output: {err}"
|
||||
assert "dev" not in info, f"Lines found: {lines}\nErr output: {err}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -265,10 +249,10 @@ def test_access_logs(cmd, expected):
|
||||
out, err, exitcode = capture(command)
|
||||
lines = out.split(b"\n")
|
||||
info = read_app_info(lines)
|
||||
error_message = f"Lines found: {lines}\nErr output: {err}"
|
||||
|
||||
assert info, error_message
|
||||
assert info["access_log"] is expected, error_message
|
||||
assert (
|
||||
info["access_log"] is expected
|
||||
), f"Lines found: {lines}\nErr output: {err}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("cmd", ("--version", "-v"))
|
||||
@@ -292,7 +276,7 @@ def test_noisy_exceptions(cmd, expected):
|
||||
out, err, exitcode = capture(command)
|
||||
lines = out.split(b"\n")
|
||||
info = read_app_info(lines)
|
||||
error_message = f"Lines found: {lines}\nErr output: {err}"
|
||||
|
||||
assert info, error_message
|
||||
assert info["noisy_exceptions"] is expected, error_message
|
||||
assert (
|
||||
info["noisy_exceptions"] is expected
|
||||
), f"Lines found: {lines}\nErr output: {err}"
|
||||
|
||||
@@ -80,18 +80,6 @@ async def test_purge_tasks(app: Sanic):
|
||||
assert len(app._task_registry) == 0
|
||||
|
||||
|
||||
async def test_purge_tasks_with_create_task(app: Sanic):
|
||||
app.add_task(asyncio.create_task(dummy(3)), name="dummy")
|
||||
|
||||
await app.cancel_task("dummy")
|
||||
|
||||
assert len(app._task_registry) == 1
|
||||
|
||||
app.purge_tasks()
|
||||
|
||||
assert len(app._task_registry) == 0
|
||||
|
||||
|
||||
def test_shutdown_tasks_on_app_stop():
|
||||
class TestSanic(Sanic):
|
||||
shutdown_tasks = Mock()
|
||||
|
||||
@@ -6,6 +6,7 @@ import pytest
|
||||
|
||||
from sanic.exceptions import LoadFileException
|
||||
from sanic.utils import load_module_from_file_location
|
||||
from sanic.utils import str_to_bool as strtobool
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -48,3 +49,20 @@ def test_load_module_from_file_location_using_env():
|
||||
module = load_module_from_file_location(location)
|
||||
|
||||
assert isinstance(module, ModuleType)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"valid,values",
|
||||
(
|
||||
(True, ["y", "yes", "t", "true", "on", "1", "Y", "yEs", "True"]),
|
||||
(False, ["n", "no", "f", "false", "off", "0", "N", "No", "False"]),
|
||||
(None, ["yyy", "foo"]),
|
||||
),
|
||||
)
|
||||
def test_strtobool(valid, values):
|
||||
for value in values:
|
||||
if valid is None:
|
||||
with pytest.raises(ValueError):
|
||||
strtobool(value)
|
||||
else:
|
||||
assert strtobool(value) is valid
|
||||
|
||||
Reference in New Issue
Block a user