Compare commits

..

4 Commits

Author SHA1 Message Date
Adam Hopkins
a61c5ff55b Get better output on test failures 2022-02-27 13:29:16 +02:00
Arie Bovenberg
7523e87937 remove overlapping slots from app.Sanic, fix broken slots inherit of HTTPResponse (#2387) 2022-02-24 17:45:23 +02:00
Bluenix
d4fb44e986 Document middleware on_request and on_response (#2398) 2022-02-13 21:08:08 +02:00
Ryu juheon
68b654d981 fix(tasks): newly assigned `None` in registry (#2381) 2022-02-08 08:33:09 +02:00
13 changed files with 123 additions and 99 deletions

View File

@@ -8,7 +8,6 @@ on:
jobs: jobs:
testPy310: testPy310:
if: github.event.pull_request.draft == false
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }} name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:

View File

@@ -140,6 +140,7 @@ To maintain the code consistency, Sanic uses following tools.
#. `isort <https://github.com/timothycrosley/isort>`_ #. `isort <https://github.com/timothycrosley/isort>`_
#. `black <https://github.com/python/black>`_ #. `black <https://github.com/python/black>`_
#. `flake8 <https://github.com/PyCQA/flake8>`_ #. `flake8 <https://github.com/PyCQA/flake8>`_
#. `slotscheck <https://github.com/ariebovenberg/slotscheck>`_
isort isort
***** *****
@@ -167,7 +168,13 @@ flake8
#. pycodestyle #. pycodestyle
#. Ned Batchelder's McCabe script #. Ned Batchelder's McCabe script
``isort``\ , ``black`` and ``flake8`` checks are performed during ``tox`` lint checks. 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.
The **easiest** way to make your code conform is to run the following before committing. The **easiest** way to make your code conform is to run the following before committing.

View File

@@ -6,5 +6,5 @@ data = ""
for i in range(1, 250000): for i in range(1, 250000):
data += str(i) 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) print(r.text)

View File

@@ -44,11 +44,8 @@ from typing import (
from urllib.parse import urlencode, urlunparse from urllib.parse import urlencode, urlunparse
from warnings import filterwarnings from warnings import filterwarnings
from sanic_routing.exceptions import ( # type: ignore from sanic_routing.exceptions import FinalizationError, NotFound
FinalizationError, from sanic_routing.route import Route
NotFound,
)
from sanic_routing.route import Route # type: ignore
from sanic.application.ext import setup_ext from sanic.application.ext import setup_ext
from sanic.application.state import ApplicationState, Mode, ServerStage from sanic.application.state import ApplicationState, Mode, ServerStage
@@ -142,7 +139,6 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
"error_handler", "error_handler",
"go_fast", "go_fast",
"listeners", "listeners",
"name",
"named_request_middleware", "named_request_middleware",
"named_response_middleware", "named_response_middleware",
"request_class", "request_class",
@@ -1268,10 +1264,9 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
... ...
def purge_tasks(self): def purge_tasks(self):
for task in self.tasks: for key, task in self._task_registry.items():
if task.done() or task.cancelled(): if task.done() or task.cancelled():
name = task.get_name() self._task_registry[key] = None
self._task_registry[name] = None
self._task_registry = { self._task_registry = {
k: v for k, v in self._task_registry.items() if v is not None k: v for k, v in self._task_registry.items() if v is not None

View File

@@ -16,9 +16,9 @@ class MiddlewareMixin(metaclass=SanicMeta):
self, middleware_or_request, attach_to="request", apply=True self, middleware_or_request, attach_to="request", apply=True
): ):
""" """
Decorate and register middleware to be called before a request. Decorate and register middleware to be called before a request
Can either be called as *@app.middleware* or is handled or after a response is created. Can either be called as
*@app.middleware('request')* *@app.middleware* or *@app.middleware('request')*.
`See user guide re: middleware `See user guide re: middleware
<https://sanicframework.org/guide/basics/middleware.html>`__ <https://sanicframework.org/guide/basics/middleware.html>`__
@@ -47,12 +47,25 @@ class MiddlewareMixin(metaclass=SanicMeta):
) )
def on_request(self, middleware=None): 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): if callable(middleware):
return self.middleware(middleware, "request") return self.middleware(middleware, "request")
else: else:
return partial(self.middleware, attach_to="request") return partial(self.middleware, attach_to="request")
def on_response(self, middleware=None): 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): if callable(middleware):
return self.middleware(middleware, "response") return self.middleware(middleware, "response")
else: else:

