Compare commits
1 Commits
v22.3.0
...
flaky-test
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a61c5ff55b |
1
.github/workflows/pr-python310.yml
vendored
1
.github/workflows/pr-python310.yml
vendored
@@ -8,7 +8,6 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
testPy310:
|
testPy310:
|
||||||
if: github.event.pull_request.draft == false
|
|
||||||
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
|
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
📜 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
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
## Version 22.3.0
|
|
||||||
|
|
||||||
### Features
|
|
||||||
- [#2347](https://github.com/sanic-org/sanic/pull/2347) API for multi-application server
|
|
||||||
- 🚨 *BREAKING CHANGE*: The old `sanic.worker.GunicornWorker` has been **removed**. To run Sanic with `gunicorn`, you should use it thru `uvicorn` [as described in their docs](https://www.uvicorn.org/#running-with-gunicorn).
|
|
||||||
- 🧁 *SIDE EFFECT*: Named background tasks are now supported, even in Python 3.7
|
|
||||||
- [#2357](https://github.com/sanic-org/sanic/pull/2357) Parse `Authorization` header as `Request.credentials`
|
|
||||||
- [#2361](https://github.com/sanic-org/sanic/pull/2361) Add config option to skip `Touchup` step in application startup
|
|
||||||
- [#2372](https://github.com/sanic-org/sanic/pull/2372) Updates to CLI help messaging
|
|
||||||
- [#2382](https://github.com/sanic-org/sanic/pull/2382) Downgrade warnings to backwater debug messages
|
|
||||||
- [#2396](https://github.com/sanic-org/sanic/pull/2396) Allow for `multidict` v0.6
|
|
||||||
- [#2401](https://github.com/sanic-org/sanic/pull/2401) Upgrade CLI catching for alternative application run types
|
|
||||||
- [#2402](https://github.com/sanic-org/sanic/pull/2402) Conditionally inject CLI arguments into factory
|
|
||||||
- [#2413](https://github.com/sanic-org/sanic/pull/2413) Add new start and stop event listeners to reloader process
|
|
||||||
- [#2414](https://github.com/sanic-org/sanic/pull/2414) Remove loop as required listener arg
|
|
||||||
- [#2415](https://github.com/sanic-org/sanic/pull/2415) Better exception for bad URL parsing
|
|
||||||
- [sanic-routing#47](https://github.com/sanic-org/sanic-routing/pull/47) Add a new extention parameter type: `<file:ext>`, `<file:ext=jpg>`, `<file:ext=jpg|png|gif|svg>`, `<file=int:ext>`, `<file=int:ext=jpg|png|gif|svg>`, `<file=float:ext=tar.gz>`
|
|
||||||
- 👶 *BETA FEATURE*: This feature will not work with `path` type matching, and is being released as a beta feature only.
|
|
||||||
- [sanic-routing#57](https://github.com/sanic-org/sanic-routing/pull/57) Change `register_pattern` to accept a `str` or `Pattern`
|
|
||||||
- [sanic-routing#58](https://github.com/sanic-org/sanic-routing/pull/58) Default matching on non-empty strings only, and new `strorempty` pattern type
|
|
||||||
- 🚨 *BREAKING CHANGE*: Previously a route with a dynamic string parameter (`/<foo>` or `/<foo:str>`) would match on any string, including empty strings. It will now **only** match a non-empty string. To retain the old behavior, you should use the new parameter type: `/<foo:strorempty>`.
|
|
||||||
|
|
||||||
### Bugfixes
|
|
||||||
- [#2373](https://github.com/sanic-org/sanic/pull/2373) Remove `error_logger` on websockets
|
|
||||||
- [#2381](https://github.com/sanic-org/sanic/pull/2381) Fix newly assigned `None` in task registry
|
|
||||||
- [sanic-routing#52](https://github.com/sanic-org/sanic-routing/pull/52) Add type casting to regex route matching
|
|
||||||
- [sanic-routing#60](https://github.com/sanic-org/sanic-routing/pull/60) Add requirements check on regex routes (this resolves, for example, multiple static directories with differing `host` values)
|
|
||||||
|
|
||||||
### Deprecations and Removals
|
|
||||||
- [#2362](https://github.com/sanic-org/sanic/pull/2362) 22.3 Deprecations and changes
|
|
||||||
1. `debug=True` and `--debug` do _NOT_ automatically run `auto_reload`
|
|
||||||
2. Default error render is with plain text (browsers still get HTML by default because `auto` looks at headers)
|
|
||||||
3. `config` is required for `ErrorHandler.finalize`
|
|
||||||
4. `ErrorHandler.lookup` requires two positional args
|
|
||||||
5. Unused websocket protocol args removed
|
|
||||||
- [#2344](https://github.com/sanic-org/sanic/pull/2344) Deprecate loading of lowercase environment variables
|
|
||||||
|
|
||||||
### Developer infrastructure
|
|
||||||
- [#2363](https://github.com/sanic-org/sanic/pull/2363) Revert code coverage back to Codecov
|
|
||||||
- [#2405](https://github.com/sanic-org/sanic/pull/2405) Upgrade tests for `sanic-routing` changes
|
|
||||||
- [sanic-testing#35](https://github.com/sanic-org/sanic-testing/pull/35) Allow for httpx v0.22
|
|
||||||
|
|
||||||
### Improved Documentation
|
|
||||||
- [#2350](https://github.com/sanic-org/sanic/pull/2350) Fix link in README for ASGI
|
|
||||||
- [#2398](https://github.com/sanic-org/sanic/pull/2398) Document middleware on_request and on_response
|
|
||||||
- [#2409](https://github.com/sanic-org/sanic/pull/2409) Add missing documentation for `Request.respond`
|
|
||||||
|
|
||||||
### Miscellaneous
|
|
||||||
- [#2376](https://github.com/sanic-org/sanic/pull/2376) Fix typing for `ListenerMixin.listener`
|
|
||||||
- [#2383](https://github.com/sanic-org/sanic/pull/2383) Clear deprecation warning in `asyncio.wait`
|
|
||||||
- [#2387](https://github.com/sanic-org/sanic/pull/2387) Cleanup `__slots__` implementations
|
|
||||||
- [#2390](https://github.com/sanic-org/sanic/pull/2390) Clear deprecation warning in `asyncio.get_event_loop`
|
|
||||||
@@ -4,7 +4,6 @@ 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("/")
|
||||||
@@ -12,7 +11,7 @@ async def handler(request):
|
|||||||
return response.redirect("/sleep/3")
|
return response.redirect("/sleep/3")
|
||||||
|
|
||||||
|
|
||||||
@app.get("/sleep/<t:float>")
|
@app.get("/sleep/<t:number>")
|
||||||
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")
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = "22.3.0"
|
__version__ = "22.3.0.dev1"
|
||||||
|
|||||||
26
sanic/app.py
26
sanic/app.py
@@ -11,6 +11,7 @@ from asyncio import (
|
|||||||
CancelledError,
|
CancelledError,
|
||||||
Task,
|
Task,
|
||||||
ensure_future,
|
ensure_future,
|
||||||
|
get_event_loop,
|
||||||
get_running_loop,
|
get_running_loop,
|
||||||
wait_for,
|
wait_for,
|
||||||
)
|
)
|
||||||
@@ -43,11 +44,8 @@ from typing import (
|
|||||||
from urllib.parse import urlencode, urlunparse
|
from urllib.parse import urlencode, urlunparse
|
||||||
from warnings import filterwarnings
|
from warnings import filterwarnings
|
||||||
|
|
||||||
from sanic_routing.exceptions import ( # type: ignore
|
from sanic_routing.exceptions import FinalizationError, NotFound
|
||||||
FinalizationError,
|
from sanic_routing.route import Route
|
||||||
NotFound,
|
|
||||||
)
|
|
||||||
from sanic_routing.route import Route # type: ignore
|
|
||||||
|
|
||||||
from sanic.application.ext import setup_ext
|
from sanic.application.ext import setup_ext
|
||||||
from sanic.application.state import ApplicationState, Mode, ServerStage
|
from sanic.application.state import ApplicationState, Mode, ServerStage
|
||||||
@@ -252,7 +250,7 @@ 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_running_loop()
|
return get_event_loop()
|
||||||
|
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
# Registration
|
# Registration
|
||||||
@@ -1130,10 +1128,7 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|||||||
async def _listener(
|
async def _listener(
|
||||||
app: Sanic, loop: AbstractEventLoop, listener: ListenerType
|
app: Sanic, loop: AbstractEventLoop, listener: ListenerType
|
||||||
):
|
):
|
||||||
try:
|
maybe_coro = listener(app, loop)
|
||||||
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
|
||||||
|
|
||||||
@@ -1510,8 +1505,7 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|||||||
if not Sanic.test_mode:
|
if not Sanic.test_mode:
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
def signalize(self, allow_fail_builtin=True):
|
def signalize(self):
|
||||||
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:
|
||||||
@@ -1526,11 +1520,8 @@ 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.config.TOUCHUP)
|
self.signalize()
|
||||||
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
|
||||||
@@ -1550,8 +1541,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
|
||||||
|
|
||||||
|
|||||||
@@ -68,13 +68,6 @@ Or, a path to a directory to run as a simple HTTP server:
|
|||||||
legacy_version = len(sys.argv) == 2 and sys.argv[-1] == "-v"
|
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()
|
||||||
|
|
||||||
@@ -120,14 +113,6 @@ 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]
|
||||||
@@ -135,26 +120,14 @@ 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()
|
||||||
app = app(self.args)
|
|
||||||
except TypeError:
|
|
||||||
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):
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ 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,
|
||||||
@@ -82,7 +81,6 @@ 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
|
||||||
|
|||||||
@@ -51,10 +51,6 @@ 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
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from typing import Callable, List, Optional, Union, overload
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
from sanic.base.meta import SanicMeta
|
from sanic.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
|
||||||
|
|
||||||
@@ -18,8 +17,6 @@ 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):
|
||||||
@@ -29,33 +26,12 @@ 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,
|
||||||
) -> Union[
|
) -> ListenerType[Sanic]:
|
||||||
ListenerType[Sanic],
|
|
||||||
Callable[[ListenerType[Sanic]], ListenerType[Sanic]],
|
|
||||||
]:
|
|
||||||
"""
|
"""
|
||||||
Create a listener from a decorated function.
|
Create a listener from a decorated function.
|
||||||
|
|
||||||
@@ -73,9 +49,7 @@ class ListenerMixin(metaclass=SanicMeta):
|
|||||||
:param event: event to listen to
|
:param event: event to listen to
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def register_listener(
|
def register_listener(listener, event):
|
||||||
listener: ListenerType[Sanic], event: str
|
|
||||||
) -> ListenerType[Sanic]:
|
|
||||||
nonlocal apply
|
nonlocal apply
|
||||||
|
|
||||||
future_listener = FutureListener(listener, event)
|
future_listener = FutureListener(listener, event)
|
||||||
@@ -85,10 +59,6 @@ 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)
|
||||||
@@ -103,16 +73,6 @@ 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]:
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ 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
|
||||||
@@ -33,7 +32,6 @@ 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
|
||||||
@@ -540,21 +538,15 @@ 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)
|
||||||
)
|
)
|
||||||
reloader_helpers.watchdog(1.0, reload_dirs)
|
return 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
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
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", bound="sanic.Sanic")
|
Sanic = TypeVar("Sanic")
|
||||||
|
|
||||||
MiddlewareResponse = Union[
|
MiddlewareResponse = Union[
|
||||||
Optional[HTTPResponse], Coroutine[Any, Any, Optional[HTTPResponse]]
|
Optional[HTTPResponse], Coroutine[Any, Any, Optional[HTTPResponse]]
|
||||||
@@ -20,9 +18,8 @@ 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 = Union[
|
ListenerType = Callable[
|
||||||
Callable[[Sanic], Optional[Coroutine[Any, Any, None]]],
|
[Sanic, AbstractEventLoop], 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]]
|
||||||
|
|||||||
@@ -30,11 +30,10 @@ 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 BadURL, InvalidUsage, ServerError
|
from sanic.exceptions import InvalidUsage, ServerError
|
||||||
from sanic.headers import (
|
from sanic.headers import (
|
||||||
AcceptContainer,
|
AcceptContainer,
|
||||||
Options,
|
Options,
|
||||||
@@ -130,10 +129,8 @@ class Request:
|
|||||||
):
|
):
|
||||||
|
|
||||||
self.raw_url = url_bytes
|
self.raw_url = url_bytes
|
||||||
try:
|
# TODO: Content-Encoding detection
|
||||||
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
|
||||||
@@ -200,53 +197,6 @@ 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.")
|
||||||
|
|||||||
@@ -1,18 +1,8 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from inspect import isawaitable
|
from inspect import isawaitable
|
||||||
from typing import TYPE_CHECKING, Any, Callable, Iterable, Optional
|
from typing import Any, Callable, Iterable, Optional
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING: # no cov
|
def trigger_events(events: Optional[Iterable[Callable[..., Any]]], loop):
|
||||||
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)
|
||||||
|
|
||||||
@@ -21,9 +11,6 @@ def trigger_events(
|
|||||||
"""
|
"""
|
||||||
if events:
|
if events:
|
||||||
for event in events:
|
for event in events:
|
||||||
try:
|
result = event(loop)
|
||||||
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)
|
||||||
|
|||||||
@@ -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 logger
|
from sanic.log import error_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=logger,
|
logger=error_logger,
|
||||||
)
|
)
|
||||||
resp: "http11.Response" = ws_conn.accept(request)
|
resp: "http11.Response" = ws_conn.accept(request)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -80,7 +80,6 @@ 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
|
||||||
@@ -130,8 +129,7 @@ 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:
|
||||||
is_reserved = event.split(".", 1)[0] in RESERVED_NAMESPACES
|
if fail_not_found:
|
||||||
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:
|
||||||
|
|||||||
6
setup.py
6
setup.py
@@ -84,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>=22.3.0,<22.6.0",
|
"sanic-routing~=0.7",
|
||||||
"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,<7.0",
|
"multidict>=5.0,<6.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
tests_require = [
|
tests_require = [
|
||||||
"sanic-testing>=22.3.0",
|
"sanic-testing>=0.7.0",
|
||||||
"pytest==6.2.5",
|
"pytest==6.2.5",
|
||||||
"coverage==5.3",
|
"coverage==5.3",
|
||||||
"gunicorn==20.0.4",
|
"gunicorn==20.0.4",
|
||||||
|
|||||||
@@ -34,12 +34,3 @@ 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
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from pyparsing import line
|
||||||
from sanic_routing import __version__ as __routing_version__
|
from sanic_routing import __version__ as __routing_version__
|
||||||
|
|
||||||
from sanic import __version__
|
from sanic import __version__
|
||||||
@@ -39,68 +40,26 @@ def read_app_info(lines):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"appname,extra",
|
"appname",
|
||||||
(
|
(
|
||||||
("fake.server.app", None),
|
"fake.server.app",
|
||||||
("fake.server:create_app", "--factory"),
|
"fake.server:app",
|
||||||
("fake.server.create_app()", None),
|
"fake.server:create_app()",
|
||||||
|
"fake.server.create_app()",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_server_run(appname, extra):
|
def test_server_run(appname):
|
||||||
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]
|
||||||
|
error_message = f"Lines found: {lines}\nErr output: {err}"
|
||||||
|
|
||||||
assert exitcode != 1
|
assert exitcode != 1
|
||||||
|
assert lines, error_message
|
||||||
assert firstline == b"Goin' Fast @ http://127.0.0.1:8000"
|
assert firstline == b"Goin' Fast @ http://127.0.0.1:8000"
|
||||||
|
|
||||||
|
|
||||||
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",
|
||||||
(
|
(
|
||||||
@@ -124,6 +83,9 @@ def test_tls_options(cmd):
|
|||||||
out, err, exitcode = capture(command)
|
out, err, exitcode = capture(command)
|
||||||
assert exitcode != 1
|
assert exitcode != 1
|
||||||
lines = out.split(b"\n")
|
lines = out.split(b"\n")
|
||||||
|
error_message = f"Lines found: {lines}\nErr output: {err}"
|
||||||
|
|
||||||
|
assert lines, error_message
|
||||||
firstline = lines[starting_line(lines) + 1]
|
firstline = lines[starting_line(lines) + 1]
|
||||||
assert firstline == b"Goin' Fast @ https://127.0.0.1:9999"
|
assert firstline == b"Goin' Fast @ https://127.0.0.1:9999"
|
||||||
|
|
||||||
@@ -146,7 +108,9 @@ def test_tls_wrong_options(cmd):
|
|||||||
assert exitcode == 1
|
assert exitcode == 1
|
||||||
assert not out
|
assert not out
|
||||||
lines = err.decode().split("\n")
|
lines = err.decode().split("\n")
|
||||||
|
error_message = f"Lines found: {lines}\nErr output: {err}"
|
||||||
|
|
||||||
|
assert lines, error_message
|
||||||
errmsg = lines[6]
|
errmsg = lines[6]
|
||||||
assert errmsg == "TLS certificates must be specified by either of:"
|
assert errmsg == "TLS certificates must be specified by either of:"
|
||||||
|
|
||||||
@@ -163,9 +127,11 @@ def test_host_port_localhost(cmd):
|
|||||||
out, err, exitcode = capture(command)
|
out, err, exitcode = capture(command)
|
||||||
lines = out.split(b"\n")
|
lines = out.split(b"\n")
|
||||||
expected = b"Goin' Fast @ http://localhost:9999"
|
expected = b"Goin' Fast @ http://localhost:9999"
|
||||||
|
error_message = f"Lines found: {lines}\nErr output: {err}"
|
||||||
|
|
||||||
assert exitcode != 1
|
assert exitcode != 1
|
||||||
assert expected in lines, f"Lines found: {lines}\nErr output: {err}"
|
assert lines, error_message
|
||||||
|
assert expected in lines, error_message
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@@ -180,9 +146,11 @@ def test_host_port_ipv4(cmd):
|
|||||||
out, err, exitcode = capture(command)
|
out, err, exitcode = capture(command)
|
||||||
lines = out.split(b"\n")
|
lines = out.split(b"\n")
|
||||||
expected = b"Goin' Fast @ http://127.0.0.127:9999"
|
expected = b"Goin' Fast @ http://127.0.0.127:9999"
|
||||||
|
error_message = f"Lines found: {lines}\nErr output: {err}"
|
||||||
|
|
||||||
assert exitcode != 1
|
assert exitcode != 1
|
||||||
assert expected in lines, f"Lines found: {lines}\nErr output: {err}"
|
assert lines, error_message
|
||||||
|
assert expected in lines, error_message
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@@ -197,9 +165,11 @@ def test_host_port_ipv6_any(cmd):
|
|||||||
out, err, exitcode = capture(command)
|
out, err, exitcode = capture(command)
|
||||||
lines = out.split(b"\n")
|
lines = out.split(b"\n")
|
||||||
expected = b"Goin' Fast @ http://[::]:9999"
|
expected = b"Goin' Fast @ http://[::]:9999"
|
||||||
|
error_message = f"Lines found: {lines}\nErr output: {err}"
|
||||||
|
|
||||||
assert exitcode != 1
|
assert exitcode != 1
|
||||||
assert expected in lines, f"Lines found: {lines}\nErr output: {err}"
|
assert lines, error_message
|
||||||
|
assert expected in lines, error_message
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@@ -214,9 +184,11 @@ def test_host_port_ipv6_loopback(cmd):
|
|||||||
out, err, exitcode = capture(command)
|
out, err, exitcode = capture(command)
|
||||||
lines = out.split(b"\n")
|
lines = out.split(b"\n")
|
||||||
expected = b"Goin' Fast @ http://[::1]:9999"
|
expected = b"Goin' Fast @ http://[::1]:9999"
|
||||||
|
error_message = f"Lines found: {lines}\nErr output: {err}"
|
||||||
|
|
||||||
assert exitcode != 1
|
assert exitcode != 1
|
||||||
assert expected in lines, f"Lines found: {lines}\nErr output: {err}"
|
assert lines, error_message
|
||||||
|
assert expected in lines, error_message
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@@ -250,12 +222,12 @@ def test_debug(cmd):
|
|||||||
out, err, exitcode = capture(command)
|
out, err, exitcode = capture(command)
|
||||||
lines = out.split(b"\n")
|
lines = out.split(b"\n")
|
||||||
info = read_app_info(lines)
|
info = read_app_info(lines)
|
||||||
|
error_message = f"Lines found: {lines}\nErr output: {err}"
|
||||||
|
|
||||||
assert info["debug"] is True, f"Lines found: {lines}\nErr output: {err}"
|
assert info, error_message
|
||||||
assert (
|
assert info["debug"] is True, error_message
|
||||||
info["auto_reload"] is False
|
assert info["auto_reload"] is False, error_message
|
||||||
), f"Lines found: {lines}\nErr output: {err}"
|
assert "dev" not in info, error_message
|
||||||
assert "dev" not in info, f"Lines found: {lines}\nErr output: {err}"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("cmd", ("--dev", "-d"))
|
@pytest.mark.parametrize("cmd", ("--dev", "-d"))
|
||||||
@@ -264,11 +236,11 @@ def test_dev(cmd):
|
|||||||
out, err, exitcode = capture(command)
|
out, err, exitcode = capture(command)
|
||||||
lines = out.split(b"\n")
|
lines = out.split(b"\n")
|
||||||
info = read_app_info(lines)
|
info = read_app_info(lines)
|
||||||
|
error_message = f"Lines found: {lines}\nErr output: {err}"
|
||||||
|
|
||||||
assert info["debug"] is True, f"Lines found: {lines}\nErr output: {err}"
|
assert info, error_message
|
||||||
assert (
|
assert info["debug"] is True, error_message
|
||||||
info["auto_reload"] is True
|
assert info["auto_reload"] is True, error_message
|
||||||
), f"Lines found: {lines}\nErr output: {err}"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("cmd", ("--auto-reload", "-r"))
|
@pytest.mark.parametrize("cmd", ("--auto-reload", "-r"))
|
||||||
@@ -277,12 +249,12 @@ def test_auto_reload(cmd):
|
|||||||
out, err, exitcode = capture(command)
|
out, err, exitcode = capture(command)
|
||||||
lines = out.split(b"\n")
|
lines = out.split(b"\n")
|
||||||
info = read_app_info(lines)
|
info = read_app_info(lines)
|
||||||
|
error_message = f"Lines found: {lines}\nErr output: {err}"
|
||||||
|
|
||||||
assert info["debug"] is False, f"Lines found: {lines}\nErr output: {err}"
|
assert info, error_message
|
||||||
assert (
|
assert info["debug"] is False, error_message
|
||||||
info["auto_reload"] is True
|
assert info["auto_reload"] is True, error_message
|
||||||
), f"Lines found: {lines}\nErr output: {err}"
|
assert "dev" not in info, error_message
|
||||||
assert "dev" not in info, f"Lines found: {lines}\nErr output: {err}"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@@ -293,10 +265,10 @@ def test_access_logs(cmd, expected):
|
|||||||
out, err, exitcode = capture(command)
|
out, err, exitcode = capture(command)
|
||||||
lines = out.split(b"\n")
|
lines = out.split(b"\n")
|
||||||
info = read_app_info(lines)
|
info = read_app_info(lines)
|
||||||
|
error_message = f"Lines found: {lines}\nErr output: {err}"
|
||||||
|
|
||||||
assert (
|
assert info, error_message
|
||||||
info["access_log"] is expected
|
assert info["access_log"] is expected, error_message
|
||||||
), f"Lines found: {lines}\nErr output: {err}"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("cmd", ("--version", "-v"))
|
@pytest.mark.parametrize("cmd", ("--version", "-v"))
|
||||||
@@ -320,7 +292,7 @@ def test_noisy_exceptions(cmd, expected):
|
|||||||
out, err, exitcode = capture(command)
|
out, err, exitcode = capture(command)
|
||||||
lines = out.split(b"\n")
|
lines = out.split(b"\n")
|
||||||
info = read_app_info(lines)
|
info = read_app_info(lines)
|
||||||
|
error_message = f"Lines found: {lines}\nErr output: {err}"
|
||||||
|
|
||||||
assert (
|
assert info, error_message
|
||||||
info["noisy_exceptions"] is expected
|
assert info["noisy_exceptions"] is expected, error_message
|
||||||
), f"Lines found: {lines}\nErr output: {err}"
|
|
||||||
|
|||||||
@@ -164,12 +164,11 @@ def test_raw_headers(app):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert b"Host: example.com" in request.raw_headers
|
assert request.raw_headers == (
|
||||||
assert b"Accept: */*" in request.raw_headers
|
b"Host: example.com\r\nAccept: */*\r\nAccept-Encoding: gzip, "
|
||||||
assert b"Accept-Encoding: gzip, deflate" in request.raw_headers
|
b"deflate\r\nConnection: keep-alive\r\nUser-Agent: "
|
||||||
assert b"Connection: keep-alive" in request.raw_headers
|
b"Sanic-Testing\r\nFOO: bar"
|
||||||
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):
|
||||||
|
|||||||
@@ -58,36 +58,6 @@ 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(
|
||||||
@@ -122,10 +92,10 @@ def write_file(filename):
|
|||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
def scanner(proc, trigger="complete"):
|
def scanner(proc):
|
||||||
for line in proc.stdout:
|
for line in proc.stdout:
|
||||||
line = line.decode().strip()
|
line = line.decode().strip()
|
||||||
if line.startswith(trigger):
|
if line.startswith("complete"):
|
||||||
yield line
|
yield line
|
||||||
|
|
||||||
|
|
||||||
@@ -138,7 +108,7 @@ argv = dict(
|
|||||||
"sanic",
|
"sanic",
|
||||||
"--port",
|
"--port",
|
||||||
"42204",
|
"42204",
|
||||||
"--auto-reload",
|
"--debug",
|
||||||
"reloader.app",
|
"reloader.app",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -148,7 +118,7 @@ argv = dict(
|
|||||||
"runargs, mode",
|
"runargs, mode",
|
||||||
[
|
[
|
||||||
(dict(port=42202, auto_reload=True), "script"),
|
(dict(port=42202, auto_reload=True), "script"),
|
||||||
(dict(port=42203, auto_reload=True), "module"),
|
(dict(port=42203, debug=True), "module"),
|
||||||
({}, "sanic"),
|
({}, "sanic"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -181,7 +151,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, auto_reload=True), "module"),
|
(dict(port=42303, debug=True), "module"),
|
||||||
({}, "sanic"),
|
({}, "sanic"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -213,30 +183,3 @@ 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)
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ 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
|
||||||
|
|
||||||
@@ -177,17 +176,3 @@ 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(),
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
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 (
|
||||||
@@ -254,7 +256,7 @@ def test_route_strict_slash(app):
|
|||||||
|
|
||||||
|
|
||||||
def test_route_invalid_parameter_syntax(app):
|
def test_route_invalid_parameter_syntax(app):
|
||||||
with pytest.raises(InvalidUsage):
|
with pytest.raises(ValueError):
|
||||||
|
|
||||||
@app.get("/get/<:str>", strict_slashes=True)
|
@app.get("/get/<:str>", strict_slashes=True)
|
||||||
def handler(request):
|
def handler(request):
|
||||||
|
|||||||
@@ -33,14 +33,6 @@ 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()
|
||||||
@@ -64,17 +56,6 @@ 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):
|
||||||
@@ -218,16 +199,3 @@ 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
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ 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
|
||||||
|
|
||||||
|
|
||||||
@@ -109,17 +108,3 @@ 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)
|
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ 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
|
||||||
|
|
||||||
@@ -30,50 +28,3 @@ 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
|
|
||||||
|
|||||||
@@ -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.text
|
assert r.status_code == 200, r.content
|
||||||
assert r.text == "Slept 0.1 seconds.\n"
|
assert r.text == "Slept 0.1 seconds.\n"
|
||||||
|
|
||||||
def spawn():
|
def spawn():
|
||||||
|
|||||||
Reference in New Issue
Block a user