Compare commits
18 Commits
fix-2388-s
...
v22.3.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc97287f8e | ||
|
|
00218aa9f2 | ||
|
|
874718db94 | ||
|
|
bb4474897f | ||
|
|
0cb342aef4 | ||
|
|
030987480c | ||
|
|
f6fdc80b40 | ||
|
|
361c242473 | ||
|
|
32962d1e1c | ||
|
|
6e0a6871b5 | ||
|
|
0030425c8c | ||
|
|
c9dbc8ed26 | ||
|
|
44b108b564 | ||
|
|
2a8e91052f | ||
|
|
0c9df02e66 | ||
|
|
7523e87937 | ||
|
|
d4fb44e986 | ||
|
|
68b654d981 |
@@ -140,6 +140,7 @@ 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
|
||||
*****
|
||||
@@ -167,7 +168,13 @@ flake8
|
||||
#. pycodestyle
|
||||
#. 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.
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
📜 Changelog
|
||||
============
|
||||
|
||||
.. mdinclude:: ./releases/22/22.3.md
|
||||
.. mdinclude:: ./releases/21/21.12.md
|
||||
.. mdinclude:: ./releases/21/21.9.md
|
||||
.. include:: ../../CHANGELOG.rst
|
||||
|
||||
52
docs/sanic/releases/22/22.3.md
Normal file
52
docs/sanic/releases/22/22.3.md
Normal file
@@ -0,0 +1,52 @@
|
||||
## 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`
|
||||
@@ -4,6 +4,7 @@ from sanic import Sanic, response
|
||||
|
||||
|
||||
app = Sanic("DelayedResponseApp", strict_slashes=True)
|
||||
app.config.AUTO_EXTEND = False
|
||||
|
||||
|
||||
@app.get("/")
|
||||
@@ -11,7 +12,7 @@ async def handler(request):
|
||||
return response.redirect("/sleep/3")
|
||||
|
||||
|
||||
@app.get("/sleep/<t:number>")
|
||||
@app.get("/sleep/<t:float>")
|
||||
async def handler2(request, t=0.3):
|
||||
await sleep(t)
|
||||
return response.text(f"Slept {t:.1f} seconds.\n")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "22.3.0.dev1"
|
||||
__version__ = "22.3.1"
|
||||
|
||||
31
sanic/app.py
31
sanic/app.py
@@ -11,7 +11,6 @@ from asyncio import (
|
||||
CancelledError,
|
||||
Task,
|
||||
ensure_future,
|
||||
get_event_loop,
|
||||
get_running_loop,
|
||||
wait_for,
|
||||
)
|
||||
@@ -142,7 +141,6 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
||||
"error_handler",
|
||||
"go_fast",
|
||||
"listeners",
|
||||
"name",
|
||||
"named_request_middleware",
|
||||
"named_response_middleware",
|
||||
"request_class",
|
||||
@@ -254,7 +252,13 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
||||
"Loop can only be retrieved after the app has started "
|
||||
"running. Not supported with `create_server` function"
|
||||
)
|
||||
return get_event_loop()
|
||||
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()
|
||||
|
||||
# -------------------------------------------------------------------- #
|
||||
# Registration
|
||||
@@ -1132,7 +1136,10 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
||||
async def _listener(
|
||||
app: Sanic, loop: AbstractEventLoop, listener: ListenerType
|
||||
):
|
||||
maybe_coro = listener(app, loop)
|
||||
try:
|
||||
maybe_coro = listener(app) # type: ignore
|
||||
except TypeError:
|
||||
maybe_coro = listener(app, loop) # type: ignore
|
||||
if maybe_coro and isawaitable(maybe_coro):
|
||||
await maybe_coro
|
||||
|
||||
@@ -1268,10 +1275,9 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
||||
...
|
||||
|
||||
def purge_tasks(self):
|
||||
for task in self.tasks:
|
||||
for key, task in self._task_registry.items():
|
||||
if task.done() or task.cancelled():
|
||||
name = task.get_name()
|
||||
self._task_registry[name] = None
|
||||
self._task_registry[key] = None
|
||||
|
||||
self._task_registry = {
|
||||
k: v for k, v in self._task_registry.items() if v is not None
|
||||
@@ -1510,7 +1516,8 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
||||
if not Sanic.test_mode:
|
||||
raise e
|
||||
|
||||
def signalize(self):
|
||||
def signalize(self, allow_fail_builtin=True):
|
||||
self.signal_router.allow_fail_builtin = allow_fail_builtin
|
||||
try:
|
||||
self.signal_router.finalize()
|
||||
except FinalizationError as e:
|
||||
@@ -1525,8 +1532,11 @@ 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.signalize(self.config.TOUCHUP)
|
||||
self.finalize()
|
||||
|
||||
# TODO: Replace in v22.6 to check against apps in app registry
|
||||
@@ -1546,7 +1556,8 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
||||
# TODO:
|
||||
# - Raise warning if secondary apps have error handler config
|
||||
ErrorHandler.finalize(self.error_handler, config=self.config)
|
||||
TouchUp.run(self)
|
||||
if self.config.TOUCHUP:
|
||||
TouchUp.run(self)
|
||||
|
||||
self.state.is_started = True
|
||||
|
||||
|
||||
@@ -68,6 +68,13 @@ 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()
|
||||
|
||||
@@ -113,6 +120,14 @@ 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]
|
||||
@@ -120,14 +135,26 @@ 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:
|
||||
app = app()
|
||||
try:
|
||||
app = app(self.args)
|
||||
except TypeError:
|
||||
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):
|
||||
|
||||
@@ -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.is_stopping:
|
||||
if app.state.is_stopping:
|
||||
return
|
||||
# Windows Python blocks signal handlers while the event loop is
|
||||
# waiting for I/O. Frequent wakeups keep interrupts flowing.
|
||||
|
||||
@@ -38,6 +38,7 @@ 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,
|
||||
@@ -81,6 +82,7 @@ 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
|
||||
|
||||
@@ -51,6 +51,10 @@ class InvalidUsage(SanicException):
|
||||
quiet = True
|
||||
|
||||
|
||||
class BadURL(InvalidUsage):
|
||||
...
|
||||
|
||||
|
||||
class MethodNotSupported(SanicException):
|
||||
"""
|
||||
**Status**: 405 Method Not Allowed
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from enum import Enum, auto
|
||||
from functools import partial
|
||||
from typing import List, Optional, Union
|
||||
from typing import Callable, List, Optional, Union, overload
|
||||
|
||||
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
|
||||
|
||||
@@ -17,6 +18,8 @@ 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):
|
||||
@@ -26,12 +29,33 @@ 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,
|
||||
) -> ListenerType[Sanic]:
|
||||
) -> Union[
|
||||
ListenerType[Sanic],
|
||||
Callable[[ListenerType[Sanic]], ListenerType[Sanic]],
|
||||
]:
|
||||
"""
|
||||
Create a listener from a decorated function.
|
||||
|
||||
@@ -49,7 +73,9 @@ class ListenerMixin(metaclass=SanicMeta):
|
||||
:param event: event to listen to
|
||||
"""
|
||||
|
||||
def register_listener(listener, event):
|
||||
def register_listener(
|
||||
listener: ListenerType[Sanic], event: str
|
||||
) -> ListenerType[Sanic]:
|
||||
nonlocal apply
|
||||
|
||||
future_listener = FutureListener(listener, event)
|
||||
@@ -59,6 +85,10 @@ 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)
|
||||
@@ -73,6 +103,16 @@ 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]:
|
||||
|
||||
@@ -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.
|
||||
Can either be called as *@app.middleware* or
|
||||
*@app.middleware('request')*
|
||||
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')*.
|
||||
|
||||
`See user guide re: middleware
|
||||
<https://sanicframework.org/guide/basics/middleware.html>`__
|
||||
@@ -47,12 +47,25 @@ 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:
|
||||
|
||||
@@ -11,6 +11,7 @@ from asyncio import (
|
||||
all_tasks,
|
||||
get_event_loop,
|
||||
get_running_loop,
|
||||
new_event_loop,
|
||||
)
|
||||
from contextlib import suppress
|
||||
from functools import partial
|
||||
@@ -32,6 +33,7 @@ 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
|
||||
@@ -538,15 +540,21 @@ 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)
|
||||
)
|
||||
return reloader_helpers.watchdog(1.0, reload_dirs)
|
||||
reloader_helpers.watchdog(1.0, reload_dirs)
|
||||
trigger_events(reloader_stop, loop, primary)
|
||||
return
|
||||
|
||||
# This exists primarily for unit testing
|
||||
if not primary.state.server_info: # no cov
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
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")
|
||||
Sanic = TypeVar("Sanic", bound="sanic.Sanic")
|
||||
|
||||
MiddlewareResponse = Union[
|
||||
Optional[HTTPResponse], Coroutine[Any, Any, Optional[HTTPResponse]]
|
||||
@@ -18,8 +20,9 @@ ErrorMiddlewareType = Callable[
|
||||
[Request, BaseException], Optional[Coroutine[Any, Any, None]]
|
||||
]
|
||||
MiddlewareType = Union[RequestMiddlewareType, ResponseMiddlewareType]
|
||||
ListenerType = Callable[
|
||||
[Sanic, AbstractEventLoop], Optional[Coroutine[Any, Any, None]]
|
||||
ListenerType = Union[
|
||||
Callable[[Sanic], Optional[Coroutine[Any, Any, None]]],
|
||||
Callable[[Sanic, AbstractEventLoop], Optional[Coroutine[Any, Any, None]]],
|
||||
]
|
||||
RouteHandler = Callable[..., Coroutine[Any, Any, Optional[HTTPResponse]]]
|
||||
SignalHandler = Callable[..., Coroutine[Any, Any, None]]
|
||||
|
||||
@@ -30,10 +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 sanic.compat import CancelledErrors, Header
|
||||
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE
|
||||
from sanic.exceptions import InvalidUsage, ServerError
|
||||
from sanic.exceptions import BadURL, InvalidUsage, ServerError
|
||||
from sanic.headers import (
|
||||
AcceptContainer,
|
||||
Options,
|
||||
@@ -129,8 +130,10 @@ 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:
|
||||
raise BadURL(f"Bad URL: {url_bytes.decode()}")
|
||||
self._id: Optional[Union[uuid.UUID, str, int]] = None
|
||||
self._name: Optional[str] = None
|
||||
self.app = app
|
||||
@@ -197,6 +200,53 @@ 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.")
|
||||
|
||||
@@ -50,6 +50,16 @@ class BaseHTTPResponse:
|
||||
The base class for all HTTP Responses
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"asgi",
|
||||
"body",
|
||||
"content_type",
|
||||
"stream",
|
||||
"status",
|
||||
"headers",
|
||||
"_cookies",
|
||||
)
|
||||
|
||||
_dumps = json_dumps
|
||||
|
||||
def __init__(self):
|
||||
@@ -156,7 +166,7 @@ class HTTPResponse(BaseHTTPResponse):
|
||||
:type content_type: Optional[str]
|
||||
"""
|
||||
|
||||
__slots__ = ("body", "status", "content_type", "headers", "_cookies")
|
||||
__slots__ = ()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from inspect import isawaitable
|
||||
from typing import Any, Callable, Iterable, Optional
|
||||
from typing import TYPE_CHECKING, Any, Callable, Iterable, Optional
|
||||
|
||||
|
||||
def trigger_events(events: Optional[Iterable[Callable[..., Any]]], loop):
|
||||
if TYPE_CHECKING: # no cov
|
||||
from sanic import Sanic
|
||||
|
||||
|
||||
def trigger_events(
|
||||
events: Optional[Iterable[Callable[..., Any]]],
|
||||
loop,
|
||||
app: Optional[Sanic] = None,
|
||||
):
|
||||
"""
|
||||
Trigger event callbacks (functions or async)
|
||||
|
||||
@@ -11,6 +21,9 @@ def trigger_events(events: Optional[Iterable[Callable[..., Any]]], loop):
|
||||
"""
|
||||
if events:
|
||||
for event in events:
|
||||
result = event(loop)
|
||||
try:
|
||||
result = event() if not app else event(app)
|
||||
except TypeError:
|
||||
result = event(loop) if not app else event(app, loop)
|
||||
if isawaitable(result):
|
||||
loop.run_until_complete(result)
|
||||
|
||||
@@ -1,26 +1,12 @@
|
||||
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.
|
||||
|
||||
@@ -5,7 +5,7 @@ from websockets.server import ServerConnection
|
||||
from websockets.typing import Subprotocol
|
||||
|
||||
from sanic.exceptions import ServerError
|
||||
from sanic.log import error_logger
|
||||
from sanic.log import 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=error_logger,
|
||||
logger=logger,
|
||||
)
|
||||
resp: "http11.Response" = ws_conn.accept(request)
|
||||
except Exception:
|
||||
|
||||
@@ -80,6 +80,7 @@ class SignalRouter(BaseRouter):
|
||||
group_class=SignalGroup,
|
||||
stacking=True,
|
||||
)
|
||||
self.allow_fail_builtin = True
|
||||
self.ctx.loop = None
|
||||
|
||||
def get( # type: ignore
|
||||
@@ -129,7 +130,8 @@ class SignalRouter(BaseRouter):
|
||||
try:
|
||||
group, handlers, params = self.get(event, condition=condition)
|
||||
except NotFound as e:
|
||||
if fail_not_found:
|
||||
is_reserved = event.split(".", 1)[0] in RESERVED_NAMESPACES
|
||||
if fail_not_found and (not is_reserved or self.allow_fail_builtin):
|
||||
raise e
|
||||
else:
|
||||
if self.ctx.app.debug and self.ctx.app.state.verbosity >= 1:
|
||||
|
||||
@@ -11,18 +11,35 @@ from sanic.helpers import import_string
|
||||
|
||||
|
||||
def str_to_bool(val: str) -> bool:
|
||||
"""
|
||||
reimplement strtobool per PEP 632 and python 3.12 deprecation
|
||||
"""Takes string and tries to turn it into bool as human would do.
|
||||
|
||||
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"]:
|
||||
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",
|
||||
}:
|
||||
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
|
||||
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(
|
||||
|
||||
28
setup.py
28
setup.py
@@ -6,6 +6,8 @@ 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
|
||||
|
||||
@@ -59,7 +61,7 @@ setup_kwargs = {
|
||||
"Build fast. Run fast."
|
||||
),
|
||||
"long_description": long_description,
|
||||
"packages": find_packages(include=[]),
|
||||
"packages": find_packages(),
|
||||
"package_data": {"sanic": ["py.typed"]},
|
||||
"platforms": "any",
|
||||
"python_requires": ">=3.7",
|
||||
@@ -82,17 +84,17 @@ ujson = "ujson>=1.35" + env_dependency
|
||||
uvloop = "uvloop>=0.5.3" + env_dependency
|
||||
types_ujson = "types-ujson" + env_dependency
|
||||
requirements = [
|
||||
"sanic-routing~=0.7",
|
||||
"sanic-routing>=22.3.0,<22.6.0",
|
||||
"httptools>=0.0.10",
|
||||
uvloop,
|
||||
ujson,
|
||||
"aiofiles>=0.6.0",
|
||||
"websockets>=10.0",
|
||||
"multidict>=5.0,<6.0",
|
||||
"multidict>=5.0,<7.0",
|
||||
]
|
||||
|
||||
tests_require = [
|
||||
"sanic-testing>=0.7.0",
|
||||
"sanic-testing>=22.3.0",
|
||||
"pytest==6.2.5",
|
||||
"coverage==5.3",
|
||||
"gunicorn==20.0.4",
|
||||
@@ -110,6 +112,7 @@ tests_require = [
|
||||
"docutils",
|
||||
"pygments",
|
||||
"uvicorn<0.15.0",
|
||||
"slotscheck>=0.8.0,<1",
|
||||
types_ujson,
|
||||
]
|
||||
|
||||
@@ -130,23 +133,6 @@ 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)
|
||||
|
||||
@@ -34,3 +34,12 @@ 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
|
||||
|
||||
@@ -39,16 +39,17 @@ def read_app_info(lines):
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"appname",
|
||||
"appname,extra",
|
||||
(
|
||||
"fake.server.app",
|
||||
"fake.server:app",
|
||||
"fake.server:create_app()",
|
||||
"fake.server.create_app()",
|
||||
("fake.server.app", None),
|
||||
("fake.server:create_app", "--factory"),
|
||||
("fake.server.create_app()", None),
|
||||
),
|
||||
)
|
||||
def test_server_run(appname):
|
||||
def test_server_run(appname, extra):
|
||||
command = ["sanic", appname]
|
||||
if extra:
|
||||
command.append(extra)
|
||||
out, err, exitcode = capture(command)
|
||||
lines = out.split(b"\n")
|
||||
firstline = lines[starting_line(lines) + 1]
|
||||
@@ -57,6 +58,49 @@ def test_server_run(appname):
|
||||
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",
|
||||
(
|
||||
|
||||
@@ -164,11 +164,12 @@ def test_raw_headers(app):
|
||||
},
|
||||
)
|
||||
|
||||
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"
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
def test_request_line(app):
|
||||
|
||||
@@ -58,6 +58,36 @@ 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(
|
||||
@@ -92,10 +122,10 @@ def write_file(filename):
|
||||
return text
|
||||
|
||||
|
||||
def scanner(proc):
|
||||
def scanner(proc, trigger="complete"):
|
||||
for line in proc.stdout:
|
||||
line = line.decode().strip()
|
||||
if line.startswith("complete"):
|
||||
if line.startswith(trigger):
|
||||
yield line
|
||||
|
||||
|
||||
@@ -108,7 +138,7 @@ argv = dict(
|
||||
"sanic",
|
||||
"--port",
|
||||
"42204",
|
||||
"--debug",
|
||||
"--auto-reload",
|
||||
"reloader.app",
|
||||
],
|
||||
)
|
||||
@@ -118,7 +148,7 @@ argv = dict(
|
||||
"runargs, mode",
|
||||
[
|
||||
(dict(port=42202, auto_reload=True), "script"),
|
||||
(dict(port=42203, debug=True), "module"),
|
||||
(dict(port=42203, auto_reload=True), "module"),
|
||||
({}, "sanic"),
|
||||
],
|
||||
)
|
||||
@@ -151,7 +181,7 @@ async def test_reloader_live(runargs, mode):
|
||||
"runargs, mode",
|
||||
[
|
||||
(dict(port=42302, auto_reload=True), "script"),
|
||||
(dict(port=42303, debug=True), "module"),
|
||||
(dict(port=42303, auto_reload=True), "module"),
|
||||
({}, "sanic"),
|
||||
],
|
||||
)
|
||||
@@ -183,3 +213,30 @@ 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)
|
||||
|
||||
@@ -4,6 +4,7 @@ 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
|
||||
|
||||
@@ -176,3 +177,17 @@ 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(),
|
||||
)
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import asyncio
|
||||
import re
|
||||
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic_routing.exceptions import (
|
||||
@@ -256,7 +254,7 @@ def test_route_strict_slash(app):
|
||||
|
||||
|
||||
def test_route_invalid_parameter_syntax(app):
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(InvalidUsage):
|
||||
|
||||
@app.get("/get/<:str>", strict_slashes=True)
|
||||
def handler(request):
|
||||
|
||||
@@ -33,6 +33,14 @@ 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()
|
||||
@@ -56,6 +64,17 @@ 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):
|
||||
@@ -199,3 +218,16 @@ 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
|
||||
|
||||
@@ -3,6 +3,7 @@ import os
|
||||
import signal
|
||||
|
||||
from queue import Queue
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
@@ -10,6 +11,7 @@ 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
|
||||
|
||||
|
||||
@@ -73,11 +75,12 @@ def test_windows_workaround():
|
||||
# Windows...
|
||||
class MockApp:
|
||||
def __init__(self):
|
||||
self.is_stopping = False
|
||||
self.state = SimpleNamespace()
|
||||
self.state.is_stopping = False
|
||||
|
||||
def stop(self):
|
||||
assert not self.is_stopping
|
||||
self.is_stopping = True
|
||||
assert not self.state.is_stopping
|
||||
self.state.is_stopping = True
|
||||
|
||||
def add_task(self, func):
|
||||
loop = asyncio.get_event_loop()
|
||||
@@ -90,11 +93,11 @@ def test_windows_workaround():
|
||||
if stop_first:
|
||||
app.stop()
|
||||
await asyncio.sleep(0.2)
|
||||
assert app.is_stopping == stop_first
|
||||
assert app.state.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.is_stopping
|
||||
assert app.state.is_stopping
|
||||
assert app.stay_active_task.result() is None
|
||||
# Second Ctrl+C should raise
|
||||
with pytest.raises(KeyboardInterrupt):
|
||||
@@ -108,3 +111,17 @@ 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)
|
||||
|
||||
@@ -80,6 +80,18 @@ 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()
|
||||
|
||||
@@ -2,6 +2,8 @@ import logging
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic_routing.exceptions import NotFound
|
||||
|
||||
from sanic.signals import RESERVED_NAMESPACES
|
||||
from sanic.touchup import TouchUp
|
||||
|
||||
@@ -28,3 +30,50 @@ 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
|
||||
|
||||
@@ -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.content
|
||||
assert r.status_code == 200, r.text
|
||||
assert r.text == "Slept 0.1 seconds.\n"
|
||||
|
||||
def spawn():
|
||||
|
||||
@@ -6,7 +6,6 @@ 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(
|
||||
@@ -49,20 +48,3 @@ 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