Compare commits

..

18 Commits

Author SHA1 Message Date
Adam Hopkins
cc97287f8e Add fall back for Windows even loop fetching (#2421) 2022-04-17 12:25:41 +03:00
Adam Hopkins
00218aa9f2 22.3 Internal version bumps (#2419) 2022-03-31 14:30:30 +03:00
Adam Hopkins
874718db94 Bump version and 22.3 changelog (#2418) 2022-03-30 15:09:45 +03:00
Javier Marcet
bb4474897f Fix "DeprecationWarning: There is no current event loop" (#2390)
Co-authored-by: Adam Hopkins <adam@amhopkins.com>
2022-03-30 09:40:30 +03:00
Adam Hopkins
0cb342aef4 Better exception for bad URL parse (#2415) 2022-03-25 00:22:12 +02:00
Ashley Sommer
030987480c Add config option to skip Touchup step, for debugging purposes (#2361)
Co-authored-by: Adam Hopkins <adam@amhopkins.com>
2022-03-24 13:52:05 +02:00
Robert Schütz
f6fdc80b40 allow multidict version 6 (#2396)
Co-authored-by: Adam Hopkins <adam@amhopkins.com>
2022-03-24 00:38:45 +02:00
Jonathan Vargas
361c242473 remove error_logger on websockets (#2373)
Co-authored-by: Adam Hopkins <adam@amhopkins.com>
2022-03-23 16:25:19 +02:00
André Ericson
32962d1e1c Fixing typing for ListenerMixin.listener (#2376)
Co-authored-by: Adam Hopkins <adam@amhopkins.com>
2022-03-23 15:34:33 +02:00
Adam Hopkins
6e0a6871b5 Upgrade tests for sanic-routing changes (#2405) 2022-03-23 13:43:36 +02:00
Adam Hopkins
0030425c8c Conditionally inject CLI arguments into factory (#2402) 2022-03-23 12:00:41 +02:00
Adam Hopkins
c9dbc8ed26 Remove loop as required listener arg (#2414) 2022-03-23 11:02:39 +02:00
Callum
44b108b564 Changes to CLI (#2401)
Co-authored-by: Callum Fleming <howzitcal@zohomail.com>
Co-authored-by: Adam Hopkins <adam@amhopkins.com>
2022-03-23 10:30:41 +02:00
Adam Hopkins
2a8e91052f Add two new events on the reloader process (#2413) 2022-03-22 23:29:39 +02:00
Bluenix
0c9df02e66 Add a docstring to Request.respond() (#2409)
Co-authored-by: Ryu juheon <saidbysolo@gmail.com>
Co-authored-by: Adam Hopkins <adam@amhopkins.com>
2022-03-14 13:10:49 +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
36 changed files with 575 additions and 125 deletions

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

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

View 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`

View File

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

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

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

View File

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

View File

@@ -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" legacy_version = len(sys.argv) == 2 and sys.argv[-1] == "-v"
parse_args = ["--version"] if legacy_version else None 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.args = self.parser.parse_args(args=parse_args)
self._precheck() 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 "." delimiter = ":" if ":" in self.args.module else "."
module_name, app_name = self.args.module.rsplit(delimiter, 1) 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("()"): if app_name.endswith("()"):
self.args.factory = True self.args.factory = True
app_name = app_name[:-2] 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) module = import_module(module_name)
app = getattr(module, app_name, None) app = getattr(module, app_name, None)
if self.args.factory: if self.args.factory:
try:
app = app(self.args)
except TypeError:
app = app() app = app()
app_type_name = type(app).__name__ app_type_name = type(app).__name__
if not isinstance(app, Sanic): 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( raise ValueError(
f"Module is not a Sanic app, it is a {app_type_name}\n" 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: except ImportError as e:
if module_name.startswith(e.name): 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""" """Asyncio wakeups to allow receiving SIGINT in Python"""
while not die: while not die:
# If someone else stopped the app, just exit # If someone else stopped the app, just exit
if app.is_stopping: if app.state.is_stopping:
return return
# Windows Python blocks signal handlers while the event loop is # Windows Python blocks signal handlers while the event loop is
# waiting for I/O. Frequent wakeups keep interrupts flowing. # waiting for I/O. Frequent wakeups keep interrupts flowing.

View File

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

View File

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

View File

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

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

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

View File

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

View File

@@ -30,10 +30,11 @@ from types import SimpleNamespace
from urllib.parse import parse_qs, parse_qsl, unquote, urlunparse from urllib.parse import parse_qs, parse_qsl, unquote, urlunparse
from httptools import parse_url # type: ignore from httptools import parse_url # type: ignore
from httptools.parser.errors import HttpParserInvalidURLError # type: ignore
from sanic.compat import CancelledErrors, Header from sanic.compat import CancelledErrors, Header
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE 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 ( from sanic.headers import (
AcceptContainer, AcceptContainer,
Options, Options,
@@ -129,8 +130,10 @@ class Request:
): ):
self.raw_url = url_bytes self.raw_url = url_bytes
# TODO: Content-Encoding detection try:
self._parsed_url = parse_url(url_bytes) 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._id: Optional[Union[uuid.UUID, str, int]] = None
self._name: Optional[str] = None self._name: Optional[str] = None
self.app = app self.app = app
@@ -197,6 +200,53 @@ class Request:
headers: Optional[Union[Header, Dict[str, str]]] = None, headers: Optional[Union[Header, Dict[str, str]]] = None,
content_type: Optional[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: try:
if self.stream is not None and self.stream.response: if self.stream is not None and self.stream.response:
raise ServerError("Second respond call is not allowed.") raise ServerError("Second respond call is not allowed.")

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,8 +1,18 @@
from __future__ import annotations
from inspect import isawaitable 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) Trigger event callbacks (functions or async)
@@ -11,6 +21,9 @@ def trigger_events(events: Optional[Iterable[Callable[..., Any]]], loop):
""" """
if events: if events:
for event in 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): if isawaitable(result):
loop.run_until_complete(result) loop.run_until_complete(result)

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

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

View File

@@ -80,6 +80,7 @@ class SignalRouter(BaseRouter):
group_class=SignalGroup, group_class=SignalGroup,
stacking=True, stacking=True,
) )
self.allow_fail_builtin = True
self.ctx.loop = None self.ctx.loop = None
def get( # type: ignore def get( # type: ignore
@@ -129,7 +130,8 @@ class SignalRouter(BaseRouter):
try: try:
group, handlers, params = self.get(event, condition=condition) group, handlers, params = self.get(event, condition=condition)
except NotFound as e: 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 raise e
else: else:
if self.ctx.app.debug and self.ctx.app.state.verbosity >= 1: if self.ctx.app.debug and self.ctx.app.state.verbosity >= 1:

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

@@ -34,3 +34,12 @@ async def shutdown(app: Sanic, _):
def create_app(): def create_app():
return 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

@@ -39,16 +39,17 @@ def read_app_info(lines):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"appname", "appname,extra",
( (
"fake.server.app", ("fake.server.app", None),
"fake.server:app", ("fake.server:create_app", "--factory"),
"fake.server:create_app()", ("fake.server.create_app()", None),
"fake.server.create_app()",
), ),
) )
def test_server_run(appname): def test_server_run(appname, extra):
command = ["sanic", appname] command = ["sanic", appname]
if extra:
command.append(extra)
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]
@@ -57,6 +58,49 @@ def test_server_run(appname):
assert firstline == b"Goin' Fast @ http://127.0.0.1:8000" 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( @pytest.mark.parametrize(
"cmd", "cmd",
( (

View File

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

View File

@@ -58,6 +58,36 @@ def write_app(filename, **runargs):
return text 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): def write_json_config_app(filename, jsonfile, **runargs):
with open(filename, "w") as f: with open(filename, "w") as f:
f.write( f.write(
@@ -92,10 +122,10 @@ def write_file(filename):
return text return text
def scanner(proc): def scanner(proc, trigger="complete"):
for line in proc.stdout: for line in proc.stdout:
line = line.decode().strip() line = line.decode().strip()
if line.startswith("complete"): if line.startswith(trigger):
yield line yield line
@@ -108,7 +138,7 @@ argv = dict(
"sanic", "sanic",
"--port", "--port",
"42204", "42204",
"--debug", "--auto-reload",
"reloader.app", "reloader.app",
], ],
) )
@@ -118,7 +148,7 @@ argv = dict(
"runargs, mode", "runargs, mode",
[ [
(dict(port=42202, auto_reload=True), "script"), (dict(port=42202, auto_reload=True), "script"),
(dict(port=42203, debug=True), "module"), (dict(port=42203, auto_reload=True), "module"),
({}, "sanic"), ({}, "sanic"),
], ],
) )
@@ -151,7 +181,7 @@ async def test_reloader_live(runargs, mode):
"runargs, mode", "runargs, mode",
[ [
(dict(port=42302, auto_reload=True), "script"), (dict(port=42302, auto_reload=True), "script"),
(dict(port=42303, debug=True), "module"), (dict(port=42303, auto_reload=True), "module"),
({}, "sanic"), ({}, "sanic"),
], ],
) )
@@ -183,3 +213,30 @@ async def test_reloader_live_with_dir(runargs, mode):
terminate(proc) terminate(proc)
with suppress(TimeoutExpired): with suppress(TimeoutExpired):
proc.wait(timeout=3) 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,6 +4,7 @@ from uuid import UUID, uuid4
import pytest import pytest
from sanic import Sanic, response from sanic import Sanic, response
from sanic.exceptions import BadURL
from sanic.request import Request, uuid from sanic.request import Request, uuid
from sanic.server import HttpProtocol from sanic.server import HttpProtocol
@@ -176,3 +177,17 @@ def test_request_accept():
"text/x-dvi; q=0.8", "text/x-dvi; q=0.8",
"text/plain; q=0.5", "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,8 +1,6 @@
import asyncio import asyncio
import re import re
from unittest.mock import Mock
import pytest import pytest
from sanic_routing.exceptions import ( from sanic_routing.exceptions import (
@@ -256,7 +254,7 @@ def test_route_strict_slash(app):
def test_route_invalid_parameter_syntax(app): def test_route_invalid_parameter_syntax(app):
with pytest.raises(ValueError): with pytest.raises(InvalidUsage):
@app.get("/get/<:str>", strict_slashes=True) @app.get("/get/<:str>", strict_slashes=True)
def handler(request): def handler(request):

View File

@@ -33,6 +33,14 @@ def create_listener(listener_name, in_list):
return _listener 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 start_stop_app(random_name_app, **run_kwargs):
def stop_on_alarm(signum, frame): def stop_on_alarm(signum, frame):
random_name_app.stop() random_name_app.stop()
@@ -56,6 +64,17 @@ def test_single_listener(app, listener_name):
assert app.name + listener_name == output.pop() 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 @skipif_no_alarm
@pytest.mark.parametrize("listener_name", AVAILABLE_LISTENERS) @pytest.mark.parametrize("listener_name", AVAILABLE_LISTENERS)
def test_register_listener(app, listener_name): def test_register_listener(app, listener_name):
@@ -199,3 +218,16 @@ async def test_missing_startup_raises_exception(app):
with pytest.raises(SanicException): with pytest.raises(SanicException):
await srv.before_start() 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,6 +3,7 @@ import os
import signal import signal
from queue import Queue from queue import Queue
from types import SimpleNamespace
from unittest.mock import MagicMock from unittest.mock import MagicMock
import pytest import pytest
@@ -10,6 +11,7 @@ import pytest
from sanic_testing.testing import HOST, PORT from sanic_testing.testing import HOST, PORT
from sanic.compat import ctrlc_workaround_for_windows from sanic.compat import ctrlc_workaround_for_windows
from sanic.exceptions import InvalidUsage
from sanic.response import HTTPResponse from sanic.response import HTTPResponse
@@ -73,11 +75,12 @@ def test_windows_workaround():
# Windows... # Windows...
class MockApp: class MockApp:
def __init__(self): def __init__(self):
self.is_stopping = False self.state = SimpleNamespace()
self.state.is_stopping = False
def stop(self): def stop(self):
assert not self.is_stopping assert not self.state.is_stopping
self.is_stopping = True self.state.is_stopping = True
def add_task(self, func): def add_task(self, func):
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
@@ -90,11 +93,11 @@ def test_windows_workaround():
if stop_first: if stop_first:
app.stop() app.stop()
await asyncio.sleep(0.2) 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 # First Ctrl+C: should call app.stop() within 0.1 seconds
os.kill(os.getpid(), signal.SIGINT) os.kill(os.getpid(), signal.SIGINT)
await asyncio.sleep(0.2) await asyncio.sleep(0.2)
assert app.is_stopping assert app.state.is_stopping
assert app.stay_active_task.result() is None assert app.stay_active_task.result() is None
# Second Ctrl+C should raise # Second Ctrl+C should raise
with pytest.raises(KeyboardInterrupt): with pytest.raises(KeyboardInterrupt):
@@ -108,3 +111,17 @@ def test_windows_workaround():
assert res == "OK" assert res == "OK"
res = loop.run_until_complete(atest(True)) res = loop.run_until_complete(atest(True))
assert res == "OK" 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,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

@@ -2,6 +2,8 @@ import logging
import pytest import pytest
from sanic_routing.exceptions import NotFound
from sanic.signals import RESERVED_NAMESPACES from sanic.signals import RESERVED_NAMESPACES
from sanic.touchup import TouchUp from sanic.touchup import TouchUp
@@ -28,3 +30,50 @@ async def test_ode_removes_dispatch_events(app, caplog, verbosity, result):
) )
in logs in logs
) is result ) 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): for _ in range(40):
async with httpx.AsyncClient(transport=transport) as client: async with httpx.AsyncClient(transport=transport) as client:
r = await client.get("http://localhost/sleep/0.1") 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" assert r.text == "Slept 0.1 seconds.\n"
def spawn(): def spawn():

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 =