Compare commits
	
		
			14 Commits
		
	
	
		
			flaky-test
			...
			v22.3.0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 00218aa9f2 | ||
|   | 874718db94 | ||
|   | bb4474897f | ||
|   | 0cb342aef4 | ||
|   | 030987480c | ||
|   | f6fdc80b40 | ||
|   | 361c242473 | ||
|   | 32962d1e1c | ||
|   | 6e0a6871b5 | ||
|   | 0030425c8c | ||
|   | c9dbc8ed26 | ||
|   | 44b108b564 | ||
|   | 2a8e91052f | ||
|   | 0c9df02e66 | 
							
								
								
									
										1
									
								
								.github/workflows/pr-python310.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/pr-python310.yml
									
									
									
									
										vendored
									
									
								
							| @@ -8,6 +8,7 @@ on: | ||||
|  | ||||
| jobs: | ||||
|   testPy310: | ||||
|     if: github.event.pull_request.draft == false | ||||
|     name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }} | ||||
|     runs-on: ${{ matrix.os }} | ||||
|     strategy: | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| 📜 Changelog | ||||
| ============ | ||||
|  | ||||
| .. mdinclude:: ./releases/22/22.3.md | ||||
| .. mdinclude:: ./releases/21/21.12.md | ||||
| .. mdinclude:: ./releases/21/21.9.md | ||||
| .. include:: ../../CHANGELOG.rst | ||||
|   | ||||
							
								
								
									
										52
									
								
								docs/sanic/releases/22/22.3.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								docs/sanic/releases/22/22.3.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| ## Version 22.3.0 | ||||
