Compare commits

..

15 Commits

Author SHA1 Message Date
Stephen Sadowski
92bfeaefd5 tests: fix for typing httptools error import - ignore 2022-02-02 11:37:50 -06:00
Stephen Sadowski
5a650f5758 fix: unused f-string 2022-02-02 11:32:31 -06:00
Stephen Sadowski
850b1f432c Merge branch 'main' into feat-2394-url_parse 2022-02-02 08:30:09 -06:00
Stephen Sadowski
df8480156d test: update malformed url test 2022-02-01 20:14:57 -06:00
Stephen Sadowski
d0cfb69f43 lint: fixed linting (again) across all files 2022-02-01 19:35:52 -06:00
Stephen Sadowski
19eee8bfb2 fix: replace strtobool inside setup.py; including from sanic.utils is a no-go 2022-02-01 19:35:52 -06:00
Stephen Sadowski
625cffb1b9 fix: linting errors 2022-02-01 19:35:52 -06:00
Stephen Sadowski
c5ac28cbcd test: add strtobool validation test for deprecation replacement 2022-02-01 19:35:52 -06:00
Stephen Sadowski
e57bea28f7 fix: update sanic.utils.str_to_bool to match deprecated strtobool, changed setup.py to use sanic.utils.str_to_bool 2022-02-01 19:35:52 -06:00
Stephen Sadowski
28665e31ce fix: added message when ValueError is raised for strtobool replacement 2022-02-01 19:35:52 -06:00
Stephen Sadowski
9f41936861 fix: removed distutils dependency in setup.py per PEP 632 2022-02-01 19:35:52 -06:00
Stephen Sadowski
7835492b09 fix: E501 line too long. 2022-02-01 19:35:52 -06:00
Stephen Sadowski
ca3bea3425 fix: replace distutils.strtobool() with locally implemented version per PEP 632 2022-02-01 19:35:52 -06:00
Stephen Sadowski
88ca56dda3 test: stubbed malformed url test 2022-02-01 15:45:09 -06:00
Stephen Sadowski
d2ab46d70b feat: better error handling for malformed urls 2022-02-01 15:44:33 -06:00
38 changed files with 152 additions and 574 deletions

View File

@@ -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.

View File

@@ -1,7 +1,6 @@
📜 Changelog
============
.. mdinclude:: ./releases/22/22.3.md
.. mdinclude:: ./releases/21/21.12.md
.. mdinclude:: ./releases/21/21.9.md
.. include:: ../../CHANGELOG.rst

View File