View File

@@ -50,6 +50,16 @@ class BaseHTTPResponse:
The base class for all HTTP Responses The base class for all HTTP Responses
""" """
__slots__ = (
"asgi",
"body",
"content_type",
"stream",
"status",
"headers",
"_cookies",
)
_dumps = json_dumps _dumps = json_dumps
def __init__(self): def __init__(self):
@@ -156,7 +166,7 @@ class HTTPResponse(BaseHTTPResponse):
:type content_type: Optional[str] :type content_type: Optional[str]
""" """
__slots__ = ("body", "status", "content_type", "headers", "_cookies") __slots__ = ()
def __init__( def __init__(
self, self,

View File

@@ -1,26 +1,12 @@
import asyncio import asyncio
from distutils.util import strtobool
from os import getenv from os import getenv
from sanic.compat import OS_IS_WINDOWS from sanic.compat import OS_IS_WINDOWS
from sanic.log import error_logger 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: def try_use_uvloop() -> None:
""" """
Use uvloop instead of the default asyncio loop. Use uvloop instead of the default asyncio loop.

View File

@@ -11,18 +11,35 @@ from sanic.helpers import import_string
def str_to_bool(val: str) -> bool: 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
True values are y, yes, t, true, on and 1; false values are n, no, f, If val is in case insensitive (
false, off and 0. Raises ValueError if val is anything else. "y", "yes", "yep", "yup", "t",
""" "true", "on", "enable", "enabled", "1"
if val.lower() in ["y", "yes", "t", "true", "on", "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",
}:
return True return True
elif val.lower() in ["n", "no", "f", "false", "off", "0"]: elif val in {"n", "no", "f", "false", "off", "disable", "disabled", "0"}:
return False return False
else: else:
raise ValueError(f"String value {val} cannot be converted to bool") raise ValueError(f"Invalid truth value {val}")
def load_module_from_file_location( def load_module_from_file_location(

View File

@@ -6,6 +6,8 @@ import os
import re import re
import sys import sys
from distutils.util import strtobool
from setuptools import find_packages, setup from setuptools import find_packages, setup
from setuptools.command.test import test as TestCommand from setuptools.command.test import test as TestCommand
@@ -59,7 +61,7 @@ setup_kwargs = {
"Build fast. Run fast." "Build fast. Run fast."
), ),
"long_description": long_description, "long_description": long_description,
"packages": find_packages(include=[]), "packages": find_packages(),
"package_data": {"sanic": ["py.typed"]}, "package_data": {"sanic": ["py.typed"]},
"platforms": "any", "platforms": "any",
"python_requires": ">=3.7", "python_requires": ">=3.7",
@@ -110,6 +112,7 @@ tests_require = [
"docutils", "docutils",
"pygments", "pygments",
"uvicorn<0.15.0", "uvicorn<0.15.0",
"slotscheck>=0.8.0,<1",
types_ujson, types_ujson,
] ]
@@ -130,23 +133,6 @@ dev_require = tests_require + [
all_require = list(set(dev_require + docs_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")): if strtobool(os.environ.get("SANIC_NO_UJSON", "no")):
print("Installing without uJSON") print("Installing without uJSON")
requirements.remove(ujson) requirements.remove(ujson)

View File

@@ -5,6 +5,7 @@ from pathlib import Path
import pytest import pytest
from pyparsing import line
from sanic_routing import __version__ as __routing_version__ from sanic_routing import __version__ as __routing_version__
from sanic import __version__ from sanic import __version__
@@ -52,8 +53,10 @@ def test_server_run(appname):
out, err, exitcode = capture(command) out, err, exitcode = capture(command)
lines = out.split(b"\n") lines = out.split(b"\n")
firstline = lines[starting_line(lines) + 1] firstline = lines[starting_line(lines) + 1]
error_message = f"Lines found: {lines}\nErr output: {err}"
assert exitcode != 1 assert exitcode != 1
assert lines, error_message
assert firstline == b"Goin' Fast @ http://127.0.0.1:8000" assert firstline == b"Goin' Fast @ http://127.0.0.1:8000"
@@ -80,6 +83,9 @@ def test_tls_options(cmd):
out, err, exitcode = capture(command) out, err, exitcode = capture(command)
assert exitcode != 1 assert exitcode != 1
lines = out.split(b"\n") lines = out.split(b"\n")
error_message = f"Lines found: {lines}\nErr output: {err}"
assert lines, error_message
firstline = lines[starting_line(lines) + 1] firstline = lines[starting_line(lines) + 1]
assert firstline == b"Goin' Fast @ https://127.0.0.1:9999" assert firstline == b"Goin' Fast @ https://127.0.0.1:9999"
@@ -102,7 +108,9 @@ def test_tls_wrong_options(cmd):
assert exitcode == 1 assert exitcode == 1
assert not out assert not out
lines = err.decode().split("\n") lines = err.decode().split("\n")
error_message = f"Lines found: {lines}\nErr output: {err}"
assert lines, error_message
errmsg = lines[6] errmsg = lines[6]
assert errmsg == "TLS certificates must be specified by either of:" assert errmsg == "TLS certificates must be specified by either of:"
@@ -119,9 +127,11 @@ def test_host_port_localhost(cmd):
out, err, exitcode = capture(command) out, err, exitcode = capture(command)
lines = out.split(b"\n") lines = out.split(b"\n")
expected = b"Goin' Fast @ http://localhost:9999" expected = b"Goin' Fast @ http://localhost:9999"
error_message = f"Lines found: {lines}\nErr output: {err}"
assert exitcode != 1 assert exitcode != 1
assert expected in lines, f"Lines found: {lines}\nErr output: {err}" assert lines, error_message
assert expected in lines, error_message
@pytest.mark.parametrize( @pytest.mark.parametrize(
@@ -136,9 +146,11 @@ def test_host_port_ipv4(cmd):
out, err, exitcode = capture(command) out, err, exitcode = capture(command)
lines = out.split(b"\n") lines = out.split(b"\n")
expected = b"Goin' Fast @ http://127.0.0.127:9999" expected = b"Goin' Fast @ http://127.0.0.127:9999"
error_message = f"Lines found: {lines}\nErr output: {err}"
assert exitcode != 1 assert exitcode != 1
assert expected in lines, f"Lines found: {lines}\nErr output: {err}" assert lines, error_message
assert expected in lines, error_message
@pytest.mark.parametrize( @pytest.mark.parametrize(
@@ -153,9 +165,11 @@ def test_host_port_ipv6_any(cmd):
out, err, exitcode = capture(command) out, err, exitcode = capture(command)
lines = out.split(b"\n") lines = out.split(b"\n")
expected = b"Goin' Fast @ http://[::]:9999" expected = b"Goin' Fast @ http://[::]:9999"
error_message = f"Lines found: {lines}\nErr output: {err}"
assert exitcode != 1 assert exitcode != 1
assert expected in lines, f"Lines found: {lines}\nErr output: {err}" assert lines, error_message
assert expected in lines, error_message
@pytest.mark.parametrize( @pytest.mark.parametrize(
@@ -170,9 +184,11 @@ def test_host_port_ipv6_loopback(cmd):
out, err, exitcode = capture(command) out, err, exitcode = capture(command)
lines = out.split(b"\n") lines = out.split(b"\n")
expected = b"Goin' Fast @ http://[::1]:9999" expected = b"Goin' Fast @ http://[::1]:9999"
error_message = f"Lines found: {lines}\nErr output: {err}"
assert exitcode != 1 assert exitcode != 1
assert expected in lines, f"Lines found: {lines}\nErr output: {err}" assert lines, error_message
assert expected in lines, error_message
@pytest.mark.parametrize( @pytest.mark.parametrize(
@@ -206,12 +222,12 @@ def test_debug(cmd):
out, err, exitcode = capture(command) out, err, exitcode = capture(command)
lines = out.split(b"\n") lines = out.split(b"\n")
info = read_app_info(lines) info = read_app_info(lines)
error_message = f"Lines found: {lines}\nErr output: {err}"
assert info["debug"] is True, f"Lines found: {lines}\nErr output: {err}" assert info, error_message
assert ( assert info["debug"] is True, error_message
info["auto_reload"] is False assert info["auto_reload"] is False, error_message
), f"Lines found: {lines}\nErr output: {err}" assert "dev" not in info, error_message
assert "dev" not in info, f"Lines found: {lines}\nErr output: {err}"
@pytest.mark.parametrize("cmd", ("--dev", "-d")) @pytest.mark.parametrize("cmd", ("--dev", "-d"))
@@ -220,11 +236,11 @@ def test_dev(cmd):
out, err, exitcode = capture(command) out, err, exitcode = capture(command)
lines = out.split(b"\n") lines = out.split(b"\n")
info = read_app_info(lines) info = read_app_info(lines)
error_message = f"Lines found: {lines}\nErr output: {err}"
assert info["debug"] is True, f"Lines found: {lines}\nErr output: {err}" assert info, error_message
assert ( assert info["debug"] is True, error_message
info["auto_reload"] is True assert info["auto_reload"] is True, error_message
), f"Lines found: {lines}\nErr output: {err}"
@pytest.mark.parametrize("cmd", ("--auto-reload", "-r")) @pytest.mark.parametrize("cmd", ("--auto-reload", "-r"))
@@ -233,12 +249,12 @@ def test_auto_reload(cmd):
out, err, exitcode = capture(command) out, err, exitcode = capture(command)
lines = out.split(b"\n") lines = out.split(b"\n")
info = read_app_info(lines) info = read_app_info(lines)
error_message = f"Lines found: {lines}\nErr output: {err}"
assert info["debug"] is False, f"Lines found: {lines}\nErr output: {err}" assert info, error_message
assert ( assert info["debug"] is False, error_message
info["auto_reload"] is True assert info["auto_reload"] is True, error_message
), f"Lines found: {lines}\nErr output: {err}" assert "dev" not in info, error_message
assert "dev" not in info, f"Lines found: {lines}\nErr output: {err}"
@pytest.mark.parametrize( @pytest.mark.parametrize(
@@ -249,10 +265,10 @@ def test_access_logs(cmd, expected):
out, err, exitcode = capture(command) out, err, exitcode = capture(command)
lines = out.split(b"\n") lines = out.split(b"\n")
info = read_app_info(lines) info = read_app_info(lines)
error_message = f"Lines found: {lines}\nErr output: {err}"
assert ( assert info, error_message
info["access_log"] is expected assert info["access_log"] is expected, error_message
), f"Lines found: {lines}\nErr output: {err}"
@pytest.mark.parametrize("cmd", ("--version", "-v")) @pytest.mark.parametrize("cmd", ("--version", "-v"))
@@ -276,7 +292,7 @@ def test_noisy_exceptions(cmd, expected):
out, err, exitcode = capture(command) out, err, exitcode = capture(command)
lines = out.split(b"\n") lines = out.split(b"\n")
info = read_app_info(lines) info = read_app_info(lines)
error_message = f"Lines found: {lines}\nErr output: {err}"
assert ( assert info, error_message
info["noisy_exceptions"] is expected assert info["noisy_exceptions"] is expected, error_message
), f"Lines found: {lines}\nErr output: {err}"

View File

@@ -80,6 +80,18 @@ async def test_purge_tasks(app: Sanic):
assert len(app._task_registry) == 0 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(): def test_shutdown_tasks_on_app_stop():
class TestSanic(Sanic): class TestSanic(Sanic):
shutdown_tasks = Mock() shutdown_tasks = Mock()

View File

@@ -6,7 +6,6 @@ import pytest
from sanic.exceptions import LoadFileException from sanic.exceptions import LoadFileException
from sanic.utils import load_module_from_file_location from sanic.utils import load_module_from_file_location
from sanic.utils import str_to_bool as strtobool
@pytest.mark.parametrize( @pytest.mark.parametrize(
@@ -49,20 +48,3 @@ def test_load_module_from_file_location_using_env():
module = load_module_from_file_location(location) module = load_module_from_file_location(location)
assert isinstance(module, ModuleType) 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

View File

@@ -21,6 +21,7 @@ commands =
flake8 sanic flake8 sanic
black --config ./.black.toml --check --verbose sanic/ black --config ./.black.toml --check --verbose sanic/
isort --check-only sanic --profile=black isort --check-only sanic --profile=black
slotscheck --verbose -m sanic
[testenv:type-checking] [testenv:type-checking]
commands = commands =