|  | ||||
| ### Features | ||||
| - [#2347](https://github.com/sanic-org/sanic/pull/2347) API for multi-application server | ||||
|     - 🚨 *BREAKING CHANGE*: The old `sanic.worker.GunicornWorker` has been **removed**. To run Sanic with `gunicorn`, you should use it thru `uvicorn` [as described in their docs](https://www.uvicorn.org/#running-with-gunicorn). | ||||
|     - 🧁 *SIDE EFFECT*: Named background tasks are now supported, even in Python 3.7 | ||||
| - [#2357](https://github.com/sanic-org/sanic/pull/2357) Parse `Authorization` header as `Request.credentials` | ||||
| - [#2361](https://github.com/sanic-org/sanic/pull/2361) Add config option to skip `Touchup` step in application startup | ||||
| - [#2372](https://github.com/sanic-org/sanic/pull/2372) Updates to CLI help messaging | ||||
| - [#2382](https://github.com/sanic-org/sanic/pull/2382) Downgrade warnings to backwater debug messages  | ||||
| - [#2396](https://github.com/sanic-org/sanic/pull/2396) Allow for `multidict` v0.6 | ||||
| - [#2401](https://github.com/sanic-org/sanic/pull/2401) Upgrade CLI catching for alternative application run types | ||||
| - [#2402](https://github.com/sanic-org/sanic/pull/2402) Conditionally inject CLI arguments into factory | ||||
| - [#2413](https://github.com/sanic-org/sanic/pull/2413) Add new start and stop event listeners to reloader process | ||||
| - [#2414](https://github.com/sanic-org/sanic/pull/2414) Remove loop as required listener arg | ||||
| - [#2415](https://github.com/sanic-org/sanic/pull/2415) Better exception for bad URL parsing | ||||
| - [sanic-routing#47](https://github.com/sanic-org/sanic-routing/pull/47) Add a new extention parameter type: `<file:ext>`, `<file:ext=jpg>`, `<file:ext=jpg|png|gif|svg>`, `<file=int:ext>`, `<file=int:ext=jpg|png|gif|svg>`, `<file=float:ext=tar.gz>` | ||||
|     - 👶 *BETA FEATURE*: This feature will not work with `path` type matching, and is being released as a beta feature only. | ||||
| - [sanic-routing#57](https://github.com/sanic-org/sanic-routing/pull/57) Change `register_pattern` to accept a `str` or `Pattern` | ||||
| - [sanic-routing#58](https://github.com/sanic-org/sanic-routing/pull/58) Default matching on non-empty strings only, and new `strorempty` pattern type | ||||
|     - 🚨 *BREAKING CHANGE*: Previously a route with a dynamic string parameter (`/<foo>` or `/<foo:str>`) would match on any string, including empty strings. It will now **only** match a non-empty string. To retain the old behavior, you should use the new parameter type: `/<foo:strorempty>`. | ||||
|  | ||||
| ### Bugfixes | ||||
| - [#2373](https://github.com/sanic-org/sanic/pull/2373) Remove `error_logger` on websockets | ||||
| - [#2381](https://github.com/sanic-org/sanic/pull/2381) Fix newly assigned `None` in task registry | ||||
| - [sanic-routing#52](https://github.com/sanic-org/sanic-routing/pull/52) Add type casting to regex route matching | ||||
| - [sanic-routing#60](https://github.com/sanic-org/sanic-routing/pull/60) Add requirements check on regex routes (this resolves, for example, multiple static directories with differing `host` values) | ||||
|  | ||||
| ### Deprecations and Removals | ||||
| - [#2362](https://github.com/sanic-org/sanic/pull/2362) 22.3 Deprecations and changes | ||||
|     1. `debug=True` and `--debug` do _NOT_ automatically run `auto_reload` | ||||
|     2. Default error render is with plain text (browsers still get HTML by default because `auto` looks at headers) | ||||
|     3. `config` is required for `ErrorHandler.finalize` | ||||
|     4. `ErrorHandler.lookup` requires two positional args | ||||
|     5. Unused websocket protocol args removed | ||||
| - [#2344](https://github.com/sanic-org/sanic/pull/2344) Deprecate loading of lowercase environment variables | ||||
|  | ||||
| ### Developer infrastructure | ||||
| - [#2363](https://github.com/sanic-org/sanic/pull/2363) Revert code coverage back to Codecov | ||||
| - [#2405](https://github.com/sanic-org/sanic/pull/2405) Upgrade tests for `sanic-routing` changes | ||||
| - [sanic-testing#35](https://github.com/sanic-org/sanic-testing/pull/35) Allow for httpx v0.22 | ||||
|  | ||||
| ### Improved Documentation | ||||
| - [#2350](https://github.com/sanic-org/sanic/pull/2350) Fix link in README for ASGI | ||||
| - [#2398](https://github.com/sanic-org/sanic/pull/2398) Document middleware on_request and on_response | ||||
| - [#2409](https://github.com/sanic-org/sanic/pull/2409) Add missing documentation for `Request.respond` | ||||
|  | ||||
| ### Miscellaneous | ||||
| - [#2376](https://github.com/sanic-org/sanic/pull/2376) Fix typing for `ListenerMixin.listener` | ||||
| - [#2383](https://github.com/sanic-org/sanic/pull/2383) Clear deprecation warning in `asyncio.wait` | ||||
| - [#2387](https://github.com/sanic-org/sanic/pull/2387) Cleanup `__slots__` implementations | ||||
| - [#2390](https://github.com/sanic-org/sanic/pull/2390) Clear deprecation warning in `asyncio.get_event_loop` | ||||
| @@ -4,6 +4,7 @@ from sanic import Sanic, response | ||||
|  | ||||
|  | ||||
| app = Sanic("DelayedResponseApp", strict_slashes=True) | ||||
| app.config.AUTO_EXTEND = False | ||||
|  | ||||
|  | ||||
| @app.get("/") | ||||
| @@ -11,7 +12,7 @@ async def handler(request): | ||||
|     return response.redirect("/sleep/3") | ||||
|  | ||||
|  | ||||
| @app.get("/sleep/<t:number>") | ||||
| @app.get("/sleep/<t:float>") | ||||
| async def handler2(request, t=0.3): | ||||
|     await sleep(t) | ||||
|     return response.text(f"Slept {t:.1f} seconds.\n") | ||||
|   | ||||
| @@ -1 +1 @@ | ||||
| __version__ = "22.3.0.dev1" | ||||
| __version__ = "22.3.0" | ||||
|   | ||||
							
								
								
									
										26
									
								
								sanic/app.py
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								sanic/app.py
									
									
									
									
									
								
							| @@ -11,7 +11,6 @@ from asyncio import ( | ||||
|     CancelledError, | ||||
|     Task, | ||||
|     ensure_future, | ||||
|     get_event_loop, | ||||
|     get_running_loop, | ||||
|     wait_for, | ||||
| ) | ||||
| @@ -44,8 +43,11 @@ from typing import ( | ||||
| from urllib.parse import urlencode, urlunparse | ||||
| from warnings import filterwarnings | ||||
|  | ||||
| from sanic_routing.exceptions import FinalizationError, NotFound | ||||
| from sanic_routing.route import Route | ||||
| from sanic_routing.exceptions import (  # type: ignore | ||||
|     FinalizationError, | ||||
|     NotFound, | ||||
| ) | ||||
| from sanic_routing.route import Route  # type: ignore | ||||
|  | ||||
| from sanic.application.ext import setup_ext | ||||
| from sanic.application.state import ApplicationState, Mode, ServerStage | ||||
| @@ -250,7 +252,7 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta): | ||||
|                 "Loop can only be retrieved after the app has started " | ||||
|                 "running. Not supported with `create_server` function" | ||||
|             ) | ||||
|         return get_event_loop() | ||||
|         return get_running_loop() | ||||
|  | ||||
|     # -------------------------------------------------------------------- # | ||||
|     # Registration | ||||
| @@ -1128,7 +1130,10 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta): | ||||
|     async def _listener( | ||||
|         app: Sanic, loop: AbstractEventLoop, listener: ListenerType | ||||
|     ): | ||||
|         maybe_coro = listener(app, loop) | ||||
|         try: | ||||
|             maybe_coro = listener(app)  # type: ignore | ||||
|         except TypeError: | ||||
|             maybe_coro = listener(app, loop)  # type: ignore | ||||
|         if maybe_coro and isawaitable(maybe_coro): | ||||
|             await maybe_coro | ||||
|  | ||||
| @@ -1505,7 +1510,8 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta): | ||||
|             if not Sanic.test_mode: | ||||
|                 raise e | ||||
|  | ||||
|     def signalize(self): | ||||
|     def signalize(self, allow_fail_builtin=True): | ||||
|         self.signal_router.allow_fail_builtin = allow_fail_builtin | ||||
|         try: | ||||
|             self.signal_router.finalize() | ||||
|         except FinalizationError as e: | ||||
| @@ -1520,8 +1526,11 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta): | ||||
|         if hasattr(self, "_ext"): | ||||
|             self.ext._display() | ||||
|  | ||||
|         if self.state.is_debug: | ||||
|             self.config.TOUCHUP = False | ||||
|  | ||||
|         # Setup routers | ||||
|         self.signalize() | ||||
|         self.signalize(self.config.TOUCHUP) | ||||
|         self.finalize() | ||||
|  | ||||
|         # TODO: Replace in v22.6 to check against apps in app registry | ||||
| @@ -1541,7 +1550,8 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta): | ||||
|             # TODO: | ||||
|             # - Raise warning if secondary apps have error handler config | ||||
|             ErrorHandler.finalize(self.error_handler, config=self.config) | ||||
|             TouchUp.run(self) | ||||
|             if self.config.TOUCHUP: | ||||
|                 TouchUp.run(self) | ||||
|  | ||||
|         self.state.is_started = True | ||||
|  | ||||
|   | ||||
| @@ -68,6 +68,13 @@ Or, a path to a directory to run as a simple HTTP server: | ||||
|         legacy_version = len(sys.argv) == 2 and sys.argv[-1] == "-v" | ||||
|         parse_args = ["--version"] if legacy_version else None | ||||
|  | ||||
|         if not parse_args: | ||||
|             parsed, unknown = self.parser.parse_known_args() | ||||
|             if unknown and parsed.factory: | ||||
|                 for arg in unknown: | ||||
|                     if arg.startswith("--"): | ||||
|                         self.parser.add_argument(arg.split("=")[0]) | ||||
|  | ||||
|         self.args = self.parser.parse_args(args=parse_args) | ||||
|         self._precheck() | ||||
|  | ||||
| @@ -113,6 +120,14 @@ Or, a path to a directory to run as a simple HTTP server: | ||||
|                 delimiter = ":" if ":" in self.args.module else "." | ||||
|                 module_name, app_name = self.args.module.rsplit(delimiter, 1) | ||||
|  | ||||
|                 if module_name == "" and os.path.isdir(self.args.module): | ||||
|                     raise ValueError( | ||||
|                         "App not found.\n" | ||||
|                         "   Please use --simple if you are passing a " | ||||
|                         "directory to sanic.\n" | ||||
|                         f"   eg. sanic {self.args.module} --simple" | ||||
|                     ) | ||||
|  | ||||
|                 if app_name.endswith("()"): | ||||
|                     self.args.factory = True | ||||
|                     app_name = app_name[:-2] | ||||
| @@ -120,14 +135,26 @@ Or, a path to a directory to run as a simple HTTP server: | ||||
|                 module = import_module(module_name) | ||||
|                 app = getattr(module, app_name, None) | ||||
|                 if self.args.factory: | ||||
|                     app = app() | ||||
|                     try: | ||||
|                         app = app(self.args) | ||||
|                     except TypeError: | ||||
|                         app = app() | ||||
|  | ||||
|                 app_type_name = type(app).__name__ | ||||
|  | ||||
|                 if not isinstance(app, Sanic): | ||||
|                     if callable(app): | ||||
|                         solution = f"sanic {self.args.module} --factory" | ||||
|                         raise ValueError( | ||||
|                             "Module is not a Sanic app, it is a" | ||||
|                             f"{app_type_name}\n" | ||||
|                             "  If this callable returns a" | ||||
|                             f"Sanic instance try: \n{solution}" | ||||
|                         ) | ||||
|  | ||||
|                     raise ValueError( | ||||
|                         f"Module is not a Sanic app, it is a {app_type_name}\n" | ||||
|                         f"  Perhaps you meant {self.args.module}.app?" | ||||
|                         f"  Perhaps you meant {self.args.module}:app?" | ||||
|                     ) | ||||
|         except ImportError as e: | ||||
|             if module_name.startswith(e.name): | ||||
|   | ||||
| @@ -38,6 +38,7 @@ DEFAULT_CONFIG = { | ||||
|     "REQUEST_MAX_SIZE": 100000000,  # 100 megabytes | ||||
|     "REQUEST_TIMEOUT": 60,  # 60 seconds | ||||
|     "RESPONSE_TIMEOUT": 60,  # 60 seconds | ||||
|     "TOUCHUP": True, | ||||
|     "USE_UVLOOP": _default, | ||||
|     "WEBSOCKET_MAX_SIZE": 2**20,  # 1 megabyte | ||||
|     "WEBSOCKET_PING_INTERVAL": 20, | ||||
| @@ -81,6 +82,7 @@ class Config(dict, metaclass=DescriptorMeta): | ||||
|     REQUEST_TIMEOUT: int | ||||
|     RESPONSE_TIMEOUT: int | ||||
|     SERVER_NAME: str | ||||
|     TOUCHUP: bool | ||||
|     USE_UVLOOP: Union[Default, bool] | ||||
|     WEBSOCKET_MAX_SIZE: int | ||||
|     WEBSOCKET_PING_INTERVAL: int | ||||
|   | ||||
| @@ -51,6 +51,10 @@ class InvalidUsage(SanicException): | ||||
|     quiet = True | ||||
|  | ||||
|  | ||||
| class BadURL(InvalidUsage): | ||||
|     ... | ||||
|  | ||||
|  | ||||
| class MethodNotSupported(SanicException): | ||||
|     """ | ||||
|     **Status**: 405 Method Not Allowed | ||||
|   | ||||
| @@ -1,8 +1,9 @@ | ||||
| from enum import Enum, auto | ||||
| from functools import partial | ||||
| from typing import List, Optional, Union | ||||
| from typing import Callable, List, Optional, Union, overload | ||||
|  | ||||
| from sanic.base.meta import SanicMeta | ||||
| from sanic.exceptions import InvalidUsage | ||||
| from sanic.models.futures import FutureListener | ||||
| from sanic.models.handler_types import ListenerType, Sanic | ||||
|  | ||||
| @@ -17,6 +18,8 @@ class ListenerEvent(str, Enum): | ||||
|     AFTER_SERVER_STOP = "server.shutdown.after" | ||||
|     MAIN_PROCESS_START = auto() | ||||
|     MAIN_PROCESS_STOP = auto() | ||||
|     RELOAD_PROCESS_START = auto() | ||||
|     RELOAD_PROCESS_STOP = auto() | ||||
|  | ||||
|  | ||||
| class ListenerMixin(metaclass=SanicMeta): | ||||
| @@ -26,12 +29,33 @@ class ListenerMixin(metaclass=SanicMeta): | ||||
|     def _apply_listener(self, listener: FutureListener): | ||||
|         raise NotImplementedError  # noqa | ||||
|  | ||||
|     @overload | ||||
|     def listener( | ||||
|         self, | ||||
|         listener_or_event: ListenerType[Sanic], | ||||
|         event_or_none: str, | ||||
|         apply: bool = ..., | ||||
|     ) -> ListenerType[Sanic]: | ||||
|         ... | ||||
|  | ||||
|     @overload | ||||
|     def listener( | ||||
|         self, | ||||
|         listener_or_event: str, | ||||
|         event_or_none: None = ..., | ||||
|         apply: bool = ..., | ||||
|     ) -> Callable[[ListenerType[Sanic]], ListenerType[Sanic]]: | ||||
|         ... | ||||
|  | ||||
|     def listener( | ||||
|         self, | ||||
|         listener_or_event: Union[ListenerType[Sanic], str], | ||||
|         event_or_none: Optional[str] = None, | ||||
|         apply: bool = True, | ||||
|     ) -> ListenerType[Sanic]: | ||||
|     ) -> Union[ | ||||
|         ListenerType[Sanic], | ||||
|         Callable[[ListenerType[Sanic]], ListenerType[Sanic]], | ||||
|     ]: | ||||
|         """ | ||||
|         Create a listener from a decorated function. | ||||
|  | ||||
| @@ -49,7 +73,9 @@ class ListenerMixin(metaclass=SanicMeta): | ||||
|         :param event: event to listen to | ||||
|         """ | ||||
|  | ||||
|         def register_listener(listener, event): | ||||
|         def register_listener( | ||||
|             listener: ListenerType[Sanic], event: str | ||||
|         ) -> ListenerType[Sanic]: | ||||
|             nonlocal apply | ||||
|  | ||||
|             future_listener = FutureListener(listener, event) | ||||
| @@ -59,6 +85,10 @@ class ListenerMixin(metaclass=SanicMeta): | ||||
|             return listener | ||||
|  | ||||
|         if callable(listener_or_event): | ||||
|             if event_or_none is None: | ||||
|                 raise InvalidUsage( | ||||
|                     "Invalid event registration: Missing event name." | ||||
|                 ) | ||||
|             return register_listener(listener_or_event, event_or_none) | ||||
|         else: | ||||
|             return partial(register_listener, event=listener_or_event) | ||||
| @@ -73,6 +103,16 @@ class ListenerMixin(metaclass=SanicMeta): | ||||
|     ) -> ListenerType[Sanic]: | ||||
|         return self.listener(listener, "main_process_stop") | ||||
|  | ||||
|     def reload_process_start( | ||||
|         self, listener: ListenerType[Sanic] | ||||
|     ) -> ListenerType[Sanic]: | ||||
|         return self.listener(listener, "reload_process_start") | ||||
|  | ||||
|     def reload_process_stop( | ||||
|         self, listener: ListenerType[Sanic] | ||||
|     ) -> ListenerType[Sanic]: | ||||
|         return self.listener(listener, "reload_process_stop") | ||||
|  | ||||
|     def before_server_start( | ||||
|         self, listener: ListenerType[Sanic] | ||||
|     ) -> ListenerType[Sanic]: | ||||
|   | ||||
| @@ -11,6 +11,7 @@ from asyncio import ( | ||||
|     all_tasks, | ||||
|     get_event_loop, | ||||
|     get_running_loop, | ||||
|     new_event_loop, | ||||
| ) | ||||
| from contextlib import suppress | ||||
| from functools import partial | ||||
| @@ -32,6 +33,7 @@ from sanic.models.handler_types import ListenerType | ||||
| from sanic.server import Signal as ServerSignal | ||||
| from sanic.server import try_use_uvloop | ||||
| from sanic.server.async_server import AsyncioServer | ||||
| from sanic.server.events import trigger_events | ||||
| from sanic.server.protocols.http_protocol import HttpProtocol | ||||
| from sanic.server.protocols.websocket_protocol import WebSocketProtocol | ||||
| from sanic.server.runners import serve, serve_multiple, serve_single | ||||
| @@ -538,15 +540,21 @@ class RunnerMixin(metaclass=SanicMeta): | ||||
|             except IndexError: | ||||
|                 raise RuntimeError("Did not find any applications.") | ||||
|  | ||||
|         reloader_start = primary.listeners.get("reload_process_start") | ||||
|         reloader_stop = primary.listeners.get("reload_process_stop") | ||||
|         # We want to run auto_reload if ANY of the applications have it enabled | ||||
|         if ( | ||||
|             cls.should_auto_reload() | ||||
|             and os.environ.get("SANIC_SERVER_RUNNING") != "true" | ||||
|         ): | ||||
|         ):  # no cov | ||||
|             loop = new_event_loop() | ||||
|             trigger_events(reloader_start, loop, primary) | ||||
|             reload_dirs: Set[Path] = primary.state.reload_dirs.union( | ||||
|                 *(app.state.reload_dirs for app in apps) | ||||
|             ) | ||||
|             return reloader_helpers.watchdog(1.0, reload_dirs) | ||||
|             reloader_helpers.watchdog(1.0, reload_dirs) | ||||
|             trigger_events(reloader_stop, loop, primary) | ||||
|             return | ||||
|  | ||||
|         # This exists primarily for unit testing | ||||
|         if not primary.state.server_info:  # no cov | ||||
|   | ||||
| @@ -1,11 +1,13 @@ | ||||
| from asyncio.events import AbstractEventLoop | ||||
| from typing import Any, Callable, Coroutine, Optional, TypeVar, Union | ||||
|  | ||||
| import sanic | ||||
|  | ||||
| from sanic.request import Request | ||||
| from sanic.response import BaseHTTPResponse, HTTPResponse | ||||
|  | ||||
|  | ||||
| Sanic = TypeVar("Sanic") | ||||
| Sanic = TypeVar("Sanic", bound="sanic.Sanic") | ||||
|  | ||||
| MiddlewareResponse = Union[ | ||||
|     Optional[HTTPResponse], Coroutine[Any, Any, Optional[HTTPResponse]] | ||||
| @@ -18,8 +20,9 @@ ErrorMiddlewareType = Callable[ | ||||
|     [Request, BaseException], Optional[Coroutine[Any, Any, None]] | ||||
| ] | ||||
| MiddlewareType = Union[RequestMiddlewareType, ResponseMiddlewareType] | ||||
| ListenerType = Callable[ | ||||
|     [Sanic, AbstractEventLoop], Optional[Coroutine[Any, Any, None]] | ||||
| ListenerType = Union[ | ||||
|     Callable[[Sanic], Optional[Coroutine[Any, Any, None]]], | ||||
|     Callable[[Sanic, AbstractEventLoop], Optional[Coroutine[Any, Any, None]]], | ||||
| ] | ||||
| RouteHandler = Callable[..., Coroutine[Any, Any, Optional[HTTPResponse]]] | ||||
| SignalHandler = Callable[..., Coroutine[Any, Any, None]] | ||||
|   | ||||
| @@ -30,10 +30,11 @@ from types import SimpleNamespace | ||||
| from urllib.parse import parse_qs, parse_qsl, unquote, urlunparse | ||||
|  | ||||
| from httptools import parse_url  # type: ignore | ||||
| from httptools.parser.errors import HttpParserInvalidURLError  # type: ignore | ||||
|  | ||||
| from sanic.compat import CancelledErrors, Header | ||||
| from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE | ||||
| from sanic.exceptions import InvalidUsage, ServerError | ||||
| from sanic.exceptions import BadURL, InvalidUsage, ServerError | ||||
| from sanic.headers import ( | ||||
|     AcceptContainer, | ||||
|     Options, | ||||
| @@ -129,8 +130,10 @@ class Request: | ||||
|     ): | ||||
|  | ||||
|         self.raw_url = url_bytes | ||||
|         # TODO: Content-Encoding detection | ||||
|         self._parsed_url = parse_url(url_bytes) | ||||
|         try: | ||||
|             self._parsed_url = parse_url(url_bytes) | ||||
|         except HttpParserInvalidURLError: | ||||
|             raise BadURL(f"Bad URL: {url_bytes.decode()}") | ||||
|         self._id: Optional[Union[uuid.UUID, str, int]] = None | ||||
|         self._name: Optional[str] = None | ||||
|         self.app = app | ||||
| @@ -197,6 +200,53 @@ class Request: | ||||
|         headers: Optional[Union[Header, Dict[str, str]]] = None, | ||||
|         content_type: Optional[str] = None, | ||||
|     ): | ||||
|         """Respond to the request without returning. | ||||
|  | ||||
|         This method can only be called once, as you can only respond once. | ||||
|         If no ``response`` argument is passed, one will be created from the | ||||
|         ``status``, ``headers`` and ``content_type`` arguments. | ||||
|  | ||||
|         **The first typical usecase** is if you wish to respond to the | ||||
|         request without returning from the handler: | ||||
|  | ||||
|         .. code-block:: python | ||||
|  | ||||
|             @app.get("/") | ||||
|             async def handler(request: Request): | ||||
|                 data = ...  # Process something | ||||
|  | ||||
|                 json_response = json({"data": data}) | ||||
|                 await request.respond(json_response) | ||||
|  | ||||
|                 # You are now free to continue executing other code | ||||
|                 ... | ||||
|  | ||||
|             @app.on_response | ||||
|             async def add_header(_, response: HTTPResponse): | ||||
|                 # Middlewares still get executed as expected | ||||
|                 response.headers["one"] = "two" | ||||
|  | ||||
|         **The second possible usecase** is for when you want to directly | ||||
|         respond to the request: | ||||
|  | ||||
|         .. code-block:: python | ||||
|  | ||||
|             response = await request.respond(content_type="text/csv") | ||||
|             await response.send("foo,") | ||||
|             await response.send("bar") | ||||
|  | ||||
|             # You can control the completion of the response by calling | ||||
|             # the 'eof()' method: | ||||
|             await response.eof() | ||||
|  | ||||
|         :param response: response instance to send | ||||
|         :param status: status code to return in the response | ||||
|         :param headers: headers to return in the response | ||||
|         :param content_type: Content-Type header of the response | ||||
|         :return: final response being sent (may be different from the | ||||
|             ``response`` parameter because of middlewares) which can be | ||||
|             used to manually send data | ||||
|         """ | ||||
|         try: | ||||
|             if self.stream is not None and self.stream.response: | ||||
|                 raise ServerError("Second respond call is not allowed.") | ||||
|   | ||||
| @@ -1,8 +1,18 @@ | ||||
| from __future__ import annotations | ||||
|  | ||||
| from inspect import isawaitable | ||||
| from typing import Any, Callable, Iterable, Optional | ||||
| from typing import TYPE_CHECKING, Any, Callable, Iterable, Optional | ||||
|  | ||||
|  | ||||
| def trigger_events(events: Optional[Iterable[Callable[..., Any]]], loop): | ||||
| if TYPE_CHECKING:  # no cov | ||||
|     from sanic import Sanic | ||||
|  | ||||
|  | ||||
| def trigger_events( | ||||
|     events: Optional[Iterable[Callable[..., Any]]], | ||||
|     loop, | ||||
|     app: Optional[Sanic] = None, | ||||
| ): | ||||
|     """ | ||||
|     Trigger event callbacks (functions or async) | ||||
|  | ||||
| @@ -11,6 +21,9 @@ def trigger_events(events: Optional[Iterable[Callable[..., Any]]], loop): | ||||
|     """ | ||||
|     if events: | ||||
|         for event in events: | ||||
|             result = event(loop) | ||||
|             try: | ||||
|                 result = event() if not app else event(app) | ||||
|             except TypeError: | ||||
|                 result = event(loop) if not app else event(app, loop) | ||||
|             if isawaitable(result): | ||||
|                 loop.run_until_complete(result) | ||||
|   | ||||
| @@ -5,7 +5,7 @@ from websockets.server import ServerConnection | ||||
| from websockets.typing import Subprotocol | ||||
|  | ||||
| from sanic.exceptions import ServerError | ||||
| from sanic.log import error_logger | ||||
| from sanic.log import logger | ||||
| from sanic.server import HttpProtocol | ||||
|  | ||||
| from ..websockets.impl import WebsocketImplProtocol | ||||
| @@ -104,7 +104,7 @@ class WebSocketProtocol(HttpProtocol): | ||||
|                 max_size=self.websocket_max_size, | ||||
|                 subprotocols=subprotocols, | ||||
|                 state=OPEN, | ||||
|                 logger=error_logger, | ||||
|                 logger=logger, | ||||
|             ) | ||||
|             resp: "http11.Response" = ws_conn.accept(request) | ||||
|         except Exception: | ||||
|   | ||||
| @@ -80,6 +80,7 @@ class SignalRouter(BaseRouter): | ||||
|             group_class=SignalGroup, | ||||
|             stacking=True, | ||||
|         ) | ||||
|         self.allow_fail_builtin = True | ||||
|         self.ctx.loop = None | ||||
|  | ||||
|     def get(  # type: ignore | ||||
| @@ -129,7 +130,8 @@ class SignalRouter(BaseRouter): | ||||
|         try: | ||||
|             group, handlers, params = self.get(event, condition=condition) | ||||
|         except NotFound as e: | ||||
|             if fail_not_found: | ||||
|             is_reserved = event.split(".", 1)[0] in RESERVED_NAMESPACES | ||||
|             if fail_not_found and (not is_reserved or self.allow_fail_builtin): | ||||
|                 raise e | ||||
|             else: | ||||
|                 if self.ctx.app.debug and self.ctx.app.state.verbosity >= 1: | ||||
|   | ||||
							
								
								
									
										6
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								setup.py
									
									
									
									
									
								
							| @@ -84,17 +84,17 @@ ujson = "ujson>=1.35" + env_dependency | ||||
| uvloop = "uvloop>=0.5.3" + env_dependency | ||||
| types_ujson = "types-ujson" + env_dependency | ||||
| requirements = [ | ||||
|     "sanic-routing~=0.7", | ||||
|     "sanic-routing>=22.3.0,<22.6.0", | ||||
|     "httptools>=0.0.10", | ||||
|     uvloop, | ||||
|     ujson, | ||||
|     "aiofiles>=0.6.0", | ||||
|     "websockets>=10.0", | ||||
|     "multidict>=5.0,<6.0", | ||||
|     "multidict>=5.0,<7.0", | ||||
| ] | ||||
|  | ||||
| tests_require = [ | ||||
|     "sanic-testing>=0.7.0", | ||||
|     "sanic-testing>=22.3.0", | ||||
|     "pytest==6.2.5", | ||||
|     "coverage==5.3", | ||||
|     "gunicorn==20.0.4", | ||||
|   | ||||
| @@ -34,3 +34,12 @@ async def shutdown(app: Sanic, _): | ||||
|  | ||||
| def create_app(): | ||||
|     return app | ||||
|  | ||||
|  | ||||
| def create_app_with_args(args): | ||||
|     try: | ||||
|         print(f"foo={args.foo}") | ||||
|     except AttributeError: | ||||
|         print(f"module={args.module}") | ||||
|  | ||||
|     return app | ||||
|   | ||||
| @@ -5,7 +5,6 @@ from pathlib import Path | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| from pyparsing import line | ||||
| from sanic_routing import __version__ as __routing_version__ | ||||
|  | ||||
| from sanic import __version__ | ||||
| @@ -40,26 +39,68 @@ def read_app_info(lines): | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize( | ||||
|     "appname", | ||||
|     "appname,extra", | ||||
|     ( | ||||
|         "fake.server.app", | ||||
|         "fake.server:app", | ||||
|         "fake.server:create_app()", | ||||
|         "fake.server.create_app()", | ||||
|         ("fake.server.app", None), | ||||
|         ("fake.server:create_app", "--factory"), | ||||
|         ("fake.server.create_app()", None), | ||||
|     ), | ||||
| ) | ||||
| def test_server_run(appname): | ||||
| def test_server_run(appname, extra): | ||||
|     command = ["sanic", appname] | ||||
|     if extra: | ||||
|         command.append(extra) | ||||
|     out, err, exitcode = capture(command) | ||||
|     lines = out.split(b"\n") | ||||
|     firstline = lines[starting_line(lines) + 1] | ||||
|     error_message = f"Lines found: {lines}\nErr output: {err}" | ||||
|  | ||||
|     assert exitcode != 1 | ||||
|     assert lines, error_message | ||||
|     assert firstline == b"Goin' Fast @ http://127.0.0.1:8000" | ||||
|  | ||||
|  | ||||
| def test_server_run_factory_with_args(): | ||||
|     command = [ | ||||
|         "sanic", | ||||
|         "fake.server.create_app_with_args", | ||||
|         "--factory", | ||||
|     ] | ||||
|     out, err, exitcode = capture(command) | ||||
|     lines = out.split(b"\n") | ||||
|  | ||||
|     assert exitcode != 1, lines | ||||
|     assert b"module=fake.server.create_app_with_args" in lines | ||||
|  | ||||
|  | ||||
| def test_server_run_factory_with_args_arbitrary(): | ||||
|     command = [ | ||||
|         "sanic", | ||||
|         "fake.server.create_app_with_args", | ||||
|         "--factory", | ||||
|         "--foo=bar", | ||||
|     ] | ||||
|     out, err, exitcode = capture(command) | ||||
|     lines = out.split(b"\n") | ||||
|  | ||||
|     assert exitcode != 1, lines | ||||
|     assert b"foo=bar" in lines | ||||
|  | ||||
|  | ||||
| def test_error_with_function_as_instance_without_factory_arg(): | ||||
|     command = ["sanic", "fake.server.create_app"] | ||||
|     out, err, exitcode = capture(command) | ||||
|     assert b"try: \nsanic fake.server.create_app --factory" in err | ||||
|     assert exitcode != 1 | ||||
|  | ||||
|  | ||||
| def test_error_with_path_as_instance_without_simple_arg(): | ||||
|     command = ["sanic", "./fake/"] | ||||
|     out, err, exitcode = capture(command) | ||||
|     assert ( | ||||
|         b"Please use --simple if you are passing a directory to sanic." in err | ||||
|     ) | ||||
|     assert exitcode != 1 | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize( | ||||
|     "cmd", | ||||
|     ( | ||||
| @@ -83,9 +124,6 @@ def test_tls_options(cmd): | ||||
|     out, err, exitcode = capture(command) | ||||
|     assert exitcode != 1 | ||||
|     lines = out.split(b"\n") | ||||
|     error_message = f"Lines found: {lines}\nErr output: {err}" | ||||
|  | ||||
|     assert lines, error_message | ||||
|     firstline = lines[starting_line(lines) + 1] | ||||
|     assert firstline == b"Goin' Fast @ https://127.0.0.1:9999" | ||||
|  | ||||
| @@ -108,9 +146,7 @@ def test_tls_wrong_options(cmd): | ||||
|     assert exitcode == 1 | ||||
|     assert not out | ||||
|     lines = err.decode().split("\n") | ||||
|     error_message = f"Lines found: {lines}\nErr output: {err}" | ||||
|  | ||||
|     assert lines, error_message | ||||
|     errmsg = lines[6] | ||||
|     assert errmsg == "TLS certificates must be specified by either of:" | ||||
|  | ||||
| @@ -127,11 +163,9 @@ def test_host_port_localhost(cmd): | ||||
|     out, err, exitcode = capture(command) | ||||
|     lines = out.split(b"\n") | ||||
|     expected = b"Goin' Fast @ http://localhost:9999" | ||||
|     error_message = f"Lines found: {lines}\nErr output: {err}" | ||||
|  | ||||
|     assert exitcode != 1 | ||||
|     assert lines, error_message | ||||
|     assert expected in lines, error_message | ||||
|     assert expected in lines, f"Lines found: {lines}\nErr output: {err}" | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize( | ||||
| @@ -146,11 +180,9 @@ def test_host_port_ipv4(cmd): | ||||
|     out, err, exitcode = capture(command) | ||||
|     lines = out.split(b"\n") | ||||
|     expected = b"Goin' Fast @ http://127.0.0.127:9999" | ||||
|     error_message = f"Lines found: {lines}\nErr output: {err}" | ||||
|  | ||||
|     assert exitcode != 1 | ||||
|     assert lines, error_message | ||||
|     assert expected in lines, error_message | ||||
|     assert expected in lines, f"Lines found: {lines}\nErr output: {err}" | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize( | ||||
| @@ -165,11 +197,9 @@ def test_host_port_ipv6_any(cmd): | ||||
|     out, err, exitcode = capture(command) | ||||
|     lines = out.split(b"\n") | ||||
|     expected = b"Goin' Fast @ http://[::]:9999" | ||||
|     error_message = f"Lines found: {lines}\nErr output: {err}" | ||||
|  | ||||
|     assert exitcode != 1 | ||||
|     assert lines, error_message | ||||
|     assert expected in lines, error_message | ||||
|     assert expected in lines, f"Lines found: {lines}\nErr output: {err}" | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize( | ||||
| @@ -184,11 +214,9 @@ def test_host_port_ipv6_loopback(cmd): | ||||
|     out, err, exitcode = capture(command) | ||||
|     lines = out.split(b"\n") | ||||
|     expected = b"Goin' Fast @ http://[::1]:9999" | ||||
|     error_message = f"Lines found: {lines}\nErr output: {err}" | ||||
|  | ||||
|     assert exitcode != 1 | ||||
|     assert lines, error_message | ||||
|     assert expected in lines, error_message | ||||
|     assert expected in lines, f"Lines found: {lines}\nErr output: {err}" | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize( | ||||
| @@ -222,12 +250,12 @@ def test_debug(cmd): | ||||
|     out, err, exitcode = capture(command) | ||||
|     lines = out.split(b"\n") | ||||
|     info = read_app_info(lines) | ||||
|     error_message = f"Lines found: {lines}\nErr output: {err}" | ||||
|  | ||||
|     assert info, error_message | ||||
|     assert info["debug"] is True, error_message | ||||
|     assert info["auto_reload"] is False, error_message | ||||
|     assert "dev" not in info, error_message | ||||
|     assert info["debug"] is True, f"Lines found: {lines}\nErr output: {err}" | ||||
|     assert ( | ||||
|         info["auto_reload"] is False | ||||
|     ), f"Lines found: {lines}\nErr output: {err}" | ||||
|     assert "dev" not in info, f"Lines found: {lines}\nErr output: {err}" | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("cmd", ("--dev", "-d")) | ||||
| @@ -236,11 +264,11 @@ def test_dev(cmd): | ||||
|     out, err, exitcode = capture(command) | ||||
|     lines = out.split(b"\n") | ||||
|     info = read_app_info(lines) | ||||
|     error_message = f"Lines found: {lines}\nErr output: {err}" | ||||
|  | ||||
|     assert info, error_message | ||||
|     assert info["debug"] is True, error_message | ||||
|     assert info["auto_reload"] is True, error_message | ||||
|     assert info["debug"] is True, f"Lines found: {lines}\nErr output: {err}" | ||||
|     assert ( | ||||
|         info["auto_reload"] is True | ||||
|     ), f"Lines found: {lines}\nErr output: {err}" | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("cmd", ("--auto-reload", "-r")) | ||||
| @@ -249,12 +277,12 @@ def test_auto_reload(cmd): | ||||
|     out, err, exitcode = capture(command) | ||||
|     lines = out.split(b"\n") | ||||
|     info = read_app_info(lines) | ||||
|     error_message = f"Lines found: {lines}\nErr output: {err}" | ||||
|  | ||||
|     assert info, error_message | ||||
|     assert info["debug"] is False, error_message | ||||
|     assert info["auto_reload"] is True, error_message | ||||
|     assert "dev" not in info, error_message | ||||
|     assert info["debug"] is False, f"Lines found: {lines}\nErr output: {err}" | ||||
|     assert ( | ||||
|         info["auto_reload"] is True | ||||
|     ), f"Lines found: {lines}\nErr output: {err}" | ||||
|     assert "dev" not in info, f"Lines found: {lines}\nErr output: {err}" | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize( | ||||
| @@ -265,10 +293,10 @@ def test_access_logs(cmd, expected): | ||||
|     out, err, exitcode = capture(command) | ||||
|     lines = out.split(b"\n") | ||||
|     info = read_app_info(lines) | ||||
|     error_message = f"Lines found: {lines}\nErr output: {err}" | ||||
|  | ||||
|     assert info, error_message | ||||
|     assert info["access_log"] is expected, error_message | ||||
|     assert ( | ||||
|         info["access_log"] is expected | ||||
|     ), f"Lines found: {lines}\nErr output: {err}" | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("cmd", ("--version", "-v")) | ||||
| @@ -292,7 +320,7 @@ def test_noisy_exceptions(cmd, expected): | ||||
|     out, err, exitcode = capture(command) | ||||
|     lines = out.split(b"\n") | ||||
|     info = read_app_info(lines) | ||||
|     error_message = f"Lines found: {lines}\nErr output: {err}" | ||||
|  | ||||
|     assert info, error_message | ||||
|     assert info["noisy_exceptions"] is expected, error_message | ||||
|     assert ( | ||||
|         info["noisy_exceptions"] is expected | ||||
|     ), f"Lines found: {lines}\nErr output: {err}" | ||||
|   | ||||
| @@ -164,11 +164,12 @@ def test_raw_headers(app): | ||||
|         }, | ||||
|     ) | ||||
|  | ||||
|     assert request.raw_headers == ( | ||||
|         b"Host: example.com\r\nAccept: */*\r\nAccept-Encoding: gzip, " | ||||
|         b"deflate\r\nConnection: keep-alive\r\nUser-Agent: " | ||||
|         b"Sanic-Testing\r\nFOO: bar" | ||||
|     ) | ||||
|     assert b"Host: example.com" in request.raw_headers | ||||
|     assert b"Accept: */*" in request.raw_headers | ||||
|     assert b"Accept-Encoding: gzip, deflate" in request.raw_headers | ||||
|     assert b"Connection: keep-alive" in request.raw_headers | ||||
|     assert b"User-Agent: Sanic-Testing" in request.raw_headers | ||||
|     assert b"FOO: bar" in request.raw_headers | ||||
|  | ||||
|  | ||||
| def test_request_line(app): | ||||
|   | ||||
| @@ -58,6 +58,36 @@ def write_app(filename, **runargs): | ||||
|     return text | ||||
|  | ||||
|  | ||||
| def write_listener_app(filename, **runargs): | ||||
|     start_text = secrets.token_urlsafe() | ||||
|     stop_text = secrets.token_urlsafe() | ||||
|     with open(filename, "w") as f: | ||||
|         f.write( | ||||
|             dedent( | ||||
|                 f"""\ | ||||
|             import os | ||||
|             from sanic import Sanic | ||||
|  | ||||
|             app = Sanic(__name__) | ||||
|  | ||||
|             app.route("/")(lambda x: x) | ||||
|  | ||||
|             @app.reload_process_start | ||||
|             async def reload_start(*_): | ||||
|                 print("reload_start", os.getpid(), {start_text!r}) | ||||
|  | ||||
|             @app.reload_process_stop | ||||
|             async def reload_stop(*_): | ||||
|                 print("reload_stop", os.getpid(), {stop_text!r}) | ||||
|  | ||||
|             if __name__ == "__main__": | ||||
|                 app.run(**{runargs!r}) | ||||
|             """ | ||||
|             ) | ||||
|         ) | ||||
|     return start_text, stop_text | ||||
|  | ||||
|  | ||||
| def write_json_config_app(filename, jsonfile, **runargs): | ||||
|     with open(filename, "w") as f: | ||||
|         f.write( | ||||
| @@ -92,10 +122,10 @@ def write_file(filename): | ||||
|     return text | ||||
|  | ||||
|  | ||||
| def scanner(proc): | ||||
| def scanner(proc, trigger="complete"): | ||||
|     for line in proc.stdout: | ||||
|         line = line.decode().strip() | ||||
|         if line.startswith("complete"): | ||||
|         if line.startswith(trigger): | ||||
|             yield line | ||||
|  | ||||
|  | ||||
| @@ -108,7 +138,7 @@ argv = dict( | ||||
|         "sanic", | ||||
|         "--port", | ||||
|         "42204", | ||||
|         "--debug", | ||||
|         "--auto-reload", | ||||
|         "reloader.app", | ||||
|     ], | ||||
| ) | ||||
| @@ -118,7 +148,7 @@ argv = dict( | ||||
|     "runargs, mode", | ||||
|     [ | ||||
|         (dict(port=42202, auto_reload=True), "script"), | ||||
|         (dict(port=42203, debug=True), "module"), | ||||
|         (dict(port=42203, auto_reload=True), "module"), | ||||
|         ({}, "sanic"), | ||||
|     ], | ||||
| ) | ||||
| @@ -151,7 +181,7 @@ async def test_reloader_live(runargs, mode): | ||||
|     "runargs, mode", | ||||
|     [ | ||||
|         (dict(port=42302, auto_reload=True), "script"), | ||||
|         (dict(port=42303, debug=True), "module"), | ||||
|         (dict(port=42303, auto_reload=True), "module"), | ||||
|         ({}, "sanic"), | ||||
|     ], | ||||
| ) | ||||
| @@ -183,3 +213,30 @@ async def test_reloader_live_with_dir(runargs, mode): | ||||
|             terminate(proc) | ||||
|             with suppress(TimeoutExpired): | ||||
|                 proc.wait(timeout=3) | ||||
|  | ||||
|  | ||||
| def test_reload_listeners(): | ||||
|     with TemporaryDirectory() as tmpdir: | ||||
|         filename = os.path.join(tmpdir, "reloader.py") | ||||
|         start_text, stop_text = write_listener_app( | ||||
|             filename, port=42305, auto_reload=True | ||||
|         ) | ||||
|  | ||||
|         proc = Popen( | ||||
|             argv["script"], cwd=tmpdir, stdout=PIPE, creationflags=flags | ||||
|         ) | ||||
|         try: | ||||
|             timeout = Timer(TIMER_DELAY, terminate, [proc]) | ||||
|             timeout.start() | ||||
|             # Python apparently keeps using the old source sometimes if | ||||
|             # we don't sleep before rewrite (pycache timestamp problem?) | ||||
|             sleep(1) | ||||
|             line = scanner(proc, "reload_start") | ||||
|             assert start_text in next(line) | ||||
|             line = scanner(proc, "reload_stop") | ||||
|             assert stop_text in next(line) | ||||
|         finally: | ||||
|             timeout.cancel() | ||||
|             terminate(proc) | ||||
|             with suppress(TimeoutExpired): | ||||
|                 proc.wait(timeout=3) | ||||
|   | ||||
| @@ -4,6 +4,7 @@ from uuid import UUID, uuid4 | ||||
| import pytest | ||||
|  | ||||
| from sanic import Sanic, response | ||||
| from sanic.exceptions import BadURL | ||||
| from sanic.request import Request, uuid | ||||
| from sanic.server import HttpProtocol | ||||
|  | ||||
| @@ -176,3 +177,17 @@ def test_request_accept(): | ||||
|         "text/x-dvi; q=0.8", | ||||
|         "text/plain; q=0.5", | ||||
|     ] | ||||
|  | ||||
|  | ||||
| def test_bad_url_parse(): | ||||
|     message = "Bad URL: my.redacted-domain.com:443" | ||||
|     with pytest.raises(BadURL, match=message): | ||||
|         Request( | ||||
|             b"my.redacted-domain.com:443", | ||||
|             Mock(), | ||||
|             Mock(), | ||||
|             Mock(), | ||||
|             Mock(), | ||||
|             Mock(), | ||||
|             Mock(), | ||||
|         ) | ||||
|   | ||||
| @@ -1,8 +1,6 @@ | ||||
| import asyncio | ||||
| import re | ||||
|  | ||||
| from unittest.mock import Mock | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| from sanic_routing.exceptions import ( | ||||
| @@ -256,7 +254,7 @@ def test_route_strict_slash(app): | ||||
|  | ||||
|  | ||||
| def test_route_invalid_parameter_syntax(app): | ||||
|     with pytest.raises(ValueError): | ||||
|     with pytest.raises(InvalidUsage): | ||||
|  | ||||
|         @app.get("/get/<:str>", strict_slashes=True) | ||||
|         def handler(request): | ||||
|   | ||||
| @@ -33,6 +33,14 @@ def create_listener(listener_name, in_list): | ||||
|     return _listener | ||||
|  | ||||
|  | ||||
| def create_listener_no_loop(listener_name, in_list): | ||||
|     async def _listener(app): | ||||
|         print(f"DEBUG MESSAGE FOR PYTEST for {listener_name}") | ||||
|         in_list.insert(0, app.name + listener_name) | ||||
|  | ||||
|     return _listener | ||||
|  | ||||
|  | ||||
| def start_stop_app(random_name_app, **run_kwargs): | ||||
|     def stop_on_alarm(signum, frame): | ||||
|         random_name_app.stop() | ||||
| @@ -56,6 +64,17 @@ def test_single_listener(app, listener_name): | ||||
|     assert app.name + listener_name == output.pop() | ||||
|  | ||||
|  | ||||
| @skipif_no_alarm | ||||
| @pytest.mark.parametrize("listener_name", AVAILABLE_LISTENERS) | ||||
| def test_single_listener_no_loop(app, listener_name): | ||||
|     """Test that listeners on their own work""" | ||||
|     output = [] | ||||
|     # Register listener | ||||
|     app.listener(listener_name)(create_listener_no_loop(listener_name, output)) | ||||
|     start_stop_app(app) | ||||
|     assert app.name + listener_name == output.pop() | ||||
|  | ||||
|  | ||||
| @skipif_no_alarm | ||||
| @pytest.mark.parametrize("listener_name", AVAILABLE_LISTENERS) | ||||
| def test_register_listener(app, listener_name): | ||||
| @@ -199,3 +218,16 @@ async def test_missing_startup_raises_exception(app): | ||||
|  | ||||
|     with pytest.raises(SanicException): | ||||
|         await srv.before_start() | ||||
|  | ||||
|  | ||||
| def test_reload_listeners_attached(app): | ||||
|     async def dummy(*_): | ||||
|         ... | ||||
|  | ||||
|     app.reload_process_start(dummy) | ||||
|     app.reload_process_stop(dummy) | ||||
|     app.listener("reload_process_start")(dummy) | ||||
|     app.listener("reload_process_stop")(dummy) | ||||
|  | ||||
|     assert len(app.listeners.get("reload_process_start")) == 2 | ||||
|     assert len(app.listeners.get("reload_process_stop")) == 2 | ||||
|   | ||||
| @@ -10,6 +10,7 @@ import pytest | ||||
| from sanic_testing.testing import HOST, PORT | ||||
|  | ||||
| from sanic.compat import ctrlc_workaround_for_windows | ||||
| from sanic.exceptions import InvalidUsage | ||||
| from sanic.response import HTTPResponse | ||||
|  | ||||
|  | ||||
| @@ -108,3 +109,17 @@ def test_windows_workaround(): | ||||
|     assert res == "OK" | ||||
|     res = loop.run_until_complete(atest(True)) | ||||
|     assert res == "OK" | ||||
|  | ||||
|  | ||||
| @pytest.mark.skipif(os.name == "nt", reason="May hang CI on py38/windows") | ||||
| def test_signals_with_invalid_invocation(app): | ||||
|     """Test if sanic register fails with invalid invocation""" | ||||
|  | ||||
|     @app.route("/hello") | ||||
|     async def hello_route(request): | ||||
|         return HTTPResponse() | ||||
|  | ||||
|     with pytest.raises( | ||||
|         InvalidUsage, match="Invalid event registration: Missing event name" | ||||
|     ): | ||||
|         app.listener(stop) | ||||
|   | ||||
| @@ -2,6 +2,8 @@ import logging | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| from sanic_routing.exceptions import NotFound | ||||
|  | ||||
| from sanic.signals import RESERVED_NAMESPACES | ||||
| from sanic.touchup import TouchUp | ||||
|  | ||||
| @@ -28,3 +30,50 @@ async def test_ode_removes_dispatch_events(app, caplog, verbosity, result): | ||||
|             ) | ||||
|             in logs | ||||
|         ) is result | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("skip_it,result", ((False, True), (True, False))) | ||||
| async def test_skip_touchup(app, caplog, skip_it, result): | ||||
|     app.config.TOUCHUP = not skip_it | ||||
|     with caplog.at_level(logging.DEBUG, logger="sanic.root"): | ||||
|         app.state.verbosity = 2 | ||||
|         await app._startup() | ||||
|     assert app.signal_router.allow_fail_builtin is (not skip_it) | ||||
|     logs = caplog.record_tuples | ||||
|  | ||||
|     for signal in RESERVED_NAMESPACES["http"]: | ||||
|         assert ( | ||||
|             ( | ||||
|                 "sanic.root", | ||||
|                 logging.DEBUG, | ||||
|                 f"Disabling event: {signal}", | ||||
|             ) | ||||
|             in logs | ||||
|         ) is result | ||||
|     not_found_exceptions = 0 | ||||
|     # Skip-touchup disables NotFound exceptions on the dispatcher | ||||
|     for signal in RESERVED_NAMESPACES["http"]: | ||||
|         try: | ||||
|             await app.dispatch(event=signal, inline=True) | ||||
|         except NotFound: | ||||
|             not_found_exceptions += 1 | ||||
|     assert (not_found_exceptions > 0) is result | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("skip_it,result", ((False, True), (True, True))) | ||||
| async def test_skip_touchup_non_reserved(app, caplog, skip_it, result): | ||||
|     app.config.TOUCHUP = not skip_it | ||||
|  | ||||
|     @app.signal("foo.bar.one") | ||||
|     def sync_signal(*_): | ||||
|         ... | ||||
|  | ||||
|     await app._startup() | ||||
|     assert app.signal_router.allow_fail_builtin is (not skip_it) | ||||
|     not_found_exception = False | ||||
|     # Skip-touchup doesn't disable NotFound exceptions for user-defined signals | ||||
|     try: | ||||
|         await app.dispatch(event="foo.baz.two", inline=True) | ||||
|     except NotFound: | ||||
|         not_found_exception = True | ||||
|     assert not_found_exception is result | ||||
|   | ||||
| @@ -199,7 +199,7 @@ async def test_zero_downtime(): | ||||
|         for _ in range(40): | ||||
|             async with httpx.AsyncClient(transport=transport) as client: | ||||
|                 r = await client.get("http://localhost/sleep/0.1") | ||||
|                 assert r.status_code == 200, r.content | ||||
|                 assert r.status_code == 200, r.text | ||||
|                 assert r.text == "Slept 0.1 seconds.\n" | ||||
|  | ||||
|     def spawn(): | ||||
|   | ||||
		Reference in New Issue
	
	Block a user