@@ -1,52 +0,0 @@
## Version 22.3.0
### Features
- [#2347](https://github.com/sanic-org/sanic/pull/2347) API for multi-application server
- 🚨 *BREAKING CHANGE*: The old `sanic.worker.GunicornWorker` has been **removed**. To run Sanic with `gunicorn`, you should use it thru `uvicorn` [as described in their docs](https://www.uvicorn.org/#running-with-gunicorn).
- 🧁 *SIDE EFFECT*: Named background tasks are now supported, even in Python 3.7
- [#2357](https://github.com/sanic-org/sanic/pull/2357) Parse `Authorization` header as `Request.credentials`
- [#2361](https://github.com/sanic-org/sanic/pull/2361) Add config option to skip `Touchup` step in application startup
- [#2372](https://github.com/sanic-org/sanic/pull/2372) Updates to CLI help messaging
- [#2382](https://github.com/sanic-org/sanic/pull/2382) Downgrade warnings to backwater debug messages
- [#2396](https://github.com/sanic-org/sanic/pull/2396) Allow for `multidict` v0.6
- [#2401](https://github.com/sanic-org/sanic/pull/2401) Upgrade CLI catching for alternative application run types
- [#2402](https://github.com/sanic-org/sanic/pull/2402) Conditionally inject CLI arguments into factory
- [#2413](https://github.com/sanic-org/sanic/pull/2413) Add new start and stop event listeners to reloader process
- [#2414](https://github.com/sanic-org/sanic/pull/2414) Remove loop as required listener arg
- [#2415](https://github.com/sanic-org/sanic/pull/2415) Better exception for bad URL parsing
- [sanic-routing#47](https://github.com/sanic-org/sanic-routing/pull/47) Add a new extention parameter type: `<file:ext>`, `<file:ext=jpg>`, `<file:ext=jpg|png|gif|svg>`, `<file=int:ext>`, `<file=int:ext=jpg|png|gif|svg>`, `<file=float:ext=tar.gz>`
- 👶 *BETA FEATURE*: This feature will not work with `path` type matching, and is being released as a beta feature only.
- [sanic-routing#57](https://github.com/sanic-org/sanic-routing/pull/57) Change `register_pattern` to accept a `str` or `Pattern`
- [sanic-routing#58](https://github.com/sanic-org/sanic-routing/pull/58) Default matching on non-empty strings only, and new `strorempty` pattern type
- 🚨 *BREAKING CHANGE*: Previously a route with a dynamic string parameter (`/<foo>` or `/<foo:str>`) would match on any string, including empty strings. It will now **only** match a non-empty string. To retain the old behavior, you should use the new parameter type: `/<foo:strorempty>`.
### Bugfixes
- [#2373](https://github.com/sanic-org/sanic/pull/2373) Remove `error_logger` on websockets
- [#2381](https://github.com/sanic-org/sanic/pull/2381) Fix newly assigned `None` in task registry
- [sanic-routing#52](https://github.com/sanic-org/sanic-routing/pull/52) Add type casting to regex route matching
- [sanic-routing#60](https://github.com/sanic-org/sanic-routing/pull/60) Add requirements check on regex routes (this resolves, for example, multiple static directories with differing `host` values)
### Deprecations and Removals
- [#2362](https://github.com/sanic-org/sanic/pull/2362) 22.3 Deprecations and changes
1. `debug=True` and `--debug` do _NOT_ automatically run `auto_reload`
2. Default error render is with plain text (browsers still get HTML by default because `auto` looks at headers)
3. `config` is required for `ErrorHandler.finalize`
4. `ErrorHandler.lookup` requires two positional args
5. Unused websocket protocol args removed
- [#2344](https://github.com/sanic-org/sanic/pull/2344) Deprecate loading of lowercase environment variables
### Developer infrastructure
- [#2363](https://github.com/sanic-org/sanic/pull/2363) Revert code coverage back to Codecov
- [#2405](https://github.com/sanic-org/sanic/pull/2405) Upgrade tests for `sanic-routing` changes
- [sanic-testing#35](https://github.com/sanic-org/sanic-testing/pull/35) Allow for httpx v0.22
### Improved Documentation
- [#2350](https://github.com/sanic-org/sanic/pull/2350) Fix link in README for ASGI
- [#2398](https://github.com/sanic-org/sanic/pull/2398) Document middleware on_request and on_response
- [#2409](https://github.com/sanic-org/sanic/pull/2409) Add missing documentation for `Request.respond`
### Miscellaneous
- [#2376](https://github.com/sanic-org/sanic/pull/2376) Fix typing for `ListenerMixin.listener`
- [#2383](https://github.com/sanic-org/sanic/pull/2383) Clear deprecation warning in `asyncio.wait`
- [#2387](https://github.com/sanic-org/sanic/pull/2387) Cleanup `__slots__` implementations
- [#2390](https://github.com/sanic-org/sanic/pull/2390) Clear deprecation warning in `asyncio.get_event_loop`

View File

@@ -4,7 +4,6 @@ from sanic import Sanic, response
app = Sanic("DelayedResponseApp", strict_slashes=True)
app.config.AUTO_EXTEND = False
@app.get("/")
@@ -12,7 +11,7 @@ async def handler(request):
return response.redirect("/sleep/3")
@app.get("/sleep/<t:float>")
@app.get("/sleep/<t:number>")
async def handler2(request, t=0.3):
await sleep(t)
return response.text(f"Slept {t:.1f} seconds.\n")

View File

@@ -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)

View File

@@ -1 +1 @@
__version__ = "22.3.2"
__version__ = "22.3.0.dev1"

View File

@@ -11,6 +11,7 @@ from asyncio import (
CancelledError,
Task,
ensure_future,
get_event_loop,
get_running_loop,
wait_for,
)
@@ -141,6 +142,7 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
"error_handler",
"go_fast",
"listeners",
"name",
"named_request_middleware",
"named_response_middleware",
"request_class",
@@ -252,13 +254,7 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
"Loop can only be retrieved after the app has started "
"running. Not supported with `create_server` function"
)
try:
return get_running_loop()
except RuntimeError:
if sys.version_info > (3, 10):
return asyncio.get_event_loop_policy().get_event_loop()
else:
return asyncio.get_event_loop()
return get_event_loop()
# -------------------------------------------------------------------- #
# Registration
@@ -1136,10 +1132,7 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
async def _listener(
app: Sanic, loop: AbstractEventLoop, listener: ListenerType
):
try:
maybe_coro = listener(app) # type: ignore
except TypeError:
maybe_coro = listener(app, loop) # type: ignore
maybe_coro = listener(app, loop)
if maybe_coro and isawaitable(maybe_coro):
await maybe_coro
@@ -1275,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
@@ -1516,8 +1510,7 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
if not Sanic.test_mode:
raise e
def signalize(self, allow_fail_builtin=True):
self.signal_router.allow_fail_builtin = allow_fail_builtin
def signalize(self):
try:
self.signal_router.finalize()
except FinalizationError as e:
@@ -1532,11 +1525,8 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
if hasattr(self, "_ext"):
self.ext._display()
if self.state.is_debug:
self.config.TOUCHUP = False
# Setup routers
self.signalize(self.config.TOUCHUP)
self.signalize()
self.finalize()
# TODO: Replace in v22.6 to check against apps in app registry
@@ -1556,8 +1546,7 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
# TODO:
# - Raise warning if secondary apps have error handler config
ErrorHandler.finalize(self.error_handler, config=self.config)
if self.config.TOUCHUP:
TouchUp.run(self)
TouchUp.run(self)
self.state.is_started = True

View File

@@ -68,13 +68,6 @@ Or, a path to a directory to run as a simple HTTP server:
legacy_version = len(sys.argv) == 2 and sys.argv[-1] == "-v"
parse_args = ["--version"] if legacy_version else None
if not parse_args:
parsed, unknown = self.parser.parse_known_args()
if unknown and parsed.factory:
for arg in unknown:
if arg.startswith("--"):
self.parser.add_argument(arg.split("=")[0])
self.args = self.parser.parse_args(args=parse_args)
self._precheck()
@@ -120,14 +113,6 @@ Or, a path to a directory to run as a simple HTTP server:
delimiter = ":" if ":" in self.args.module else "."
module_name, app_name = self.args.module.rsplit(delimiter, 1)
if module_name == "" and os.path.isdir(self.args.module):
raise ValueError(
"App not found.\n"
" Please use --simple if you are passing a "
"directory to sanic.\n"
f" eg. sanic {self.args.module} --simple"
)
if app_name.endswith("()"):
self.args.factory = True
app_name = app_name[:-2]
@@ -135,26 +120,14 @@ Or, a path to a directory to run as a simple HTTP server:
module = import_module(module_name)
app = getattr(module, app_name, None)
if self.args.factory:
try:
app = app(self.args)
except TypeError:
app = app()
app = app()
app_type_name = type(app).__name__
if not isinstance(app, Sanic):
if callable(app):
solution = f"sanic {self.args.module} --factory"
raise ValueError(
"Module is not a Sanic app, it is a"
f"{app_type_name}\n"
" If this callable returns a"
f"Sanic instance try: \n{solution}"
)
raise ValueError(
f"Module is not a Sanic app, it is a {app_type_name}\n"
f" Perhaps you meant {self.args.module}:app?"
f" Perhaps you meant {self.args.module}.app?"
)
except ImportError as e:
if module_name.startswith(e.name):

View File

@@ -72,7 +72,7 @@ def ctrlc_workaround_for_windows(app):
"""Asyncio wakeups to allow receiving SIGINT in Python"""
while not die:
# If someone else stopped the app, just exit
if app.state.is_stopping:
if app.is_stopping:
return
# Windows Python blocks signal handlers while the event loop is
# waiting for I/O. Frequent wakeups keep interrupts flowing.

View File

@@ -38,7 +38,6 @@ DEFAULT_CONFIG = {
"REQUEST_MAX_SIZE": 100000000, # 100 megabytes
"REQUEST_TIMEOUT": 60, # 60 seconds
"RESPONSE_TIMEOUT": 60, # 60 seconds
"TOUCHUP": True,
"USE_UVLOOP": _default,
"WEBSOCKET_MAX_SIZE": 2**20, # 1 megabyte
"WEBSOCKET_PING_INTERVAL": 20,
@@ -82,7 +81,6 @@ class Config(dict, metaclass=DescriptorMeta):
REQUEST_TIMEOUT: int
RESPONSE_TIMEOUT: int
SERVER_NAME: str
TOUCHUP: bool
USE_UVLOOP: Union[Default, bool]
WEBSOCKET_MAX_SIZE: int
WEBSOCKET_PING_INTERVAL: int

View File

@@ -51,10 +51,6 @@ class InvalidUsage(SanicException):
quiet = True
class BadURL(InvalidUsage):
...
class MethodNotSupported(SanicException):
"""
**Status**: 405 Method Not Allowed

View File

@@ -78,7 +78,7 @@ class ErrorHandler:
@classmethod
def _get_fallback_value(cls, error_handler: ErrorHandler, config: Config):
if error_handler._fallback is not _default:
if config._FALLBACK_ERROR_FORMAT == error_handler._fallback:
if config._FALLBACK_ERROR_FORMAT is _default:
return error_handler.fallback
error_logger.warning(

View File

@@ -1,9 +1,8 @@
from enum import Enum, auto
from functools import partial
from typing import Callable, List, Optional, Union, overload
from typing import List, Optional, Union
from sanic.base.meta import SanicMeta
from sanic.exceptions import InvalidUsage
from sanic.models.futures import FutureListener
from sanic.models.handler_types import ListenerType, Sanic
@@ -18,8 +17,6 @@ class ListenerEvent(str, Enum):
AFTER_SERVER_STOP = "server.shutdown.after"
MAIN_PROCESS_START = auto()
MAIN_PROCESS_STOP = auto()
RELOAD_PROCESS_START = auto()
RELOAD_PROCESS_STOP = auto()
class ListenerMixin(metaclass=SanicMeta):
@@ -29,33 +26,12 @@ class ListenerMixin(metaclass=SanicMeta):
def _apply_listener(self, listener: FutureListener):
raise NotImplementedError # noqa
@overload
def listener(
self,
listener_or_event: ListenerType[Sanic],
event_or_none: str,
apply: bool = ...,
) -> ListenerType[Sanic]:
...
@overload
def listener(
self,
listener_or_event: str,
event_or_none: None = ...,
apply: bool = ...,
) -> Callable[[ListenerType[Sanic]], ListenerType[Sanic]]:
...
def listener(
self,
listener_or_event: Union[ListenerType[Sanic], str],
event_or_none: Optional[str] = None,
apply: bool = True,
) -> Union[
ListenerType[Sanic],
Callable[[ListenerType[Sanic]], ListenerType[Sanic]],
]:
) -> ListenerType[Sanic]:
"""
Create a listener from a decorated function.
@@ -73,9 +49,7 @@ class ListenerMixin(metaclass=SanicMeta):
:param event: event to listen to
"""
def register_listener(
listener: ListenerType[Sanic], event: str
) -> ListenerType[Sanic]:
def register_listener(listener, event):
nonlocal apply
future_listener = FutureListener(listener, event)
@@ -85,10 +59,6 @@ class ListenerMixin(metaclass=SanicMeta):
return listener
if callable(listener_or_event):
if event_or_none is None:
raise InvalidUsage(
"Invalid event registration: Missing event name."
)
return register_listener(listener_or_event, event_or_none)
else:
return partial(register_listener, event=listener_or_event)
@@ -103,16 +73,6 @@ class ListenerMixin(metaclass=SanicMeta):
) -> ListenerType[Sanic]:
return self.listener(listener, "main_process_stop")
def reload_process_start(
self, listener: ListenerType[Sanic]
) -> ListenerType[Sanic]:
return self.listener(listener, "reload_process_start")
def reload_process_stop(
self, listener: ListenerType[Sanic]
) -> ListenerType[Sanic]:
return self.listener(listener, "reload_process_stop")
def before_server_start(
self, listener: ListenerType[Sanic]
) -> ListenerType[Sanic]:

View File

@@ -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:

View File

@@ -11,7 +11,6 @@ from asyncio import (
all_tasks,
get_event_loop,
get_running_loop,
new_event_loop,
)
from contextlib import suppress
from functools import partial
@@ -33,7 +32,6 @@ from sanic.models.handler_types import ListenerType
from sanic.server import Signal as ServerSignal
from sanic.server import try_use_uvloop
from sanic.server.async_server import AsyncioServer
from sanic.server.events import trigger_events
from sanic.server.protocols.http_protocol import HttpProtocol
from sanic.server.protocols.websocket_protocol import WebSocketProtocol
from sanic.server.runners import serve, serve_multiple, serve_single
@@ -540,21 +538,15 @@ class RunnerMixin(metaclass=SanicMeta):
except IndexError:
raise RuntimeError("Did not find any applications.")
reloader_start = primary.listeners.get("reload_process_start")
reloader_stop = primary.listeners.get("reload_process_stop")
# We want to run auto_reload if ANY of the applications have it enabled
if (
cls.should_auto_reload()
and os.environ.get("SANIC_SERVER_RUNNING") != "true"
): # no cov
loop = new_event_loop()
trigger_events(reloader_start, loop, primary)
):
reload_dirs: Set[Path] = primary.state.reload_dirs.union(
*(app.state.reload_dirs for app in apps)
)
reloader_helpers.watchdog(1.0, reload_dirs)
trigger_events(reloader_stop, loop, primary)
return
return reloader_helpers.watchdog(1.0, reload_dirs)
# This exists primarily for unit testing
if not primary.state.server_info: # no cov

View File

@@ -1,13 +1,11 @@
from asyncio.events import AbstractEventLoop
from typing import Any, Callable, Coroutine, Optional, TypeVar, Union
import sanic
from sanic.request import Request
from sanic.response import BaseHTTPResponse, HTTPResponse
Sanic = TypeVar("Sanic", bound="sanic.Sanic")
Sanic = TypeVar("Sanic")
MiddlewareResponse = Union[
Optional[HTTPResponse], Coroutine[Any, Any, Optional[HTTPResponse]]
@@ -20,9 +18,8 @@ ErrorMiddlewareType = Callable[
[Request, BaseException], Optional[Coroutine[Any, Any, None]]
]
MiddlewareType = Union[RequestMiddlewareType, ResponseMiddlewareType]
ListenerType = Union[
Callable[[Sanic], Optional[Coroutine[Any, Any, None]]],
Callable[[Sanic, AbstractEventLoop], Optional[Coroutine[Any, Any, None]]],
ListenerType = Callable[
[Sanic, AbstractEventLoop], Optional[Coroutine[Any, Any, None]]
]
RouteHandler = Callable[..., Coroutine[Any, Any, Optional[HTTPResponse]]]
SignalHandler = Callable[..., Coroutine[Any, Any, None]]

View File

@@ -30,11 +30,11 @@ 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 httptools.parser.errors import HttpParserInvalidURLError # type: ignore
from sanic.compat import CancelledErrors, Header
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE
from sanic.exceptions import BadURL, InvalidUsage, ServerError
from sanic.exceptions import InvalidUsage, ServerError
from sanic.headers import (
AcceptContainer,
Options,
@@ -130,10 +130,13 @@ class Request:
):
self.raw_url = url_bytes
# TODO: Content-Encoding detection
try:
self._parsed_url = parse_url(url_bytes)
except HttpParserInvalidURLError:
raise BadURL(f"Bad URL: {url_bytes.decode()}")
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
@@ -200,53 +203,6 @@ class Request:
headers: Optional[Union[Header, Dict[str, str]]] = None,
content_type: Optional[str] = None,
):
"""Respond to the request without returning.
This method can only be called once, as you can only respond once.
If no ``response`` argument is passed, one will be created from the
``status``, ``headers`` and ``content_type`` arguments.
**The first typical usecase** is if you wish to respond to the
request without returning from the handler:
.. code-block:: python
@app.get("/")
async def handler(request: Request):
data = ... # Process something
json_response = json({"data": data})
await request.respond(json_response)
# You are now free to continue executing other code
...
@app.on_response
async def add_header(_, response: HTTPResponse):
# Middlewares still get executed as expected
response.headers["one"] = "two"
**The second possible usecase** is for when you want to directly
respond to the request:
.. code-block:: python
response = await request.respond(content_type="text/csv")
await response.send("foo,")
await response.send("bar")
# You can control the completion of the response by calling
# the 'eof()' method:
await response.eof()
:param response: response instance to send
:param status: status code to return in the response
:param headers: headers to return in the response
:param content_type: Content-Type header of the response
:return: final response being sent (may be different from the
``response`` parameter because of middlewares) which can be
used to manually send data
"""
try:
if self.stream is not None and self.stream.response:
raise ServerError("Second respond call is not allowed.")

View File

@@ -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,

View File

@@ -1,18 +1,8 @@
from __future__ import annotations
from inspect import isawaitable
from typing import TYPE_CHECKING, Any, Callable, Iterable, Optional
from typing import Any, Callable, Iterable, Optional
if TYPE_CHECKING: # no cov
from sanic import Sanic
def trigger_events(
events: Optional[Iterable[Callable[..., Any]]],
loop,
app: Optional[Sanic] = None,
):
def trigger_events(events: Optional[Iterable[Callable[..., Any]]], loop):
"""
Trigger event callbacks (functions or async)
@@ -21,9 +11,6 @@ def trigger_events(
"""
if events:
for event in events:
try:
result = event() if not app else event(app)
except TypeError:
result = event(loop) if not app else event(app, loop)
result = event(loop)
if isawaitable(result):
loop.run_until_complete(result)

View File

@@ -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.

View File

@@ -5,7 +5,7 @@ from websockets.server import ServerConnection
from websockets.typing import Subprotocol
from sanic.exceptions import ServerError
from sanic.log import logger
from sanic.log import error_logger
from sanic.server import HttpProtocol
from ..websockets.impl import WebsocketImplProtocol
@@ -104,7 +104,7 @@ class WebSocketProtocol(HttpProtocol):
max_size=self.websocket_max_size,
subprotocols=subprotocols,
state=OPEN,
logger=logger,
logger=error_logger,
)
resp: "http11.Response" = ws_conn.accept(request)
except Exception:

View File

@@ -80,7 +80,6 @@ class SignalRouter(BaseRouter):
group_class=SignalGroup,
stacking=True,
)
self.allow_fail_builtin = True
self.ctx.loop = None
def get( # type: ignore
@@ -130,8 +129,7 @@ class SignalRouter(BaseRouter):
try:
group, handlers, params = self.get(event, condition=condition)
except NotFound as e:
is_reserved = event.split(".", 1)[0] in RESERVED_NAMESPACES
if fail_not_found and (not is_reserved or self.allow_fail_builtin):
if fail_not_found:
raise e
else:
if self.ctx.app.debug and self.ctx.app.state.verbosity >= 1:

View File

@@ -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(

View File

@@ -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",
@@ -84,17 +82,17 @@ ujson = "ujson>=1.35" + env_dependency
uvloop = "uvloop>=0.5.3" + env_dependency
types_ujson = "types-ujson" + env_dependency
requirements = [
"sanic-routing>=22.3.0,<22.6.0",
"sanic-routing~=0.7",
"httptools>=0.0.10",
uvloop,
ujson,
"aiofiles>=0.6.0",
"websockets>=10.0",
"multidict>=5.0,<7.0",
"multidict>=5.0,<6.0",
]
tests_require = [
"sanic-testing>=22.3.0",
"sanic-testing>=0.7.0",
"pytest==6.2.5",
"coverage==5.3",
"gunicorn==20.0.4",
@@ -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)

View File

@@ -34,12 +34,3 @@ async def shutdown(app: Sanic, _):
def create_app():
return app
def create_app_with_args(args):
try:
print(f"foo={args.foo}")
except AttributeError:
print(f"module={args.module}")
return app

View File

@@ -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]

View File

@@ -39,17 +39,16 @@ def read_app_info(lines):
@pytest.mark.parametrize(
"appname,extra",
"appname",
(
("fake.server.app", None),
("fake.server:create_app", "--factory"),
("fake.server.create_app()", None),
"fake.server.app",
"fake.server:app",
"fake.server:create_app()",
"fake.server.create_app()",
),
)
def test_server_run(appname, extra):
def test_server_run(appname):
command = ["sanic", appname]
if extra:
command.append(extra)
out, err, exitcode = capture(command)
lines = out.split(b"\n")
firstline = lines[starting_line(lines) + 1]
@@ -58,49 +57,6 @@ def test_server_run(appname, extra):
assert firstline == b"Goin' Fast @ http://127.0.0.1:8000"
def test_server_run_factory_with_args():
command = [
"sanic",
"fake.server.create_app_with_args",
"--factory",
]
out, err, exitcode = capture(command)
lines = out.split(b"\n")
assert exitcode != 1, lines
assert b"module=fake.server.create_app_with_args" in lines
def test_server_run_factory_with_args_arbitrary():
command = [
"sanic",
"fake.server.create_app_with_args",
"--factory",
"--foo=bar",
]
out, err, exitcode = capture(command)
lines = out.split(b"\n")
assert exitcode != 1, lines
assert b"foo=bar" in lines
def test_error_with_function_as_instance_without_factory_arg():
command = ["sanic", "fake.server.create_app"]
out, err, exitcode = capture(command)
assert b"try: \nsanic fake.server.create_app --factory" in err
assert exitcode != 1
def test_error_with_path_as_instance_without_simple_arg():
command = ["sanic", "./fake/"]
out, err, exitcode = capture(command)
assert (
b"Please use --simple if you are passing a directory to sanic." in err
)
assert exitcode != 1
@pytest.mark.parametrize(
"cmd",
(

View File

@@ -164,12 +164,11 @@ def test_raw_headers(app):
},
)
assert b"Host: example.com" in request.raw_headers
assert b"Accept: */*" in request.raw_headers
assert b"Accept-Encoding: gzip, deflate" in request.raw_headers
assert b"Connection: keep-alive" in request.raw_headers
assert b"User-Agent: Sanic-Testing" in request.raw_headers
assert b"FOO: bar" in request.raw_headers
assert request.raw_headers == (
b"Host: example.com\r\nAccept: */*\r\nAccept-Encoding: gzip, "
b"deflate\r\nConnection: keep-alive\r\nUser-Agent: "
b"Sanic-Testing\r\nFOO: bar"
)
def test_request_line(app):

View File

@@ -58,36 +58,6 @@ def write_app(filename, **runargs):
return text
def write_listener_app(filename, **runargs):
start_text = secrets.token_urlsafe()
stop_text = secrets.token_urlsafe()
with open(filename, "w") as f:
f.write(
dedent(
f"""\
import os
from sanic import Sanic
app = Sanic(__name__)
app.route("/")(lambda x: x)
@app.reload_process_start
async def reload_start(*_):
print("reload_start", os.getpid(), {start_text!r})
@app.reload_process_stop
async def reload_stop(*_):
print("reload_stop", os.getpid(), {stop_text!r})
if __name__ == "__main__":
app.run(**{runargs!r})
"""
)
)
return start_text, stop_text
def write_json_config_app(filename, jsonfile, **runargs):
with open(filename, "w") as f:
f.write(
@@ -122,10 +92,10 @@ def write_file(filename):
return text
def scanner(proc, trigger="complete"):
def scanner(proc):
for line in proc.stdout:
line = line.decode().strip()
if line.startswith(trigger):
if line.startswith("complete"):
yield line
@@ -138,7 +108,7 @@ argv = dict(
"sanic",
"--port",
"42204",
"--auto-reload",
"--debug",
"reloader.app",
],
)
@@ -148,7 +118,7 @@ argv = dict(
"runargs, mode",
[
(dict(port=42202, auto_reload=True), "script"),
(dict(port=42203, auto_reload=True), "module"),
(dict(port=42203, debug=True), "module"),
({}, "sanic"),
],
)
@@ -181,7 +151,7 @@ async def test_reloader_live(runargs, mode):
"runargs, mode",
[
(dict(port=42302, auto_reload=True), "script"),
(dict(port=42303, auto_reload=True), "module"),
(dict(port=42303, debug=True), "module"),
({}, "sanic"),
],
)
@@ -213,30 +183,3 @@ async def test_reloader_live_with_dir(runargs, mode):
terminate(proc)
with suppress(TimeoutExpired):
proc.wait(timeout=3)
def test_reload_listeners():
with TemporaryDirectory() as tmpdir:
filename = os.path.join(tmpdir, "reloader.py")
start_text, stop_text = write_listener_app(
filename, port=42305, auto_reload=True
)
proc = Popen(
argv["script"], cwd=tmpdir, stdout=PIPE, creationflags=flags
)
try:
timeout = Timer(TIMER_DELAY, terminate, [proc])
timeout.start()
# Python apparently keeps using the old source sometimes if
# we don't sleep before rewrite (pycache timestamp problem?)
sleep(1)
line = scanner(proc, "reload_start")
assert start_text in next(line)
line = scanner(proc, "reload_stop")
assert stop_text in next(line)
finally:
timeout.cancel()
terminate(proc)
with suppress(TimeoutExpired):
proc.wait(timeout=3)

View File

@@ -4,7 +4,6 @@ from uuid import UUID, uuid4
import pytest
from sanic import Sanic, response
from sanic.exceptions import BadURL
from sanic.request import Request, uuid
from sanic.server import HttpProtocol
@@ -177,17 +176,3 @@ def test_request_accept():
"text/x-dvi; q=0.8",
"text/plain; q=0.5",
]
def test_bad_url_parse():
message = "Bad URL: my.redacted-domain.com:443"
with pytest.raises(BadURL, match=message):
Request(
b"my.redacted-domain.com:443",
Mock(),
Mock(),
Mock(),
Mock(),
Mock(),
Mock(),
)

View File

@@ -1,6 +1,8 @@
import asyncio
import re
from unittest.mock import Mock
import pytest
from sanic_routing.exceptions import (
@@ -254,7 +256,7 @@ def test_route_strict_slash(app):
def test_route_invalid_parameter_syntax(app):
with pytest.raises(InvalidUsage):
with pytest.raises(ValueError):
@app.get("/get/<:str>", strict_slashes=True)
def handler(request):

View File

@@ -33,14 +33,6 @@ def create_listener(listener_name, in_list):
return _listener
def create_listener_no_loop(listener_name, in_list):
async def _listener(app):
print(f"DEBUG MESSAGE FOR PYTEST for {listener_name}")
in_list.insert(0, app.name + listener_name)
return _listener
def start_stop_app(random_name_app, **run_kwargs):
def stop_on_alarm(signum, frame):
random_name_app.stop()
@@ -64,17 +56,6 @@ def test_single_listener(app, listener_name):
assert app.name + listener_name == output.pop()
@skipif_no_alarm
@pytest.mark.parametrize("listener_name", AVAILABLE_LISTENERS)
def test_single_listener_no_loop(app, listener_name):
"""Test that listeners on their own work"""
output = []
# Register listener
app.listener(listener_name)(create_listener_no_loop(listener_name, output))
start_stop_app(app)
assert app.name + listener_name == output.pop()
@skipif_no_alarm
@pytest.mark.parametrize("listener_name", AVAILABLE_LISTENERS)
def test_register_listener(app, listener_name):
@@ -218,16 +199,3 @@ async def test_missing_startup_raises_exception(app):
with pytest.raises(SanicException):
await srv.before_start()
def test_reload_listeners_attached(app):
async def dummy(*_):
...
app.reload_process_start(dummy)
app.reload_process_stop(dummy)
app.listener("reload_process_start")(dummy)
app.listener("reload_process_stop")(dummy)
assert len(app.listeners.get("reload_process_start")) == 2
assert len(app.listeners.get("reload_process_stop")) == 2

View File

@@ -3,7 +3,6 @@ import os
import signal
from queue import Queue
from types import SimpleNamespace
from unittest.mock import MagicMock
import pytest
@@ -11,7 +10,6 @@ import pytest
from sanic_testing.testing import HOST, PORT
from sanic.compat import ctrlc_workaround_for_windows
from sanic.exceptions import InvalidUsage
from sanic.response import HTTPResponse
@@ -75,12 +73,11 @@ def test_windows_workaround():
# Windows...
class MockApp:
def __init__(self):
self.state = SimpleNamespace()
self.state.is_stopping = False
self.is_stopping = False
def stop(self):
assert not self.state.is_stopping
self.state.is_stopping = True
assert not self.is_stopping
self.is_stopping = True
def add_task(self, func):
loop = asyncio.get_event_loop()
@@ -93,11 +90,11 @@ def test_windows_workaround():
if stop_first:
app.stop()
await asyncio.sleep(0.2)
assert app.state.is_stopping == stop_first
assert app.is_stopping == stop_first
# First Ctrl+C: should call app.stop() within 0.1 seconds
os.kill(os.getpid(), signal.SIGINT)
await asyncio.sleep(0.2)
assert app.state.is_stopping
assert app.is_stopping
assert app.stay_active_task.result() is None
# Second Ctrl+C should raise
with pytest.raises(KeyboardInterrupt):
@@ -111,17 +108,3 @@ def test_windows_workaround():
assert res == "OK"
res = loop.run_until_complete(atest(True))
assert res == "OK"
@pytest.mark.skipif(os.name == "nt", reason="May hang CI on py38/windows")
def test_signals_with_invalid_invocation(app):
"""Test if sanic register fails with invalid invocation"""
@app.route("/hello")
async def hello_route(request):
return HTTPResponse()
with pytest.raises(
InvalidUsage, match="Invalid event registration: Missing event name"
):
app.listener(stop)

View File

@@ -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()

View File

@@ -2,8 +2,6 @@ import logging
import pytest
from sanic_routing.exceptions import NotFound
from sanic.signals import RESERVED_NAMESPACES
from sanic.touchup import TouchUp
@@ -30,50 +28,3 @@ async def test_ode_removes_dispatch_events(app, caplog, verbosity, result):
)
in logs
) is result
@pytest.mark.parametrize("skip_it,result", ((False, True), (True, False)))
async def test_skip_touchup(app, caplog, skip_it, result):
app.config.TOUCHUP = not skip_it
with caplog.at_level(logging.DEBUG, logger="sanic.root"):
app.state.verbosity = 2
await app._startup()
assert app.signal_router.allow_fail_builtin is (not skip_it)
logs = caplog.record_tuples
for signal in RESERVED_NAMESPACES["http"]:
assert (
(
"sanic.root",
logging.DEBUG,
f"Disabling event: {signal}",
)
in logs
) is result
not_found_exceptions = 0
# Skip-touchup disables NotFound exceptions on the dispatcher
for signal in RESERVED_NAMESPACES["http"]:
try:
await app.dispatch(event=signal, inline=True)
except NotFound:
not_found_exceptions += 1
assert (not_found_exceptions > 0) is result
@pytest.mark.parametrize("skip_it,result", ((False, True), (True, True)))
async def test_skip_touchup_non_reserved(app, caplog, skip_it, result):
app.config.TOUCHUP = not skip_it
@app.signal("foo.bar.one")
def sync_signal(*_):
...
await app._startup()
assert app.signal_router.allow_fail_builtin is (not skip_it)
not_found_exception = False
# Skip-touchup doesn't disable NotFound exceptions for user-defined signals
try:
await app.dispatch(event="foo.baz.two", inline=True)
except NotFound:
not_found_exception = True
assert not_found_exception is result

View File

@@ -199,7 +199,7 @@ async def test_zero_downtime():
for _ in range(40):
async with httpx.AsyncClient(transport=transport) as client:
r = await client.get("http://localhost/sleep/0.1")
assert r.status_code == 200, r.text
assert r.status_code == 200, r.content
assert r.text == "Slept 0.1 seconds.\n"
def spawn():

View File

@@ -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

View File

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