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,6 +1,7 @@ | |||||||
| 📜 Changelog | 📜 Changelog | ||||||
| ============ | ============ | ||||||
|  |  | ||||||
|  | .. mdinclude:: ./releases/22/22.3.md | ||||||
| .. mdinclude:: ./releases/21/21.12.md | .. mdinclude:: ./releases/21/21.12.md | ||||||
| .. mdinclude:: ./releases/21/21.9.md | .. mdinclude:: ./releases/21/21.9.md | ||||||
| .. include:: ../../CHANGELOG.rst | .. include:: ../../CHANGELOG.rst | ||||||
|   | |||||||
							
								
								
									
										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 = Sanic("DelayedResponseApp", strict_slashes=True) | ||||||
|  | app.config.AUTO_EXTEND = False | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.get("/") | @app.get("/") | ||||||
| @@ -11,7 +12,7 @@ async def handler(request): | |||||||
|     return response.redirect("/sleep/3") |     return response.redirect("/sleep/3") | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.get("/sleep/<t:number>") | @app.get("/sleep/<t:float>") | ||||||
| async def handler2(request, t=0.3): | async def handler2(request, t=0.3): | ||||||
|     await sleep(t) |     await sleep(t) | ||||||
|     return response.text(f"Slept {t:.1f} seconds.\n") |     return response.text(f"Slept {t:.1f} seconds.\n") | ||||||
|   | |||||||
| @@ -1 +1 @@ | |||||||
| __version__ = "22.3.0.dev1" | __version__ = "22.3.0" | ||||||
|   | |||||||
							
								
								
									
										19
									
								
								sanic/app.py
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								sanic/app.py
									
									
									
									
									
								
							| @@ -11,7 +11,6 @@ from asyncio import ( | |||||||
|     CancelledError, |     CancelledError, | ||||||
|     Task, |     Task, | ||||||
|     ensure_future, |     ensure_future, | ||||||
|     get_event_loop, |  | ||||||
|     get_running_loop, |     get_running_loop, | ||||||
|     wait_for, |     wait_for, | ||||||
| ) | ) | ||||||
| @@ -253,7 +252,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_event_loop() |         return get_running_loop() | ||||||
|  |  | ||||||
|     # -------------------------------------------------------------------- # |     # -------------------------------------------------------------------- # | ||||||
|     # Registration |     # Registration | ||||||
| @@ -1131,7 +1130,10 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta): | |||||||
|     async def _listener( |     async def _listener( | ||||||
|         app: Sanic, loop: AbstractEventLoop, listener: ListenerType |         app: Sanic, loop: AbstractEventLoop, listener: ListenerType | ||||||
|     ): |     ): | ||||||
|         maybe_coro = listener(app, loop) |         try: | ||||||
|  |             maybe_coro = listener(app)  # type: ignore | ||||||
|  |         except TypeError: | ||||||
|  |             maybe_coro = listener(app, loop)  # type: ignore | ||||||
|         if maybe_coro and isawaitable(maybe_coro): |         if maybe_coro and isawaitable(maybe_coro): | ||||||
|             await maybe_coro |             await maybe_coro | ||||||
|  |  | ||||||
| @@ -1508,7 +1510,8 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta): | |||||||
|             if not Sanic.test_mode: |             if not Sanic.test_mode: | ||||||
|                 raise e |                 raise e | ||||||
|  |  | ||||||
|     def signalize(self): |     def signalize(self, allow_fail_builtin=True): | ||||||
|  |         self.signal_router.allow_fail_builtin = allow_fail_builtin | ||||||
|         try: |         try: | ||||||
|             self.signal_router.finalize() |             self.signal_router.finalize() | ||||||
|         except FinalizationError as e: |         except FinalizationError as e: | ||||||
| @@ -1523,8 +1526,11 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta): | |||||||
|         if hasattr(self, "_ext"): |         if hasattr(self, "_ext"): | ||||||
|             self.ext._display() |             self.ext._display() | ||||||
|  |  | ||||||
|  |         if self.state.is_debug: | ||||||
|  |             self.config.TOUCHUP = False | ||||||
|  |  | ||||||
|         # Setup routers |         # Setup routers | ||||||
|         self.signalize() |         self.signalize(self.config.TOUCHUP) | ||||||
|         self.finalize() |         self.finalize() | ||||||
|  |  | ||||||
|         # TODO: Replace in v22.6 to check against apps in app registry |         # TODO: Replace in v22.6 to check against apps in app registry | ||||||
| @@ -1544,7 +1550,8 @@ 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) | ||||||
|             TouchUp.run(self) |             if self.config.TOUCHUP: | ||||||
|  |                 TouchUp.run(self) | ||||||
|  |  | ||||||
|         self.state.is_started = True |         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" |         legacy_version = len(sys.argv) == 2 and sys.argv[-1] == "-v" | ||||||
|         parse_args = ["--version"] if legacy_version else None |         parse_args = ["--version"] if legacy_version else None | ||||||
|  |  | ||||||
|  |         if not parse_args: | ||||||
|  |             parsed, unknown = self.parser.parse_known_args() | ||||||
|  |             if unknown and parsed.factory: | ||||||
|  |                 for arg in unknown: | ||||||
|  |                     if arg.startswith("--"): | ||||||
|  |                         self.parser.add_argument(arg.split("=")[0]) | ||||||
|  |  | ||||||
|         self.args = self.parser.parse_args(args=parse_args) |         self.args = self.parser.parse_args(args=parse_args) | ||||||
|         self._precheck() |         self._precheck() | ||||||
|  |  | ||||||
| @@ -113,6 +120,14 @@ Or, a path to a directory to run as a simple HTTP server: | |||||||
|                 delimiter = ":" if ":" in self.args.module else "." |                 delimiter = ":" if ":" in self.args.module else "." | ||||||
|                 module_name, app_name = self.args.module.rsplit(delimiter, 1) |                 module_name, app_name = self.args.module.rsplit(delimiter, 1) | ||||||
|  |  | ||||||
|  |                 if module_name == "" and os.path.isdir(self.args.module): | ||||||
|  |                     raise ValueError( | ||||||
|  |                         "App not found.\n" | ||||||
|  |                         "   Please use --simple if you are passing a " | ||||||
|  |                         "directory to sanic.\n" | ||||||
|  |                         f"   eg. sanic {self.args.module} --simple" | ||||||
|  |                     ) | ||||||
|  |  | ||||||
|                 if app_name.endswith("()"): |                 if app_name.endswith("()"): | ||||||
|                     self.args.factory = True |                     self.args.factory = True | ||||||
|                     app_name = app_name[:-2] |                     app_name = app_name[:-2] | ||||||
| @@ -120,14 +135,26 @@ Or, a path to a directory to run as a simple HTTP server: | |||||||
|                 module = import_module(module_name) |                 module = import_module(module_name) | ||||||
|                 app = getattr(module, app_name, None) |                 app = getattr(module, app_name, None) | ||||||
|                 if self.args.factory: |                 if self.args.factory: | ||||||
|                     app = app() |                     try: | ||||||
|  |                         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,6 +38,7 @@ DEFAULT_CONFIG = { | |||||||
|     "REQUEST_MAX_SIZE": 100000000,  # 100 megabytes |     "REQUEST_MAX_SIZE": 100000000,  # 100 megabytes | ||||||
|     "REQUEST_TIMEOUT": 60,  # 60 seconds |     "REQUEST_TIMEOUT": 60,  # 60 seconds | ||||||
|     "RESPONSE_TIMEOUT": 60,  # 60 seconds |     "RESPONSE_TIMEOUT": 60,  # 60 seconds | ||||||
|  |     "TOUCHUP": True, | ||||||
|     "USE_UVLOOP": _default, |     "USE_UVLOOP": _default, | ||||||
|     "WEBSOCKET_MAX_SIZE": 2**20,  # 1 megabyte |     "WEBSOCKET_MAX_SIZE": 2**20,  # 1 megabyte | ||||||
|     "WEBSOCKET_PING_INTERVAL": 20, |     "WEBSOCKET_PING_INTERVAL": 20, | ||||||
| @@ -81,6 +82,7 @@ class Config(dict, metaclass=DescriptorMeta): | |||||||
|     REQUEST_TIMEOUT: int |     REQUEST_TIMEOUT: int | ||||||
|     RESPONSE_TIMEOUT: int |     RESPONSE_TIMEOUT: int | ||||||
|     SERVER_NAME: str |     SERVER_NAME: str | ||||||
|  |     TOUCHUP: bool | ||||||
|     USE_UVLOOP: Union[Default, bool] |     USE_UVLOOP: Union[Default, bool] | ||||||
|     WEBSOCKET_MAX_SIZE: int |     WEBSOCKET_MAX_SIZE: int | ||||||
|     WEBSOCKET_PING_INTERVAL: int |     WEBSOCKET_PING_INTERVAL: int | ||||||
|   | |||||||
| @@ -51,6 +51,10 @@ class InvalidUsage(SanicException): | |||||||
|     quiet = True |     quiet = True | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class BadURL(InvalidUsage): | ||||||
|  |     ... | ||||||
|  |  | ||||||
|  |  | ||||||
| class MethodNotSupported(SanicException): | class MethodNotSupported(SanicException): | ||||||
|     """ |     """ | ||||||
|     **Status**: 405 Method Not Allowed |     **Status**: 405 Method Not Allowed | ||||||
|   | |||||||
| @@ -1,8 +1,9 @@ | |||||||
| from enum import Enum, auto | from enum import Enum, auto | ||||||
| from functools import partial | from functools import partial | ||||||
| from typing import List, Optional, Union | from typing import Callable, List, Optional, Union, overload | ||||||
|  |  | ||||||
| from sanic.base.meta import SanicMeta | from sanic.base.meta import SanicMeta | ||||||
|  | from sanic.exceptions import InvalidUsage | ||||||
| from sanic.models.futures import FutureListener | from sanic.models.futures import FutureListener | ||||||
| from sanic.models.handler_types import ListenerType, Sanic | from sanic.models.handler_types import ListenerType, Sanic | ||||||
|  |  | ||||||
| @@ -17,6 +18,8 @@ class ListenerEvent(str, Enum): | |||||||
|     AFTER_SERVER_STOP = "server.shutdown.after" |     AFTER_SERVER_STOP = "server.shutdown.after" | ||||||
|     MAIN_PROCESS_START = auto() |     MAIN_PROCESS_START = auto() | ||||||
|     MAIN_PROCESS_STOP = auto() |     MAIN_PROCESS_STOP = auto() | ||||||
|  |     RELOAD_PROCESS_START = auto() | ||||||
|  |     RELOAD_PROCESS_STOP = auto() | ||||||
|  |  | ||||||
|  |  | ||||||
| class ListenerMixin(metaclass=SanicMeta): | class ListenerMixin(metaclass=SanicMeta): | ||||||
| @@ -26,12 +29,33 @@ class ListenerMixin(metaclass=SanicMeta): | |||||||
|     def _apply_listener(self, listener: FutureListener): |     def _apply_listener(self, listener: FutureListener): | ||||||
|         raise NotImplementedError  # noqa |         raise NotImplementedError  # noqa | ||||||
|  |  | ||||||
|  |     @overload | ||||||
|  |     def listener( | ||||||
|  |         self, | ||||||
|  |         listener_or_event: ListenerType[Sanic], | ||||||
|  |         event_or_none: str, | ||||||
|  |         apply: bool = ..., | ||||||
|  |     ) -> ListenerType[Sanic]: | ||||||
|  |         ... | ||||||
|  |  | ||||||
|  |     @overload | ||||||
|  |     def listener( | ||||||
|  |         self, | ||||||
|  |         listener_or_event: str, | ||||||
|  |         event_or_none: None = ..., | ||||||
|  |         apply: bool = ..., | ||||||
|  |     ) -> Callable[[ListenerType[Sanic]], ListenerType[Sanic]]: | ||||||
|  |         ... | ||||||
|  |  | ||||||
|     def listener( |     def listener( | ||||||
|         self, |         self, | ||||||
|         listener_or_event: Union[ListenerType[Sanic], str], |         listener_or_event: Union[ListenerType[Sanic], str], | ||||||
|         event_or_none: Optional[str] = None, |         event_or_none: Optional[str] = None, | ||||||
|         apply: bool = True, |         apply: bool = True, | ||||||
|     ) -> ListenerType[Sanic]: |     ) -> Union[ | ||||||
|  |         ListenerType[Sanic], | ||||||
|  |         Callable[[ListenerType[Sanic]], ListenerType[Sanic]], | ||||||
|  |     ]: | ||||||
|         """ |         """ | ||||||
|         Create a listener from a decorated function. |         Create a listener from a decorated function. | ||||||
|  |  | ||||||
| @@ -49,7 +73,9 @@ class ListenerMixin(metaclass=SanicMeta): | |||||||
|         :param event: event to listen to |         :param event: event to listen to | ||||||
|         """ |         """ | ||||||
|  |  | ||||||
|         def register_listener(listener, event): |         def register_listener( | ||||||
|  |             listener: ListenerType[Sanic], event: str | ||||||
|  |         ) -> ListenerType[Sanic]: | ||||||
|             nonlocal apply |             nonlocal apply | ||||||
|  |  | ||||||
|             future_listener = FutureListener(listener, event) |             future_listener = FutureListener(listener, event) | ||||||
| @@ -59,6 +85,10 @@ class ListenerMixin(metaclass=SanicMeta): | |||||||
|             return listener |             return listener | ||||||
|  |  | ||||||
|         if callable(listener_or_event): |         if callable(listener_or_event): | ||||||
|  |             if event_or_none is None: | ||||||
|  |                 raise InvalidUsage( | ||||||
|  |                     "Invalid event registration: Missing event name." | ||||||
|  |                 ) | ||||||
|             return register_listener(listener_or_event, event_or_none) |             return register_listener(listener_or_event, event_or_none) | ||||||
|         else: |         else: | ||||||
|             return partial(register_listener, event=listener_or_event) |             return partial(register_listener, event=listener_or_event) | ||||||
| @@ -73,6 +103,16 @@ class ListenerMixin(metaclass=SanicMeta): | |||||||
|     ) -> ListenerType[Sanic]: |     ) -> ListenerType[Sanic]: | ||||||
|         return self.listener(listener, "main_process_stop") |         return self.listener(listener, "main_process_stop") | ||||||
|  |  | ||||||
|  |     def reload_process_start( | ||||||
|  |         self, listener: ListenerType[Sanic] | ||||||
|  |     ) -> ListenerType[Sanic]: | ||||||
|  |         return self.listener(listener, "reload_process_start") | ||||||
|  |  | ||||||
|  |     def reload_process_stop( | ||||||
|  |         self, listener: ListenerType[Sanic] | ||||||
|  |     ) -> ListenerType[Sanic]: | ||||||
|  |         return self.listener(listener, "reload_process_stop") | ||||||
|  |  | ||||||
|     def before_server_start( |     def before_server_start( | ||||||
|         self, listener: ListenerType[Sanic] |         self, listener: ListenerType[Sanic] | ||||||
|     ) -> ListenerType[Sanic]: |     ) -> ListenerType[Sanic]: | ||||||
|   | |||||||
| @@ -11,6 +11,7 @@ from asyncio import ( | |||||||
|     all_tasks, |     all_tasks, | ||||||
|     get_event_loop, |     get_event_loop, | ||||||
|     get_running_loop, |     get_running_loop, | ||||||
|  |     new_event_loop, | ||||||
| ) | ) | ||||||
| from contextlib import suppress | from contextlib import suppress | ||||||
| from functools import partial | from functools import partial | ||||||
| @@ -32,6 +33,7 @@ from sanic.models.handler_types import ListenerType | |||||||
| from sanic.server import Signal as ServerSignal | from sanic.server import Signal as ServerSignal | ||||||
| from sanic.server import try_use_uvloop | from sanic.server import try_use_uvloop | ||||||
| from sanic.server.async_server import AsyncioServer | from sanic.server.async_server import AsyncioServer | ||||||
|  | from sanic.server.events import trigger_events | ||||||
| from sanic.server.protocols.http_protocol import HttpProtocol | from sanic.server.protocols.http_protocol import HttpProtocol | ||||||
| from sanic.server.protocols.websocket_protocol import WebSocketProtocol | from sanic.server.protocols.websocket_protocol import WebSocketProtocol | ||||||
| from sanic.server.runners import serve, serve_multiple, serve_single | from sanic.server.runners import serve, serve_multiple, serve_single | ||||||
| @@ -538,15 +540,21 @@ class RunnerMixin(metaclass=SanicMeta): | |||||||
|             except IndexError: |             except IndexError: | ||||||
|                 raise RuntimeError("Did not find any applications.") |                 raise RuntimeError("Did not find any applications.") | ||||||
|  |  | ||||||
|  |         reloader_start = primary.listeners.get("reload_process_start") | ||||||
|  |         reloader_stop = primary.listeners.get("reload_process_stop") | ||||||
|         # We want to run auto_reload if ANY of the applications have it enabled |         # We want to run auto_reload if ANY of the applications have it enabled | ||||||
|         if ( |         if ( | ||||||
|             cls.should_auto_reload() |             cls.should_auto_reload() | ||||||
|             and os.environ.get("SANIC_SERVER_RUNNING") != "true" |             and os.environ.get("SANIC_SERVER_RUNNING") != "true" | ||||||
|         ): |         ):  # no cov | ||||||
|  |             loop = new_event_loop() | ||||||
|  |             trigger_events(reloader_start, loop, primary) | ||||||
|             reload_dirs: Set[Path] = primary.state.reload_dirs.union( |             reload_dirs: Set[Path] = primary.state.reload_dirs.union( | ||||||
|                 *(app.state.reload_dirs for app in apps) |                 *(app.state.reload_dirs for app in apps) | ||||||
|             ) |             ) | ||||||
|             return reloader_helpers.watchdog(1.0, reload_dirs) |             reloader_helpers.watchdog(1.0, reload_dirs) | ||||||
|  |             trigger_events(reloader_stop, loop, primary) | ||||||
|  |             return | ||||||
|  |  | ||||||
|         # This exists primarily for unit testing |         # This exists primarily for unit testing | ||||||
|         if not primary.state.server_info:  # no cov |         if not primary.state.server_info:  # no cov | ||||||
|   | |||||||
| @@ -1,11 +1,13 @@ | |||||||
| from asyncio.events import AbstractEventLoop | from asyncio.events import AbstractEventLoop | ||||||
| from typing import Any, Callable, Coroutine, Optional, TypeVar, Union | from typing import Any, Callable, Coroutine, Optional, TypeVar, Union | ||||||
|  |  | ||||||
|  | import sanic | ||||||
|  |  | ||||||
| from sanic.request import Request | from sanic.request import Request | ||||||
| from sanic.response import BaseHTTPResponse, HTTPResponse | from sanic.response import BaseHTTPResponse, HTTPResponse | ||||||
|  |  | ||||||
|  |  | ||||||
| Sanic = TypeVar("Sanic") | Sanic = TypeVar("Sanic", bound="sanic.Sanic") | ||||||
|  |  | ||||||
| MiddlewareResponse = Union[ | MiddlewareResponse = Union[ | ||||||
|     Optional[HTTPResponse], Coroutine[Any, Any, Optional[HTTPResponse]] |     Optional[HTTPResponse], Coroutine[Any, Any, Optional[HTTPResponse]] | ||||||
| @@ -18,8 +20,9 @@ ErrorMiddlewareType = Callable[ | |||||||
|     [Request, BaseException], Optional[Coroutine[Any, Any, None]] |     [Request, BaseException], Optional[Coroutine[Any, Any, None]] | ||||||
| ] | ] | ||||||
| MiddlewareType = Union[RequestMiddlewareType, ResponseMiddlewareType] | MiddlewareType = Union[RequestMiddlewareType, ResponseMiddlewareType] | ||||||
| ListenerType = Callable[ | ListenerType = Union[ | ||||||
|     [Sanic, AbstractEventLoop], Optional[Coroutine[Any, Any, None]] |     Callable[[Sanic], Optional[Coroutine[Any, Any, None]]], | ||||||
|  |     Callable[[Sanic, AbstractEventLoop], Optional[Coroutine[Any, Any, None]]], | ||||||
| ] | ] | ||||||
| RouteHandler = Callable[..., Coroutine[Any, Any, Optional[HTTPResponse]]] | RouteHandler = Callable[..., Coroutine[Any, Any, Optional[HTTPResponse]]] | ||||||
| SignalHandler = Callable[..., Coroutine[Any, Any, None]] | SignalHandler = Callable[..., Coroutine[Any, Any, None]] | ||||||
|   | |||||||
| @@ -30,10 +30,11 @@ from types import SimpleNamespace | |||||||
| from urllib.parse import parse_qs, parse_qsl, unquote, urlunparse | from urllib.parse import parse_qs, parse_qsl, unquote, urlunparse | ||||||
|  |  | ||||||
| from httptools import parse_url  # type: ignore | from httptools import parse_url  # type: ignore | ||||||
|  | from httptools.parser.errors import HttpParserInvalidURLError  # type: ignore | ||||||
|  |  | ||||||
| from sanic.compat import CancelledErrors, Header | from sanic.compat import CancelledErrors, Header | ||||||
| from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE | from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE | ||||||
| from sanic.exceptions import InvalidUsage, ServerError | from sanic.exceptions import BadURL, InvalidUsage, ServerError | ||||||
| from sanic.headers import ( | from sanic.headers import ( | ||||||
|     AcceptContainer, |     AcceptContainer, | ||||||
|     Options, |     Options, | ||||||
| @@ -129,8 +130,10 @@ class Request: | |||||||
|     ): |     ): | ||||||
|  |  | ||||||
|         self.raw_url = url_bytes |         self.raw_url = url_bytes | ||||||
|         # TODO: Content-Encoding detection |         try: | ||||||
|         self._parsed_url = parse_url(url_bytes) |             self._parsed_url = parse_url(url_bytes) | ||||||
|  |         except HttpParserInvalidURLError: | ||||||
|  |             raise BadURL(f"Bad URL: {url_bytes.decode()}") | ||||||
|         self._id: Optional[Union[uuid.UUID, str, int]] = None |         self._id: Optional[Union[uuid.UUID, str, int]] = None | ||||||
|         self._name: Optional[str] = None |         self._name: Optional[str] = None | ||||||
|         self.app = app |         self.app = app | ||||||
| @@ -197,6 +200,53 @@ class Request: | |||||||
|         headers: Optional[Union[Header, Dict[str, str]]] = None, |         headers: Optional[Union[Header, Dict[str, str]]] = None, | ||||||
|         content_type: Optional[str] = None, |         content_type: Optional[str] = None, | ||||||
|     ): |     ): | ||||||
|  |         """Respond to the request without returning. | ||||||
|  |  | ||||||
|  |         This method can only be called once, as you can only respond once. | ||||||
|  |         If no ``response`` argument is passed, one will be created from the | ||||||
|  |         ``status``, ``headers`` and ``content_type`` arguments. | ||||||
|  |  | ||||||
|  |         **The first typical usecase** is if you wish to respond to the | ||||||
|  |         request without returning from the handler: | ||||||
|  |  | ||||||
|  |         .. code-block:: python | ||||||
|  |  | ||||||
|  |             @app.get("/") | ||||||
|  |             async def handler(request: Request): | ||||||
|  |                 data = ...  # Process something | ||||||
|  |  | ||||||
|  |                 json_response = json({"data": data}) | ||||||
|  |                 await request.respond(json_response) | ||||||
|  |  | ||||||
|  |                 # You are now free to continue executing other code | ||||||
|  |                 ... | ||||||
|  |  | ||||||
|  |             @app.on_response | ||||||
|  |             async def add_header(_, response: HTTPResponse): | ||||||
|  |                 # Middlewares still get executed as expected | ||||||
|  |                 response.headers["one"] = "two" | ||||||
|  |  | ||||||
|  |         **The second possible usecase** is for when you want to directly | ||||||
|  |         respond to the request: | ||||||
|  |  | ||||||
|  |         .. code-block:: python | ||||||
|  |  | ||||||
|  |             response = await request.respond(content_type="text/csv") | ||||||
|  |             await response.send("foo,") | ||||||
|  |             await response.send("bar") | ||||||
|  |  | ||||||
|  |             # You can control the completion of the response by calling | ||||||
|  |             # the 'eof()' method: | ||||||
|  |             await response.eof() | ||||||
|  |  | ||||||
|  |         :param response: response instance to send | ||||||
|  |         :param status: status code to return in the response | ||||||
|  |         :param headers: headers to return in the response | ||||||
|  |         :param content_type: Content-Type header of the response | ||||||
|  |         :return: final response being sent (may be different from the | ||||||
|  |             ``response`` parameter because of middlewares) which can be | ||||||
|  |             used to manually send data | ||||||
|  |         """ | ||||||
|         try: |         try: | ||||||
|             if self.stream is not None and self.stream.response: |             if self.stream is not None and self.stream.response: | ||||||
|                 raise ServerError("Second respond call is not allowed.") |                 raise ServerError("Second respond call is not allowed.") | ||||||
|   | |||||||
| @@ -1,8 +1,18 @@ | |||||||
|  | from __future__ import annotations | ||||||
|  |  | ||||||
| from inspect import isawaitable | from inspect import isawaitable | ||||||
| from typing import Any, Callable, Iterable, Optional | from typing import TYPE_CHECKING, Any, Callable, Iterable, Optional | ||||||
|  |  | ||||||
|  |  | ||||||
| def trigger_events(events: Optional[Iterable[Callable[..., Any]]], loop): | if TYPE_CHECKING:  # no cov | ||||||
|  |     from sanic import Sanic | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def trigger_events( | ||||||
|  |     events: Optional[Iterable[Callable[..., Any]]], | ||||||
|  |     loop, | ||||||
|  |     app: Optional[Sanic] = None, | ||||||
|  | ): | ||||||
|     """ |     """ | ||||||
|     Trigger event callbacks (functions or async) |     Trigger event callbacks (functions or async) | ||||||
|  |  | ||||||
| @@ -11,6 +21,9 @@ def trigger_events(events: Optional[Iterable[Callable[..., Any]]], loop): | |||||||
|     """ |     """ | ||||||
|     if events: |     if events: | ||||||
|         for event in events: |         for event in events: | ||||||
|             result = event(loop) |             try: | ||||||
|  |                 result = event() if not app else event(app) | ||||||
|  |             except TypeError: | ||||||
|  |                 result = event(loop) if not app else event(app, loop) | ||||||
|             if isawaitable(result): |             if isawaitable(result): | ||||||
|                 loop.run_until_complete(result) |                 loop.run_until_complete(result) | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ from websockets.server import ServerConnection | |||||||
| from websockets.typing import Subprotocol | from websockets.typing import Subprotocol | ||||||
|  |  | ||||||
| from sanic.exceptions import ServerError | from sanic.exceptions import ServerError | ||||||
| from sanic.log import error_logger | from sanic.log import logger | ||||||
| from sanic.server import HttpProtocol | from sanic.server import HttpProtocol | ||||||
|  |  | ||||||
| from ..websockets.impl import WebsocketImplProtocol | from ..websockets.impl import WebsocketImplProtocol | ||||||
| @@ -104,7 +104,7 @@ class WebSocketProtocol(HttpProtocol): | |||||||
|                 max_size=self.websocket_max_size, |                 max_size=self.websocket_max_size, | ||||||
|                 subprotocols=subprotocols, |                 subprotocols=subprotocols, | ||||||
|                 state=OPEN, |                 state=OPEN, | ||||||
|                 logger=error_logger, |                 logger=logger, | ||||||
|             ) |             ) | ||||||
|             resp: "http11.Response" = ws_conn.accept(request) |             resp: "http11.Response" = ws_conn.accept(request) | ||||||
|         except Exception: |         except Exception: | ||||||
|   | |||||||
| @@ -80,6 +80,7 @@ class SignalRouter(BaseRouter): | |||||||
|             group_class=SignalGroup, |             group_class=SignalGroup, | ||||||
|             stacking=True, |             stacking=True, | ||||||
|         ) |         ) | ||||||
|  |         self.allow_fail_builtin = True | ||||||
|         self.ctx.loop = None |         self.ctx.loop = None | ||||||
|  |  | ||||||
|     def get(  # type: ignore |     def get(  # type: ignore | ||||||
| @@ -129,7 +130,8 @@ class SignalRouter(BaseRouter): | |||||||
|         try: |         try: | ||||||
|             group, handlers, params = self.get(event, condition=condition) |             group, handlers, params = self.get(event, condition=condition) | ||||||
|         except NotFound as e: |         except NotFound as e: | ||||||
|             if fail_not_found: |             is_reserved = event.split(".", 1)[0] in RESERVED_NAMESPACES | ||||||
|  |             if fail_not_found and (not is_reserved or self.allow_fail_builtin): | ||||||
|                 raise e |                 raise e | ||||||
|             else: |             else: | ||||||
|                 if self.ctx.app.debug and self.ctx.app.state.verbosity >= 1: |                 if self.ctx.app.debug and self.ctx.app.state.verbosity >= 1: | ||||||
|   | |||||||
							
								
								
									
										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~=0.7", |     "sanic-routing>=22.3.0,<22.6.0", | ||||||
|     "httptools>=0.0.10", |     "httptools>=0.0.10", | ||||||
|     uvloop, |     uvloop, | ||||||
|     ujson, |     ujson, | ||||||
|     "aiofiles>=0.6.0", |     "aiofiles>=0.6.0", | ||||||
|     "websockets>=10.0", |     "websockets>=10.0", | ||||||
|     "multidict>=5.0,<6.0", |     "multidict>=5.0,<7.0", | ||||||
| ] | ] | ||||||
|  |  | ||||||
| tests_require = [ | tests_require = [ | ||||||
|     "sanic-testing>=0.7.0", |     "sanic-testing>=22.3.0", | ||||||
|     "pytest==6.2.5", |     "pytest==6.2.5", | ||||||
|     "coverage==5.3", |     "coverage==5.3", | ||||||
|     "gunicorn==20.0.4", |     "gunicorn==20.0.4", | ||||||
|   | |||||||
| @@ -34,3 +34,12 @@ async def shutdown(app: Sanic, _): | |||||||
|  |  | ||||||
| def create_app(): | def create_app(): | ||||||
|     return app |     return app | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def create_app_with_args(args): | ||||||
|  |     try: | ||||||
|  |         print(f"foo={args.foo}") | ||||||
|  |     except AttributeError: | ||||||
|  |         print(f"module={args.module}") | ||||||
|  |  | ||||||
|  |     return app | ||||||
|   | |||||||
| @@ -39,16 +39,17 @@ def read_app_info(lines): | |||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.parametrize( | @pytest.mark.parametrize( | ||||||
|     "appname", |     "appname,extra", | ||||||
|     ( |     ( | ||||||
|         "fake.server.app", |         ("fake.server.app", None), | ||||||
|         "fake.server:app", |         ("fake.server:create_app", "--factory"), | ||||||
|         "fake.server:create_app()", |         ("fake.server.create_app()", None), | ||||||
|         "fake.server.create_app()", |  | ||||||
|     ), |     ), | ||||||
| ) | ) | ||||||
| def test_server_run(appname): | def test_server_run(appname, extra): | ||||||
|     command = ["sanic", appname] |     command = ["sanic", appname] | ||||||
|  |     if extra: | ||||||
|  |         command.append(extra) | ||||||
|     out, err, exitcode = capture(command) |     out, err, exitcode = capture(command) | ||||||
|     lines = out.split(b"\n") |     lines = out.split(b"\n") | ||||||
|     firstline = lines[starting_line(lines) + 1] |     firstline = lines[starting_line(lines) + 1] | ||||||
| @@ -57,6 +58,49 @@ def test_server_run(appname): | |||||||
|     assert firstline == b"Goin' Fast @ http://127.0.0.1:8000" |     assert firstline == b"Goin' Fast @ http://127.0.0.1:8000" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_server_run_factory_with_args(): | ||||||
|  |     command = [ | ||||||
|  |         "sanic", | ||||||
|  |         "fake.server.create_app_with_args", | ||||||
|  |         "--factory", | ||||||
|  |     ] | ||||||
|  |     out, err, exitcode = capture(command) | ||||||
|  |     lines = out.split(b"\n") | ||||||
|  |  | ||||||
|  |     assert exitcode != 1, lines | ||||||
|  |     assert b"module=fake.server.create_app_with_args" in lines | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_server_run_factory_with_args_arbitrary(): | ||||||
|  |     command = [ | ||||||
|  |         "sanic", | ||||||
|  |         "fake.server.create_app_with_args", | ||||||
|  |         "--factory", | ||||||
|  |         "--foo=bar", | ||||||
|  |     ] | ||||||
|  |     out, err, exitcode = capture(command) | ||||||
|  |     lines = out.split(b"\n") | ||||||
|  |  | ||||||
|  |     assert exitcode != 1, lines | ||||||
|  |     assert b"foo=bar" in lines | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_error_with_function_as_instance_without_factory_arg(): | ||||||
|  |     command = ["sanic", "fake.server.create_app"] | ||||||
|  |     out, err, exitcode = capture(command) | ||||||
|  |     assert b"try: \nsanic fake.server.create_app --factory" in err | ||||||
|  |     assert exitcode != 1 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_error_with_path_as_instance_without_simple_arg(): | ||||||
|  |     command = ["sanic", "./fake/"] | ||||||
|  |     out, err, exitcode = capture(command) | ||||||
|  |     assert ( | ||||||
|  |         b"Please use --simple if you are passing a directory to sanic." in err | ||||||
|  |     ) | ||||||
|  |     assert exitcode != 1 | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.parametrize( | @pytest.mark.parametrize( | ||||||
|     "cmd", |     "cmd", | ||||||
|     ( |     ( | ||||||
|   | |||||||
| @@ -164,11 +164,12 @@ def test_raw_headers(app): | |||||||
|         }, |         }, | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     assert request.raw_headers == ( |     assert b"Host: example.com" in request.raw_headers | ||||||
|         b"Host: example.com\r\nAccept: */*\r\nAccept-Encoding: gzip, " |     assert b"Accept: */*" in request.raw_headers | ||||||
|         b"deflate\r\nConnection: keep-alive\r\nUser-Agent: " |     assert b"Accept-Encoding: gzip, deflate" in request.raw_headers | ||||||
|         b"Sanic-Testing\r\nFOO: bar" |     assert b"Connection: keep-alive" in request.raw_headers | ||||||
|     ) |     assert b"User-Agent: Sanic-Testing" in request.raw_headers | ||||||
|  |     assert b"FOO: bar" in request.raw_headers | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_request_line(app): | def test_request_line(app): | ||||||
|   | |||||||
| @@ -58,6 +58,36 @@ def write_app(filename, **runargs): | |||||||
|     return text |     return text | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def write_listener_app(filename, **runargs): | ||||||
|  |     start_text = secrets.token_urlsafe() | ||||||
|  |     stop_text = secrets.token_urlsafe() | ||||||
|  |     with open(filename, "w") as f: | ||||||
|  |         f.write( | ||||||
|  |             dedent( | ||||||
|  |                 f"""\ | ||||||
|  |             import os | ||||||
|  |             from sanic import Sanic | ||||||
|  |  | ||||||
|  |             app = Sanic(__name__) | ||||||
|  |  | ||||||
|  |             app.route("/")(lambda x: x) | ||||||
|  |  | ||||||
|  |             @app.reload_process_start | ||||||
|  |             async def reload_start(*_): | ||||||
|  |                 print("reload_start", os.getpid(), {start_text!r}) | ||||||
|  |  | ||||||
|  |             @app.reload_process_stop | ||||||
|  |             async def reload_stop(*_): | ||||||
|  |                 print("reload_stop", os.getpid(), {stop_text!r}) | ||||||
|  |  | ||||||
|  |             if __name__ == "__main__": | ||||||
|  |                 app.run(**{runargs!r}) | ||||||
|  |             """ | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |     return start_text, stop_text | ||||||
|  |  | ||||||
|  |  | ||||||
| def write_json_config_app(filename, jsonfile, **runargs): | def write_json_config_app(filename, jsonfile, **runargs): | ||||||
|     with open(filename, "w") as f: |     with open(filename, "w") as f: | ||||||
|         f.write( |         f.write( | ||||||
| @@ -92,10 +122,10 @@ def write_file(filename): | |||||||
|     return text |     return text | ||||||
|  |  | ||||||
|  |  | ||||||
| def scanner(proc): | def scanner(proc, trigger="complete"): | ||||||
|     for line in proc.stdout: |     for line in proc.stdout: | ||||||
|         line = line.decode().strip() |         line = line.decode().strip() | ||||||
|         if line.startswith("complete"): |         if line.startswith(trigger): | ||||||
|             yield line |             yield line | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -108,7 +138,7 @@ argv = dict( | |||||||
|         "sanic", |         "sanic", | ||||||
|         "--port", |         "--port", | ||||||
|         "42204", |         "42204", | ||||||
|         "--debug", |         "--auto-reload", | ||||||
|         "reloader.app", |         "reloader.app", | ||||||
|     ], |     ], | ||||||
| ) | ) | ||||||
| @@ -118,7 +148,7 @@ argv = dict( | |||||||
|     "runargs, mode", |     "runargs, mode", | ||||||
|     [ |     [ | ||||||
|         (dict(port=42202, auto_reload=True), "script"), |         (dict(port=42202, auto_reload=True), "script"), | ||||||
|         (dict(port=42203, debug=True), "module"), |         (dict(port=42203, auto_reload=True), "module"), | ||||||
|         ({}, "sanic"), |         ({}, "sanic"), | ||||||
|     ], |     ], | ||||||
| ) | ) | ||||||
| @@ -151,7 +181,7 @@ async def test_reloader_live(runargs, mode): | |||||||
|     "runargs, mode", |     "runargs, mode", | ||||||
|     [ |     [ | ||||||
|         (dict(port=42302, auto_reload=True), "script"), |         (dict(port=42302, auto_reload=True), "script"), | ||||||
|         (dict(port=42303, debug=True), "module"), |         (dict(port=42303, auto_reload=True), "module"), | ||||||
|         ({}, "sanic"), |         ({}, "sanic"), | ||||||
|     ], |     ], | ||||||
| ) | ) | ||||||
| @@ -183,3 +213,30 @@ async def test_reloader_live_with_dir(runargs, mode): | |||||||
|             terminate(proc) |             terminate(proc) | ||||||
|             with suppress(TimeoutExpired): |             with suppress(TimeoutExpired): | ||||||
|                 proc.wait(timeout=3) |                 proc.wait(timeout=3) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_reload_listeners(): | ||||||
|  |     with TemporaryDirectory() as tmpdir: | ||||||
|  |         filename = os.path.join(tmpdir, "reloader.py") | ||||||
|  |         start_text, stop_text = write_listener_app( | ||||||
|  |             filename, port=42305, auto_reload=True | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         proc = Popen( | ||||||
|  |             argv["script"], cwd=tmpdir, stdout=PIPE, creationflags=flags | ||||||
|  |         ) | ||||||
|  |         try: | ||||||
|  |             timeout = Timer(TIMER_DELAY, terminate, [proc]) | ||||||
|  |             timeout.start() | ||||||
|  |             # Python apparently keeps using the old source sometimes if | ||||||
|  |             # we don't sleep before rewrite (pycache timestamp problem?) | ||||||
|  |             sleep(1) | ||||||
|  |             line = scanner(proc, "reload_start") | ||||||
|  |             assert start_text in next(line) | ||||||
|  |             line = scanner(proc, "reload_stop") | ||||||
|  |             assert stop_text in next(line) | ||||||
|  |         finally: | ||||||
|  |             timeout.cancel() | ||||||
|  |             terminate(proc) | ||||||
|  |             with suppress(TimeoutExpired): | ||||||
|  |                 proc.wait(timeout=3) | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ from uuid import UUID, uuid4 | |||||||
| import pytest | import pytest | ||||||
|  |  | ||||||
| from sanic import Sanic, response | from sanic import Sanic, response | ||||||
|  | from sanic.exceptions import BadURL | ||||||
| from sanic.request import Request, uuid | from sanic.request import Request, uuid | ||||||
| from sanic.server import HttpProtocol | from sanic.server import HttpProtocol | ||||||
|  |  | ||||||
| @@ -176,3 +177,17 @@ def test_request_accept(): | |||||||
|         "text/x-dvi; q=0.8", |         "text/x-dvi; q=0.8", | ||||||
|         "text/plain; q=0.5", |         "text/plain; q=0.5", | ||||||
|     ] |     ] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_bad_url_parse(): | ||||||
|  |     message = "Bad URL: my.redacted-domain.com:443" | ||||||
|  |     with pytest.raises(BadURL, match=message): | ||||||
|  |         Request( | ||||||
|  |             b"my.redacted-domain.com:443", | ||||||
|  |             Mock(), | ||||||
|  |             Mock(), | ||||||
|  |             Mock(), | ||||||
|  |             Mock(), | ||||||
|  |             Mock(), | ||||||
|  |             Mock(), | ||||||
|  |         ) | ||||||
|   | |||||||
| @@ -1,8 +1,6 @@ | |||||||
| import asyncio | import asyncio | ||||||
| import re | import re | ||||||
|  |  | ||||||
| from unittest.mock import Mock |  | ||||||
|  |  | ||||||
| import pytest | import pytest | ||||||
|  |  | ||||||
| from sanic_routing.exceptions import ( | from sanic_routing.exceptions import ( | ||||||
| @@ -256,7 +254,7 @@ def test_route_strict_slash(app): | |||||||
|  |  | ||||||
|  |  | ||||||
| def test_route_invalid_parameter_syntax(app): | def test_route_invalid_parameter_syntax(app): | ||||||
|     with pytest.raises(ValueError): |     with pytest.raises(InvalidUsage): | ||||||
|  |  | ||||||
|         @app.get("/get/<:str>", strict_slashes=True) |         @app.get("/get/<:str>", strict_slashes=True) | ||||||
|         def handler(request): |         def handler(request): | ||||||
|   | |||||||
| @@ -33,6 +33,14 @@ def create_listener(listener_name, in_list): | |||||||
|     return _listener |     return _listener | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def create_listener_no_loop(listener_name, in_list): | ||||||
|  |     async def _listener(app): | ||||||
|  |         print(f"DEBUG MESSAGE FOR PYTEST for {listener_name}") | ||||||
|  |         in_list.insert(0, app.name + listener_name) | ||||||
|  |  | ||||||
|  |     return _listener | ||||||
|  |  | ||||||
|  |  | ||||||
| def start_stop_app(random_name_app, **run_kwargs): | def start_stop_app(random_name_app, **run_kwargs): | ||||||
|     def stop_on_alarm(signum, frame): |     def stop_on_alarm(signum, frame): | ||||||
|         random_name_app.stop() |         random_name_app.stop() | ||||||
| @@ -56,6 +64,17 @@ def test_single_listener(app, listener_name): | |||||||
|     assert app.name + listener_name == output.pop() |     assert app.name + listener_name == output.pop() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @skipif_no_alarm | ||||||
|  | @pytest.mark.parametrize("listener_name", AVAILABLE_LISTENERS) | ||||||
|  | def test_single_listener_no_loop(app, listener_name): | ||||||
|  |     """Test that listeners on their own work""" | ||||||
|  |     output = [] | ||||||
|  |     # Register listener | ||||||
|  |     app.listener(listener_name)(create_listener_no_loop(listener_name, output)) | ||||||
|  |     start_stop_app(app) | ||||||
|  |     assert app.name + listener_name == output.pop() | ||||||
|  |  | ||||||
|  |  | ||||||
| @skipif_no_alarm | @skipif_no_alarm | ||||||
| @pytest.mark.parametrize("listener_name", AVAILABLE_LISTENERS) | @pytest.mark.parametrize("listener_name", AVAILABLE_LISTENERS) | ||||||
| def test_register_listener(app, listener_name): | def test_register_listener(app, listener_name): | ||||||
| @@ -199,3 +218,16 @@ async def test_missing_startup_raises_exception(app): | |||||||
|  |  | ||||||
|     with pytest.raises(SanicException): |     with pytest.raises(SanicException): | ||||||
|         await srv.before_start() |         await srv.before_start() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_reload_listeners_attached(app): | ||||||
|  |     async def dummy(*_): | ||||||
|  |         ... | ||||||
|  |  | ||||||
|  |     app.reload_process_start(dummy) | ||||||
|  |     app.reload_process_stop(dummy) | ||||||
|  |     app.listener("reload_process_start")(dummy) | ||||||
|  |     app.listener("reload_process_stop")(dummy) | ||||||
|  |  | ||||||
|  |     assert len(app.listeners.get("reload_process_start")) == 2 | ||||||
|  |     assert len(app.listeners.get("reload_process_stop")) == 2 | ||||||
|   | |||||||
| @@ -10,6 +10,7 @@ import pytest | |||||||
| from sanic_testing.testing import HOST, PORT | from sanic_testing.testing import HOST, PORT | ||||||
|  |  | ||||||
| from sanic.compat import ctrlc_workaround_for_windows | from sanic.compat import ctrlc_workaround_for_windows | ||||||
|  | from sanic.exceptions import InvalidUsage | ||||||
| from sanic.response import HTTPResponse | from sanic.response import HTTPResponse | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -108,3 +109,17 @@ def test_windows_workaround(): | |||||||
|     assert res == "OK" |     assert res == "OK" | ||||||
|     res = loop.run_until_complete(atest(True)) |     res = loop.run_until_complete(atest(True)) | ||||||
|     assert res == "OK" |     assert res == "OK" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.skipif(os.name == "nt", reason="May hang CI on py38/windows") | ||||||
|  | def test_signals_with_invalid_invocation(app): | ||||||
|  |     """Test if sanic register fails with invalid invocation""" | ||||||
|  |  | ||||||
|  |     @app.route("/hello") | ||||||
|  |     async def hello_route(request): | ||||||
|  |         return HTTPResponse() | ||||||
|  |  | ||||||
|  |     with pytest.raises( | ||||||
|  |         InvalidUsage, match="Invalid event registration: Missing event name" | ||||||
|  |     ): | ||||||
|  |         app.listener(stop) | ||||||
|   | |||||||
| @@ -2,6 +2,8 @@ import logging | |||||||
|  |  | ||||||
| import pytest | import pytest | ||||||
|  |  | ||||||
|  | from sanic_routing.exceptions import NotFound | ||||||
|  |  | ||||||
| from sanic.signals import RESERVED_NAMESPACES | from sanic.signals import RESERVED_NAMESPACES | ||||||
| from sanic.touchup import TouchUp | from sanic.touchup import TouchUp | ||||||
|  |  | ||||||
| @@ -28,3 +30,50 @@ async def test_ode_removes_dispatch_events(app, caplog, verbosity, result): | |||||||
|             ) |             ) | ||||||
|             in logs |             in logs | ||||||
|         ) is result |         ) is result | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.parametrize("skip_it,result", ((False, True), (True, False))) | ||||||
|  | async def test_skip_touchup(app, caplog, skip_it, result): | ||||||
|  |     app.config.TOUCHUP = not skip_it | ||||||
|  |     with caplog.at_level(logging.DEBUG, logger="sanic.root"): | ||||||
|  |         app.state.verbosity = 2 | ||||||
|  |         await app._startup() | ||||||
|  |     assert app.signal_router.allow_fail_builtin is (not skip_it) | ||||||
|  |     logs = caplog.record_tuples | ||||||
|  |  | ||||||
|  |     for signal in RESERVED_NAMESPACES["http"]: | ||||||
|  |         assert ( | ||||||
|  |             ( | ||||||
|  |                 "sanic.root", | ||||||
|  |                 logging.DEBUG, | ||||||
|  |                 f"Disabling event: {signal}", | ||||||
|  |             ) | ||||||
|  |             in logs | ||||||
|  |         ) is result | ||||||
|  |     not_found_exceptions = 0 | ||||||
|  |     # Skip-touchup disables NotFound exceptions on the dispatcher | ||||||
|  |     for signal in RESERVED_NAMESPACES["http"]: | ||||||
|  |         try: | ||||||
|  |             await app.dispatch(event=signal, inline=True) | ||||||
|  |         except NotFound: | ||||||
|  |             not_found_exceptions += 1 | ||||||
|  |     assert (not_found_exceptions > 0) is result | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.parametrize("skip_it,result", ((False, True), (True, True))) | ||||||
|  | async def test_skip_touchup_non_reserved(app, caplog, skip_it, result): | ||||||
|  |     app.config.TOUCHUP = not skip_it | ||||||
|  |  | ||||||
|  |     @app.signal("foo.bar.one") | ||||||
|  |     def sync_signal(*_): | ||||||
|  |         ... | ||||||
|  |  | ||||||
|  |     await app._startup() | ||||||
|  |     assert app.signal_router.allow_fail_builtin is (not skip_it) | ||||||
|  |     not_found_exception = False | ||||||
|  |     # Skip-touchup doesn't disable NotFound exceptions for user-defined signals | ||||||
|  |     try: | ||||||
|  |         await app.dispatch(event="foo.baz.two", inline=True) | ||||||
|  |     except NotFound: | ||||||
|  |         not_found_exception = True | ||||||
|  |     assert not_found_exception is result | ||||||
|   | |||||||
| @@ -199,7 +199,7 @@ async def test_zero_downtime(): | |||||||
|         for _ in range(40): |         for _ in range(40): | ||||||
|             async with httpx.AsyncClient(transport=transport) as client: |             async with httpx.AsyncClient(transport=transport) as client: | ||||||
|                 r = await client.get("http://localhost/sleep/0.1") |                 r = await client.get("http://localhost/sleep/0.1") | ||||||
|                 assert r.status_code == 200, r.content |                 assert r.status_code == 200, r.text | ||||||
|                 assert r.text == "Slept 0.1 seconds.\n" |                 assert r.text == "Slept 0.1 seconds.\n" | ||||||
|  |  | ||||||
|     def spawn(): |     def spawn(): | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user