Compare commits
	
		
			20 Commits
		
	
	
		
			fix-2388-s
			...
			v22.3.2
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 8aecbdb52c | ||
|   | 3a1a9f071d | ||
|   | cc97287f8e | ||
|   | 00218aa9f2 | ||
|   | 874718db94 | ||
|   | bb4474897f | ||
|   | 0cb342aef4 | ||
|   | 030987480c | ||
|   | f6fdc80b40 | ||
|   | 361c242473 | ||
|   | 32962d1e1c | ||
|   | 6e0a6871b5 | ||
|   | 0030425c8c | ||
|   | c9dbc8ed26 | ||
|   | 44b108b564 | ||
|   | 2a8e91052f | ||
|   | 0c9df02e66 | ||
|   | 7523e87937 | ||
|   | d4fb44e986 | ||
|   | 68b654d981 | 
| @@ -140,6 +140,7 @@ To maintain the code consistency, Sanic uses following tools. | ||||
| #. `isort <https://github.com/timothycrosley/isort>`_ | ||||
| #. `black <https://github.com/python/black>`_ | ||||
| #. `flake8 <https://github.com/PyCQA/flake8>`_ | ||||
| #. `slotscheck <https://github.com/ariebovenberg/slotscheck>`_ | ||||
|  | ||||
| isort | ||||
| ***** | ||||
| @@ -167,7 +168,13 @@ flake8 | ||||
| #. pycodestyle | ||||
| #. Ned Batchelder's McCabe script | ||||
|  | ||||
| ``isort``\ , ``black`` and ``flake8`` checks are performed during ``tox`` lint checks. | ||||
| slotscheck | ||||
| ********** | ||||
|  | ||||
| ``slotscheck`` ensures that there are no problems with ``__slots__`` | ||||
| (e.g. overlaps, or missing slots in base classes). | ||||
|  | ||||
| ``isort``\ , ``black``\ , ``flake8`` and ``slotscheck`` checks are performed during ``tox`` lint checks. | ||||
|  | ||||
| The **easiest** way to make your code conform is to run the following before committing. | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| 📜 Changelog | ||||
| ============ | ||||
|  | ||||
| .. mdinclude:: ./releases/22/22.3.md | ||||
| .. mdinclude:: ./releases/21/21.12.md | ||||
| .. mdinclude:: ./releases/21/21.9.md | ||||
| .. include:: ../../CHANGELOG.rst | ||||
|   | ||||
							
								
								
									
										52
									
								
								docs/sanic/releases/22/22.3.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								docs/sanic/releases/22/22.3.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| ## Version 22.3.0 | ||||
|  | ||||
| ### Features | ||||
| - [#2347](https://github.com/sanic-org/sanic/pull/2347) API for multi-application server | ||||
|     - 🚨 *BREAKING CHANGE*: The old `sanic.worker.GunicornWorker` has been **removed**. To run Sanic with `gunicorn`, you should use it thru `uvicorn` [as described in their docs](https://www.uvicorn.org/#running-with-gunicorn). | ||||
|     - 🧁 *SIDE EFFECT*: Named background tasks are now supported, even in Python 3.7 | ||||
| - [#2357](https://github.com/sanic-org/sanic/pull/2357) Parse `Authorization` header as `Request.credentials` | ||||
| - [#2361](https://github.com/sanic-org/sanic/pull/2361) Add config option to skip `Touchup` step in application startup | ||||
| - [#2372](https://github.com/sanic-org/sanic/pull/2372) Updates to CLI help messaging | ||||
| - [#2382](https://github.com/sanic-org/sanic/pull/2382) Downgrade warnings to backwater debug messages  | ||||
| - [#2396](https://github.com/sanic-org/sanic/pull/2396) Allow for `multidict` v0.6 | ||||
| - [#2401](https://github.com/sanic-org/sanic/pull/2401) Upgrade CLI catching for alternative application run types | ||||
| - [#2402](https://github.com/sanic-org/sanic/pull/2402) Conditionally inject CLI arguments into factory | ||||
| - [#2413](https://github.com/sanic-org/sanic/pull/2413) Add new start and stop event listeners to reloader process | ||||
| - [#2414](https://github.com/sanic-org/sanic/pull/2414) Remove loop as required listener arg | ||||
| - [#2415](https://github.com/sanic-org/sanic/pull/2415) Better exception for bad URL parsing | ||||
| - [sanic-routing#47](https://github.com/sanic-org/sanic-routing/pull/47) Add a new extention parameter type: `<file:ext>`, `<file:ext=jpg>`, `<file:ext=jpg|png|gif|svg>`, `<file=int:ext>`, `<file=int:ext=jpg|png|gif|svg>`, `<file=float:ext=tar.gz>` | ||||
|     - 👶 *BETA FEATURE*: This feature will not work with `path` type matching, and is being released as a beta feature only. | ||||
| - [sanic-routing#57](https://github.com/sanic-org/sanic-routing/pull/57) Change `register_pattern` to accept a `str` or `Pattern` | ||||
| - [sanic-routing#58](https://github.com/sanic-org/sanic-routing/pull/58) Default matching on non-empty strings only, and new `strorempty` pattern type | ||||
|     - 🚨 *BREAKING CHANGE*: Previously a route with a dynamic string parameter (`/<foo>` or `/<foo:str>`) would match on any string, including empty strings. It will now **only** match a non-empty string. To retain the old behavior, you should use the new parameter type: `/<foo:strorempty>`. | ||||
|  | ||||
| ### Bugfixes | ||||
| - [#2373](https://github.com/sanic-org/sanic/pull/2373) Remove `error_logger` on websockets | ||||
| - [#2381](https://github.com/sanic-org/sanic/pull/2381) Fix newly assigned `None` in task registry | ||||
| - [sanic-routing#52](https://github.com/sanic-org/sanic-routing/pull/52) Add type casting to regex route matching | ||||
| - [sanic-routing#60](https://github.com/sanic-org/sanic-routing/pull/60) Add requirements check on regex routes (this resolves, for example, multiple static directories with differing `host` values) | ||||
|  | ||||
| ### Deprecations and Removals | ||||
| - [#2362](https://github.com/sanic-org/sanic/pull/2362) 22.3 Deprecations and changes | ||||
|     1. `debug=True` and `--debug` do _NOT_ automatically run `auto_reload` | ||||
|     2. Default error render is with plain text (browsers still get HTML by default because `auto` looks at headers) | ||||
|     3. `config` is required for `ErrorHandler.finalize` | ||||
|     4. `ErrorHandler.lookup` requires two positional args | ||||
|     5. Unused websocket protocol args removed | ||||
| - [#2344](https://github.com/sanic-org/sanic/pull/2344) Deprecate loading of lowercase environment variables | ||||
|  | ||||
| ### Developer infrastructure | ||||
| - [#2363](https://github.com/sanic-org/sanic/pull/2363) Revert code coverage back to Codecov | ||||
| - [#2405](https://github.com/sanic-org/sanic/pull/2405) Upgrade tests for `sanic-routing` changes | ||||
| - [sanic-testing#35](https://github.com/sanic-org/sanic-testing/pull/35) Allow for httpx v0.22 | ||||
|  | ||||
| ### Improved Documentation | ||||
| - [#2350](https://github.com/sanic-org/sanic/pull/2350) Fix link in README for ASGI | ||||
| - [#2398](https://github.com/sanic-org/sanic/pull/2398) Document middleware on_request and on_response | ||||
| - [#2409](https://github.com/sanic-org/sanic/pull/2409) Add missing documentation for `Request.respond` | ||||
|  | ||||
| ### Miscellaneous | ||||
| - [#2376](https://github.com/sanic-org/sanic/pull/2376) Fix typing for `ListenerMixin.listener` | ||||
| - [#2383](https://github.com/sanic-org/sanic/pull/2383) Clear deprecation warning in `asyncio.wait` | ||||
| - [#2387](https://github.com/sanic-org/sanic/pull/2387) Cleanup `__slots__` implementations | ||||
| - [#2390](https://github.com/sanic-org/sanic/pull/2390) Clear deprecation warning in `asyncio.get_event_loop` | ||||
| @@ -4,6 +4,7 @@ from sanic import Sanic, response | ||||
|  | ||||
|  | ||||
| app = Sanic("DelayedResponseApp", strict_slashes=True) | ||||
| app.config.AUTO_EXTEND = False | ||||
|  | ||||
|  | ||||
| @app.get("/") | ||||
| @@ -11,7 +12,7 @@ async def handler(request): | ||||
|     return response.redirect("/sleep/3") | ||||
|  | ||||
|  | ||||
| @app.get("/sleep/<t:number>") | ||||
| @app.get("/sleep/<t:float>") | ||||
| async def handler2(request, t=0.3): | ||||
|     await sleep(t) | ||||
|     return response.text(f"Slept {t:.1f} seconds.\n") | ||||
|   | ||||
| @@ -1 +1 @@ | ||||
| __version__ = "22.3.0.dev1" | ||||
| __version__ = "22.3.2" | ||||
|   | ||||
							
								
								
									
										29
									
								
								sanic/app.py
									
									
									
									
									
								
							
							
						
						
									
										29
									
								
								sanic/app.py
									
									
									
									
									
								
							| @@ -11,7 +11,6 @@ from asyncio import ( | ||||
|     CancelledError, | ||||
|     Task, | ||||
|     ensure_future, | ||||
|     get_event_loop, | ||||
|     get_running_loop, | ||||
|     wait_for, | ||||
| ) | ||||
| @@ -142,7 +141,6 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta): | ||||
|         "error_handler", | ||||
|         "go_fast", | ||||
|         "listeners", | ||||
|         "name", | ||||
|         "named_request_middleware", | ||||
|         "named_response_middleware", | ||||
|         "request_class", | ||||
| @@ -254,7 +252,13 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta): | ||||
|                 "Loop can only be retrieved after the app has started " | ||||
|                 "running. Not supported with `create_server` function" | ||||
|             ) | ||||
|         return get_event_loop() | ||||
|         try: | ||||
|             return get_running_loop() | ||||
|         except RuntimeError: | ||||
|             if sys.version_info > (3, 10): | ||||
|                 return asyncio.get_event_loop_policy().get_event_loop() | ||||
|             else: | ||||
|                 return asyncio.get_event_loop() | ||||
|  | ||||
|     # -------------------------------------------------------------------- # | ||||
|     # Registration | ||||
| @@ -1132,7 +1136,10 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta): | ||||
|     async def _listener( | ||||
|         app: Sanic, loop: AbstractEventLoop, listener: ListenerType | ||||
|     ): | ||||
|         maybe_coro = listener(app, loop) | ||||
|         try: | ||||
|             maybe_coro = listener(app)  # type: ignore | ||||
|         except TypeError: | ||||
|             maybe_coro = listener(app, loop)  # type: ignore | ||||
|         if maybe_coro and isawaitable(maybe_coro): | ||||
|             await maybe_coro | ||||
|  | ||||
| @@ -1268,10 +1275,9 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta): | ||||
|                 ... | ||||
|  | ||||
|     def purge_tasks(self): | ||||
|         for task in self.tasks: | ||||
|         for key, task in self._task_registry.items(): | ||||
|             if task.done() or task.cancelled(): | ||||
|                 name = task.get_name() | ||||
|                 self._task_registry[name] = None | ||||
|                 self._task_registry[key] = None | ||||
|  | ||||
|         self._task_registry = { | ||||
|             k: v for k, v in self._task_registry.items() if v is not None | ||||
| @@ -1510,7 +1516,8 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta): | ||||
|             if not Sanic.test_mode: | ||||
|                 raise e | ||||
|  | ||||
|     def signalize(self): | ||||
|     def signalize(self, allow_fail_builtin=True): | ||||
|         self.signal_router.allow_fail_builtin = allow_fail_builtin | ||||
|         try: | ||||
|             self.signal_router.finalize() | ||||
|         except FinalizationError as e: | ||||
| @@ -1525,8 +1532,11 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta): | ||||
|         if hasattr(self, "_ext"): | ||||
|             self.ext._display() | ||||
|  | ||||
|         if self.state.is_debug: | ||||
|             self.config.TOUCHUP = False | ||||
|  | ||||
|         # Setup routers | ||||
|         self.signalize() | ||||
|         self.signalize(self.config.TOUCHUP) | ||||
|         self.finalize() | ||||
|  | ||||
|         # TODO: Replace in v22.6 to check against apps in app registry | ||||
| @@ -1546,6 +1556,7 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta): | ||||
|             # TODO: | ||||
|             # - Raise warning if secondary apps have error handler config | ||||
|             ErrorHandler.finalize(self.error_handler, config=self.config) | ||||
|             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: | ||||
|                     try: | ||||
|                         app = app(self.args) | ||||
|                     except TypeError: | ||||
|                         app = app() | ||||
|  | ||||
|                 app_type_name = type(app).__name__ | ||||
|  | ||||
|                 if not isinstance(app, Sanic): | ||||
|                     if callable(app): | ||||
|                         solution = f"sanic {self.args.module} --factory" | ||||
|                         raise ValueError( | ||||
|                             "Module is not a Sanic app, it is a" | ||||
|                             f"{app_type_name}\n" | ||||
|                             "  If this callable returns a" | ||||
|                             f"Sanic instance try: \n{solution}" | ||||
|                         ) | ||||
|  | ||||
|                     raise ValueError( | ||||
|                         f"Module is not a Sanic app, it is a {app_type_name}\n" | ||||
|                         f"  Perhaps you meant {self.args.module}.app?" | ||||
|                         f"  Perhaps you meant {self.args.module}:app?" | ||||
|                     ) | ||||
|         except ImportError as e: | ||||
|             if module_name.startswith(e.name): | ||||
|   | ||||
| @@ -72,7 +72,7 @@ def ctrlc_workaround_for_windows(app): | ||||
|         """Asyncio wakeups to allow receiving SIGINT in Python""" | ||||
|         while not die: | ||||
|             # If someone else stopped the app, just exit | ||||
|             if app.is_stopping: | ||||
|             if app.state.is_stopping: | ||||
|                 return | ||||
|             # Windows Python blocks signal handlers while the event loop is | ||||
|             # waiting for I/O. Frequent wakeups keep interrupts flowing. | ||||
|   | ||||
| @@ -38,6 +38,7 @@ DEFAULT_CONFIG = { | ||||
|     "REQUEST_MAX_SIZE": 100000000,  # 100 megabytes | ||||
|     "REQUEST_TIMEOUT": 60,  # 60 seconds | ||||
|     "RESPONSE_TIMEOUT": 60,  # 60 seconds | ||||
|     "TOUCHUP": True, | ||||
|     "USE_UVLOOP": _default, | ||||
|     "WEBSOCKET_MAX_SIZE": 2**20,  # 1 megabyte | ||||
|     "WEBSOCKET_PING_INTERVAL": 20, | ||||
| @@ -81,6 +82,7 @@ class Config(dict, metaclass=DescriptorMeta): | ||||
|     REQUEST_TIMEOUT: int | ||||
|     RESPONSE_TIMEOUT: int | ||||
|     SERVER_NAME: str | ||||
|     TOUCHUP: bool | ||||
|     USE_UVLOOP: Union[Default, bool] | ||||
|     WEBSOCKET_MAX_SIZE: int | ||||
|     WEBSOCKET_PING_INTERVAL: int | ||||
|   | ||||
| @@ -51,6 +51,10 @@ class InvalidUsage(SanicException): | ||||
|     quiet = True | ||||
|  | ||||
|  | ||||
| class BadURL(InvalidUsage): | ||||
|     ... | ||||
|  | ||||
|  | ||||
| class MethodNotSupported(SanicException): | ||||
|     """ | ||||
|     **Status**: 405 Method Not Allowed | ||||
|   | ||||
| @@ -78,7 +78,7 @@ class ErrorHandler: | ||||
|     @classmethod | ||||
|     def _get_fallback_value(cls, error_handler: ErrorHandler, config: Config): | ||||
|         if error_handler._fallback is not _default: | ||||
|             if config._FALLBACK_ERROR_FORMAT is _default: | ||||
|             if config._FALLBACK_ERROR_FORMAT == error_handler._fallback: | ||||
|                 return error_handler.fallback | ||||
|  | ||||
|             error_logger.warning( | ||||
|   | ||||
| @@ -1,8 +1,9 @@ | ||||
| from enum import Enum, auto | ||||
| from functools import partial | ||||
| from typing import List, Optional, Union | ||||
| from typing import Callable, List, Optional, Union, overload | ||||
|  | ||||
| from sanic.base.meta import SanicMeta | ||||
| from sanic.exceptions import InvalidUsage | ||||
| from sanic.models.futures import FutureListener | ||||
| from sanic.models.handler_types import ListenerType, Sanic | ||||
|  | ||||
| @@ -17,6 +18,8 @@ class ListenerEvent(str, Enum): | ||||
|     AFTER_SERVER_STOP = "server.shutdown.after" | ||||
|     MAIN_PROCESS_START = auto() | ||||
|     MAIN_PROCESS_STOP = auto() | ||||
|     RELOAD_PROCESS_START = auto() | ||||
|     RELOAD_PROCESS_STOP = auto() | ||||
|  | ||||
|  | ||||
| class ListenerMixin(metaclass=SanicMeta): | ||||
| @@ -26,12 +29,33 @@ class ListenerMixin(metaclass=SanicMeta): | ||||
|     def _apply_listener(self, listener: FutureListener): | ||||
|         raise NotImplementedError  # noqa | ||||
|  | ||||
|     @overload | ||||
|     def listener( | ||||
|         self, | ||||
|         listener_or_event: ListenerType[Sanic], | ||||
|         event_or_none: str, | ||||
|         apply: bool = ..., | ||||
|     ) -> ListenerType[Sanic]: | ||||
|         ... | ||||
|  | ||||
|     @overload | ||||
|     def listener( | ||||
|         self, | ||||
|         listener_or_event: str, | ||||
|         event_or_none: None = ..., | ||||
|         apply: bool = ..., | ||||
|     ) -> Callable[[ListenerType[Sanic]], ListenerType[Sanic]]: | ||||
|         ... | ||||
|  | ||||
|     def listener( | ||||
|         self, | ||||
|         listener_or_event: Union[ListenerType[Sanic], str], | ||||
|         event_or_none: Optional[str] = None, | ||||
|         apply: bool = True, | ||||
|     ) -> ListenerType[Sanic]: | ||||
|     ) -> Union[ | ||||
|         ListenerType[Sanic], | ||||
|         Callable[[ListenerType[Sanic]], ListenerType[Sanic]], | ||||
|     ]: | ||||
|         """ | ||||
|         Create a listener from a decorated function. | ||||
|  | ||||
| @@ -49,7 +73,9 @@ class ListenerMixin(metaclass=SanicMeta): | ||||
|         :param event: event to listen to | ||||
|         """ | ||||
|  | ||||
|         def register_listener(listener, event): | ||||
|         def register_listener( | ||||
|             listener: ListenerType[Sanic], event: str | ||||
|         ) -> ListenerType[Sanic]: | ||||
|             nonlocal apply | ||||
|  | ||||
|             future_listener = FutureListener(listener, event) | ||||
| @@ -59,6 +85,10 @@ class ListenerMixin(metaclass=SanicMeta): | ||||
|             return listener | ||||
|  | ||||
|         if callable(listener_or_event): | ||||
|             if event_or_none is None: | ||||
|                 raise InvalidUsage( | ||||
|                     "Invalid event registration: Missing event name." | ||||
|                 ) | ||||
|             return register_listener(listener_or_event, event_or_none) | ||||
|         else: | ||||
|             return partial(register_listener, event=listener_or_event) | ||||
| @@ -73,6 +103,16 @@ class ListenerMixin(metaclass=SanicMeta): | ||||
|     ) -> ListenerType[Sanic]: | ||||
|         return self.listener(listener, "main_process_stop") | ||||
|  | ||||
|     def reload_process_start( | ||||
|         self, listener: ListenerType[Sanic] | ||||
|     ) -> ListenerType[Sanic]: | ||||
|         return self.listener(listener, "reload_process_start") | ||||
|  | ||||
|     def reload_process_stop( | ||||
|         self, listener: ListenerType[Sanic] | ||||
|     ) -> ListenerType[Sanic]: | ||||
|         return self.listener(listener, "reload_process_stop") | ||||
|  | ||||
|     def before_server_start( | ||||
|         self, listener: ListenerType[Sanic] | ||||
|     ) -> ListenerType[Sanic]: | ||||
|   | ||||
| @@ -16,9 +16,9 @@ class MiddlewareMixin(metaclass=SanicMeta): | ||||
|         self, middleware_or_request, attach_to="request", apply=True | ||||
|     ): | ||||
|         """ | ||||
|         Decorate and register middleware to be called before a request. | ||||
|         Can either be called as *@app.middleware* or | ||||
|         *@app.middleware('request')* | ||||
|         Decorate and register middleware to be called before a request | ||||
|         is handled or after a response is created. Can either be called as | ||||
|         *@app.middleware* or *@app.middleware('request')*. | ||||
|  | ||||
|         `See user guide re: middleware | ||||
|         <https://sanicframework.org/guide/basics/middleware.html>`__ | ||||
| @@ -47,12 +47,25 @@ class MiddlewareMixin(metaclass=SanicMeta): | ||||
|             ) | ||||
|  | ||||
|     def on_request(self, middleware=None): | ||||
|         """Register a middleware to be called before a request is handled. | ||||
|  | ||||
|         This is the same as *@app.middleware('request')*. | ||||
|  | ||||
|         :param: middleware: A callable that takes in request. | ||||
|         """ | ||||
|         if callable(middleware): | ||||
|             return self.middleware(middleware, "request") | ||||
|         else: | ||||
|             return partial(self.middleware, attach_to="request") | ||||
|  | ||||
|     def on_response(self, middleware=None): | ||||
|         """Register a middleware to be called after a response is created. | ||||
|  | ||||
|         This is the same as *@app.middleware('response')*. | ||||
|  | ||||
|         :param: middleware: | ||||
|             A callable that takes in a request and its response. | ||||
|         """ | ||||
|         if callable(middleware): | ||||
|             return self.middleware(middleware, "response") | ||||
|         else: | ||||
|   | ||||
| @@ -11,6 +11,7 @@ from asyncio import ( | ||||
|     all_tasks, | ||||
|     get_event_loop, | ||||
|     get_running_loop, | ||||
|     new_event_loop, | ||||
| ) | ||||
| from contextlib import suppress | ||||
| from functools import partial | ||||
| @@ -32,6 +33,7 @@ from sanic.models.handler_types import ListenerType | ||||
| from sanic.server import Signal as ServerSignal | ||||
| from sanic.server import try_use_uvloop | ||||
| from sanic.server.async_server import AsyncioServer | ||||
| from sanic.server.events import trigger_events | ||||
| from sanic.server.protocols.http_protocol import HttpProtocol | ||||
| from sanic.server.protocols.websocket_protocol import WebSocketProtocol | ||||
| from sanic.server.runners import serve, serve_multiple, serve_single | ||||
| @@ -538,15 +540,21 @@ class RunnerMixin(metaclass=SanicMeta): | ||||
|             except IndexError: | ||||
|                 raise RuntimeError("Did not find any applications.") | ||||
|  | ||||
|         reloader_start = primary.listeners.get("reload_process_start") | ||||
|         reloader_stop = primary.listeners.get("reload_process_stop") | ||||
|         # We want to run auto_reload if ANY of the applications have it enabled | ||||
|         if ( | ||||
|             cls.should_auto_reload() | ||||
|             and os.environ.get("SANIC_SERVER_RUNNING") != "true" | ||||
|         ): | ||||
|         ):  # no cov | ||||
|             loop = new_event_loop() | ||||
|             trigger_events(reloader_start, loop, primary) | ||||
|             reload_dirs: Set[Path] = primary.state.reload_dirs.union( | ||||
|                 *(app.state.reload_dirs for app in apps) | ||||
|             ) | ||||
|             return reloader_helpers.watchdog(1.0, reload_dirs) | ||||
|             reloader_helpers.watchdog(1.0, reload_dirs) | ||||
|             trigger_events(reloader_stop, loop, primary) | ||||
|             return | ||||
|  | ||||
|         # This exists primarily for unit testing | ||||
|         if not primary.state.server_info:  # no cov | ||||
|   | ||||
| @@ -1,11 +1,13 @@ | ||||
| from asyncio.events import AbstractEventLoop | ||||
| from typing import Any, Callable, Coroutine, Optional, TypeVar, Union | ||||
|  | ||||
| import sanic | ||||
|  | ||||
| from sanic.request import Request | ||||
| from sanic.response import BaseHTTPResponse, HTTPResponse | ||||
|  | ||||
|  | ||||
| Sanic = TypeVar("Sanic") | ||||
| Sanic = TypeVar("Sanic", bound="sanic.Sanic") | ||||
|  | ||||
| MiddlewareResponse = Union[ | ||||
|     Optional[HTTPResponse], Coroutine[Any, Any, Optional[HTTPResponse]] | ||||
| @@ -18,8 +20,9 @@ ErrorMiddlewareType = Callable[ | ||||
|     [Request, BaseException], Optional[Coroutine[Any, Any, None]] | ||||
| ] | ||||
| MiddlewareType = Union[RequestMiddlewareType, ResponseMiddlewareType] | ||||
| ListenerType = Callable[ | ||||
|     [Sanic, AbstractEventLoop], Optional[Coroutine[Any, Any, None]] | ||||
| ListenerType = Union[ | ||||
|     Callable[[Sanic], Optional[Coroutine[Any, Any, None]]], | ||||
|     Callable[[Sanic, AbstractEventLoop], Optional[Coroutine[Any, Any, None]]], | ||||
| ] | ||||
| RouteHandler = Callable[..., Coroutine[Any, Any, Optional[HTTPResponse]]] | ||||
| SignalHandler = Callable[..., Coroutine[Any, Any, None]] | ||||
|   | ||||
| @@ -30,10 +30,11 @@ from types import SimpleNamespace | ||||
| from urllib.parse import parse_qs, parse_qsl, unquote, urlunparse | ||||
|  | ||||
| from httptools import parse_url  # type: ignore | ||||
| from httptools.parser.errors import HttpParserInvalidURLError  # type: ignore | ||||
|  | ||||
| from sanic.compat import CancelledErrors, Header | ||||
| from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE | ||||
| from sanic.exceptions import InvalidUsage, ServerError | ||||
| from sanic.exceptions import BadURL, InvalidUsage, ServerError | ||||
| from sanic.headers import ( | ||||
|     AcceptContainer, | ||||
|     Options, | ||||
| @@ -129,8 +130,10 @@ class Request: | ||||
|     ): | ||||
|  | ||||
|         self.raw_url = url_bytes | ||||
|         # TODO: Content-Encoding detection | ||||
|         try: | ||||
|             self._parsed_url = parse_url(url_bytes) | ||||
|         except HttpParserInvalidURLError: | ||||
|             raise BadURL(f"Bad URL: {url_bytes.decode()}") | ||||
|         self._id: Optional[Union[uuid.UUID, str, int]] = None | ||||
|         self._name: Optional[str] = None | ||||
|         self.app = app | ||||
| @@ -197,6 +200,53 @@ class Request: | ||||
|         headers: Optional[Union[Header, Dict[str, str]]] = None, | ||||
|         content_type: Optional[str] = None, | ||||
|     ): | ||||
|         """Respond to the request without returning. | ||||
|  | ||||
|         This method can only be called once, as you can only respond once. | ||||
|         If no ``response`` argument is passed, one will be created from the | ||||
|         ``status``, ``headers`` and ``content_type`` arguments. | ||||
|  | ||||
|         **The first typical usecase** is if you wish to respond to the | ||||
|         request without returning from the handler: | ||||
|  | ||||
|         .. code-block:: python | ||||
|  | ||||
|             @app.get("/") | ||||
|             async def handler(request: Request): | ||||
|                 data = ...  # Process something | ||||
|  | ||||
|                 json_response = json({"data": data}) | ||||
|                 await request.respond(json_response) | ||||
|  | ||||
|                 # You are now free to continue executing other code | ||||
|                 ... | ||||
|  | ||||
|             @app.on_response | ||||
|             async def add_header(_, response: HTTPResponse): | ||||
|                 # Middlewares still get executed as expected | ||||
|                 response.headers["one"] = "two" | ||||
|  | ||||
|         **The second possible usecase** is for when you want to directly | ||||
|         respond to the request: | ||||
|  | ||||
|         .. code-block:: python | ||||
|  | ||||
|             response = await request.respond(content_type="text/csv") | ||||
|             await response.send("foo,") | ||||
|             await response.send("bar") | ||||
|  | ||||
|             # You can control the completion of the response by calling | ||||
|             # the 'eof()' method: | ||||
|             await response.eof() | ||||
|  | ||||
|         :param response: response instance to send | ||||
|         :param status: status code to return in the response | ||||
|         :param headers: headers to return in the response | ||||
|         :param content_type: Content-Type header of the response | ||||
|         :return: final response being sent (may be different from the | ||||
|             ``response`` parameter because of middlewares) which can be | ||||
|             used to manually send data | ||||
|         """ | ||||
|         try: | ||||
|             if self.stream is not None and self.stream.response: | ||||
|                 raise ServerError("Second respond call is not allowed.") | ||||
|   | ||||
| @@ -50,6 +50,16 @@ class BaseHTTPResponse: | ||||
|     The base class for all HTTP Responses | ||||
|     """ | ||||
|  | ||||
|     __slots__ = ( | ||||
|         "asgi", | ||||
|         "body", | ||||
|         "content_type", | ||||
|         "stream", | ||||
|         "status", | ||||
|         "headers", | ||||
|         "_cookies", | ||||
|     ) | ||||
|  | ||||
|     _dumps = json_dumps | ||||
|  | ||||
|     def __init__(self): | ||||
| @@ -156,7 +166,7 @@ class HTTPResponse(BaseHTTPResponse): | ||||
|     :type content_type: Optional[str] | ||||
|     """ | ||||
|  | ||||
|     __slots__ = ("body", "status", "content_type", "headers", "_cookies") | ||||
|     __slots__ = () | ||||
|  | ||||
|     def __init__( | ||||
|         self, | ||||
|   | ||||
| @@ -1,8 +1,18 @@ | ||||
| from __future__ import annotations | ||||
|  | ||||
| from inspect import isawaitable | ||||
| from typing import Any, Callable, Iterable, Optional | ||||
| from typing import TYPE_CHECKING, Any, Callable, Iterable, Optional | ||||
|  | ||||
|  | ||||
| def trigger_events(events: Optional[Iterable[Callable[..., Any]]], loop): | ||||
| if TYPE_CHECKING:  # no cov | ||||
|     from sanic import Sanic | ||||
|  | ||||
|  | ||||
| def trigger_events( | ||||
|     events: Optional[Iterable[Callable[..., Any]]], | ||||
|     loop, | ||||
|     app: Optional[Sanic] = None, | ||||
| ): | ||||
|     """ | ||||
|     Trigger event callbacks (functions or async) | ||||
|  | ||||
| @@ -11,6 +21,9 @@ def trigger_events(events: Optional[Iterable[Callable[..., Any]]], loop): | ||||
|     """ | ||||
|     if events: | ||||
|         for event in events: | ||||
|             result = event(loop) | ||||
|             try: | ||||
|                 result = event() if not app else event(app) | ||||
|             except TypeError: | ||||
|                 result = event(loop) if not app else event(app, loop) | ||||
|             if isawaitable(result): | ||||
|                 loop.run_until_complete(result) | ||||
|   | ||||
| @@ -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: | ||||
|   | ||||
							
								
								
									
										7
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										7
									
								
								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", | ||||
| @@ -112,6 +112,7 @@ tests_require = [ | ||||
|     "docutils", | ||||
|     "pygments", | ||||
|     "uvicorn<0.15.0", | ||||
|     "slotscheck>=0.8.0,<1", | ||||
|     types_ujson, | ||||
| ] | ||||
|  | ||||
|   | ||||
| @@ -34,3 +34,12 @@ async def shutdown(app: Sanic, _): | ||||
|  | ||||
| def create_app(): | ||||
|     return app | ||||
|  | ||||
|  | ||||
| def create_app_with_args(args): | ||||
|     try: | ||||
|         print(f"foo={args.foo}") | ||||
|     except AttributeError: | ||||
|         print(f"module={args.module}") | ||||
|  | ||||
|     return app | ||||
|   | ||||
| @@ -39,16 +39,17 @@ def read_app_info(lines): | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize( | ||||
|     "appname", | ||||
|     "appname,extra", | ||||
|     ( | ||||
|         "fake.server.app", | ||||
|         "fake.server:app", | ||||
|         "fake.server:create_app()", | ||||
|         "fake.server.create_app()", | ||||
|         ("fake.server.app", None), | ||||
|         ("fake.server:create_app", "--factory"), | ||||
|         ("fake.server.create_app()", None), | ||||
|     ), | ||||
| ) | ||||
| def test_server_run(appname): | ||||
| def test_server_run(appname, extra): | ||||
|     command = ["sanic", appname] | ||||
|     if extra: | ||||
|         command.append(extra) | ||||
|     out, err, exitcode = capture(command) | ||||
|     lines = out.split(b"\n") | ||||
|     firstline = lines[starting_line(lines) + 1] | ||||
| @@ -57,6 +58,49 @@ def test_server_run(appname): | ||||
|     assert firstline == b"Goin' Fast @ http://127.0.0.1:8000" | ||||
|  | ||||
|  | ||||
| def test_server_run_factory_with_args(): | ||||
|     command = [ | ||||
|         "sanic", | ||||
|         "fake.server.create_app_with_args", | ||||
|         "--factory", | ||||
|     ] | ||||
|     out, err, exitcode = capture(command) | ||||
|     lines = out.split(b"\n") | ||||
|  | ||||
|     assert exitcode != 1, lines | ||||
|     assert b"module=fake.server.create_app_with_args" in lines | ||||
|  | ||||
|  | ||||
| def test_server_run_factory_with_args_arbitrary(): | ||||
|     command = [ | ||||
|         "sanic", | ||||
|         "fake.server.create_app_with_args", | ||||
|         "--factory", | ||||
|         "--foo=bar", | ||||
|     ] | ||||
|     out, err, exitcode = capture(command) | ||||
|     lines = out.split(b"\n") | ||||
|  | ||||
|     assert exitcode != 1, lines | ||||
|     assert b"foo=bar" in lines | ||||
|  | ||||
|  | ||||
| def test_error_with_function_as_instance_without_factory_arg(): | ||||
|     command = ["sanic", "fake.server.create_app"] | ||||
|     out, err, exitcode = capture(command) | ||||
|     assert b"try: \nsanic fake.server.create_app --factory" in err | ||||
|     assert exitcode != 1 | ||||
|  | ||||
|  | ||||
| def test_error_with_path_as_instance_without_simple_arg(): | ||||
|     command = ["sanic", "./fake/"] | ||||
|     out, err, exitcode = capture(command) | ||||
|     assert ( | ||||
|         b"Please use --simple if you are passing a directory to sanic." in err | ||||
|     ) | ||||
|     assert exitcode != 1 | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize( | ||||
|     "cmd", | ||||
|     ( | ||||
|   | ||||
| @@ -164,11 +164,12 @@ def test_raw_headers(app): | ||||
|         }, | ||||
|     ) | ||||
|  | ||||
|     assert request.raw_headers == ( | ||||
|         b"Host: example.com\r\nAccept: */*\r\nAccept-Encoding: gzip, " | ||||
|         b"deflate\r\nConnection: keep-alive\r\nUser-Agent: " | ||||
|         b"Sanic-Testing\r\nFOO: bar" | ||||
|     ) | ||||
|     assert b"Host: example.com" in request.raw_headers | ||||
|     assert b"Accept: */*" in request.raw_headers | ||||
|     assert b"Accept-Encoding: gzip, deflate" in request.raw_headers | ||||
|     assert b"Connection: keep-alive" in request.raw_headers | ||||
|     assert b"User-Agent: Sanic-Testing" in request.raw_headers | ||||
|     assert b"FOO: bar" in request.raw_headers | ||||
|  | ||||
|  | ||||
| def test_request_line(app): | ||||
|   | ||||
| @@ -58,6 +58,36 @@ def write_app(filename, **runargs): | ||||
|     return text | ||||
|  | ||||
|  | ||||
| def write_listener_app(filename, **runargs): | ||||
|     start_text = secrets.token_urlsafe() | ||||
|     stop_text = secrets.token_urlsafe() | ||||
|     with open(filename, "w") as f: | ||||
|         f.write( | ||||
|             dedent( | ||||
|                 f"""\ | ||||
|             import os | ||||
|             from sanic import Sanic | ||||
|  | ||||
|             app = Sanic(__name__) | ||||
|  | ||||
|             app.route("/")(lambda x: x) | ||||
|  | ||||
|             @app.reload_process_start | ||||
|             async def reload_start(*_): | ||||
|                 print("reload_start", os.getpid(), {start_text!r}) | ||||
|  | ||||
|             @app.reload_process_stop | ||||
|             async def reload_stop(*_): | ||||
|                 print("reload_stop", os.getpid(), {stop_text!r}) | ||||
|  | ||||
|             if __name__ == "__main__": | ||||
|                 app.run(**{runargs!r}) | ||||
|             """ | ||||
|             ) | ||||
|         ) | ||||
|     return start_text, stop_text | ||||
|  | ||||
|  | ||||
| def write_json_config_app(filename, jsonfile, **runargs): | ||||
|     with open(filename, "w") as f: | ||||
|         f.write( | ||||
| @@ -92,10 +122,10 @@ def write_file(filename): | ||||
|     return text | ||||
|  | ||||
|  | ||||
| def scanner(proc): | ||||
| def scanner(proc, trigger="complete"): | ||||
|     for line in proc.stdout: | ||||
|         line = line.decode().strip() | ||||
|         if line.startswith("complete"): | ||||
|         if line.startswith(trigger): | ||||
|             yield line | ||||
|  | ||||
|  | ||||
| @@ -108,7 +138,7 @@ argv = dict( | ||||
|         "sanic", | ||||
|         "--port", | ||||
|         "42204", | ||||
|         "--debug", | ||||
|         "--auto-reload", | ||||
|         "reloader.app", | ||||
|     ], | ||||
| ) | ||||
| @@ -118,7 +148,7 @@ argv = dict( | ||||
|     "runargs, mode", | ||||
|     [ | ||||
|         (dict(port=42202, auto_reload=True), "script"), | ||||
|         (dict(port=42203, debug=True), "module"), | ||||
|         (dict(port=42203, auto_reload=True), "module"), | ||||
|         ({}, "sanic"), | ||||
|     ], | ||||
| ) | ||||
| @@ -151,7 +181,7 @@ async def test_reloader_live(runargs, mode): | ||||
|     "runargs, mode", | ||||
|     [ | ||||
|         (dict(port=42302, auto_reload=True), "script"), | ||||
|         (dict(port=42303, debug=True), "module"), | ||||
|         (dict(port=42303, auto_reload=True), "module"), | ||||
|         ({}, "sanic"), | ||||
|     ], | ||||
| ) | ||||
| @@ -183,3 +213,30 @@ async def test_reloader_live_with_dir(runargs, mode): | ||||
|             terminate(proc) | ||||
|             with suppress(TimeoutExpired): | ||||
|                 proc.wait(timeout=3) | ||||
|  | ||||
|  | ||||
| def test_reload_listeners(): | ||||
|     with TemporaryDirectory() as tmpdir: | ||||
|         filename = os.path.join(tmpdir, "reloader.py") | ||||
|         start_text, stop_text = write_listener_app( | ||||
|             filename, port=42305, auto_reload=True | ||||
|         ) | ||||
|  | ||||
|         proc = Popen( | ||||
|             argv["script"], cwd=tmpdir, stdout=PIPE, creationflags=flags | ||||
|         ) | ||||
|         try: | ||||
|             timeout = Timer(TIMER_DELAY, terminate, [proc]) | ||||
|             timeout.start() | ||||
|             # Python apparently keeps using the old source sometimes if | ||||
|             # we don't sleep before rewrite (pycache timestamp problem?) | ||||
|             sleep(1) | ||||
|             line = scanner(proc, "reload_start") | ||||
|             assert start_text in next(line) | ||||
|             line = scanner(proc, "reload_stop") | ||||
|             assert stop_text in next(line) | ||||
|         finally: | ||||
|             timeout.cancel() | ||||
|             terminate(proc) | ||||
|             with suppress(TimeoutExpired): | ||||
|                 proc.wait(timeout=3) | ||||
|   | ||||
| @@ -4,6 +4,7 @@ from uuid import UUID, uuid4 | ||||
| import pytest | ||||
|  | ||||
| from sanic import Sanic, response | ||||
| from sanic.exceptions import BadURL | ||||
| from sanic.request import Request, uuid | ||||
| from sanic.server import HttpProtocol | ||||
|  | ||||
| @@ -176,3 +177,17 @@ def test_request_accept(): | ||||
|         "text/x-dvi; q=0.8", | ||||
|         "text/plain; q=0.5", | ||||
|     ] | ||||
|  | ||||
|  | ||||
| def test_bad_url_parse(): | ||||
|     message = "Bad URL: my.redacted-domain.com:443" | ||||
|     with pytest.raises(BadURL, match=message): | ||||
|         Request( | ||||
|             b"my.redacted-domain.com:443", | ||||
|             Mock(), | ||||
|             Mock(), | ||||
|             Mock(), | ||||
|             Mock(), | ||||
|             Mock(), | ||||
|             Mock(), | ||||
|         ) | ||||
|   | ||||
| @@ -1,8 +1,6 @@ | ||||
| import asyncio | ||||
| import re | ||||
|  | ||||
| from unittest.mock import Mock | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| from sanic_routing.exceptions import ( | ||||
| @@ -256,7 +254,7 @@ def test_route_strict_slash(app): | ||||
|  | ||||
|  | ||||
| def test_route_invalid_parameter_syntax(app): | ||||
|     with pytest.raises(ValueError): | ||||
|     with pytest.raises(InvalidUsage): | ||||
|  | ||||
|         @app.get("/get/<:str>", strict_slashes=True) | ||||
|         def handler(request): | ||||
|   | ||||
| @@ -33,6 +33,14 @@ def create_listener(listener_name, in_list): | ||||
|     return _listener | ||||
|  | ||||
|  | ||||
| def create_listener_no_loop(listener_name, in_list): | ||||
|     async def _listener(app): | ||||
|         print(f"DEBUG MESSAGE FOR PYTEST for {listener_name}") | ||||
|         in_list.insert(0, app.name + listener_name) | ||||
|  | ||||
|     return _listener | ||||
|  | ||||
|  | ||||
| def start_stop_app(random_name_app, **run_kwargs): | ||||
|     def stop_on_alarm(signum, frame): | ||||
|         random_name_app.stop() | ||||
| @@ -56,6 +64,17 @@ def test_single_listener(app, listener_name): | ||||
|     assert app.name + listener_name == output.pop() | ||||
|  | ||||
|  | ||||
| @skipif_no_alarm | ||||
| @pytest.mark.parametrize("listener_name", AVAILABLE_LISTENERS) | ||||
| def test_single_listener_no_loop(app, listener_name): | ||||
|     """Test that listeners on their own work""" | ||||
|     output = [] | ||||
|     # Register listener | ||||
|     app.listener(listener_name)(create_listener_no_loop(listener_name, output)) | ||||
|     start_stop_app(app) | ||||
|     assert app.name + listener_name == output.pop() | ||||
|  | ||||
|  | ||||
| @skipif_no_alarm | ||||
| @pytest.mark.parametrize("listener_name", AVAILABLE_LISTENERS) | ||||
| def test_register_listener(app, listener_name): | ||||
| @@ -199,3 +218,16 @@ async def test_missing_startup_raises_exception(app): | ||||
|  | ||||
|     with pytest.raises(SanicException): | ||||
|         await srv.before_start() | ||||
|  | ||||
|  | ||||
| def test_reload_listeners_attached(app): | ||||
|     async def dummy(*_): | ||||
|         ... | ||||
|  | ||||
|     app.reload_process_start(dummy) | ||||
|     app.reload_process_stop(dummy) | ||||
|     app.listener("reload_process_start")(dummy) | ||||
|     app.listener("reload_process_stop")(dummy) | ||||
|  | ||||
|     assert len(app.listeners.get("reload_process_start")) == 2 | ||||
|     assert len(app.listeners.get("reload_process_stop")) == 2 | ||||
|   | ||||
| @@ -3,6 +3,7 @@ import os | ||||
| import signal | ||||
|  | ||||
| from queue import Queue | ||||
| from types import SimpleNamespace | ||||
| from unittest.mock import MagicMock | ||||
|  | ||||
| import pytest | ||||
| @@ -10,6 +11,7 @@ import pytest | ||||
| from sanic_testing.testing import HOST, PORT | ||||
|  | ||||
| from sanic.compat import ctrlc_workaround_for_windows | ||||
| from sanic.exceptions import InvalidUsage | ||||
| from sanic.response import HTTPResponse | ||||
|  | ||||
|  | ||||
| @@ -73,11 +75,12 @@ def test_windows_workaround(): | ||||
|     # Windows... | ||||
|     class MockApp: | ||||
|         def __init__(self): | ||||
|             self.is_stopping = False | ||||
|             self.state = SimpleNamespace() | ||||
|             self.state.is_stopping = False | ||||
|  | ||||
|         def stop(self): | ||||
|             assert not self.is_stopping | ||||
|             self.is_stopping = True | ||||
|             assert not self.state.is_stopping | ||||
|             self.state.is_stopping = True | ||||
|  | ||||
|         def add_task(self, func): | ||||
|             loop = asyncio.get_event_loop() | ||||
| @@ -90,11 +93,11 @@ def test_windows_workaround(): | ||||
|         if stop_first: | ||||
|             app.stop() | ||||
|             await asyncio.sleep(0.2) | ||||
|         assert app.is_stopping == stop_first | ||||
|         assert app.state.is_stopping == stop_first | ||||
|         # First Ctrl+C: should call app.stop() within 0.1 seconds | ||||
|         os.kill(os.getpid(), signal.SIGINT) | ||||
|         await asyncio.sleep(0.2) | ||||
|         assert app.is_stopping | ||||
|         assert app.state.is_stopping | ||||
|         assert app.stay_active_task.result() is None | ||||
|         # Second Ctrl+C should raise | ||||
|         with pytest.raises(KeyboardInterrupt): | ||||
| @@ -108,3 +111,17 @@ def test_windows_workaround(): | ||||
|     assert res == "OK" | ||||
|     res = loop.run_until_complete(atest(True)) | ||||
|     assert res == "OK" | ||||
|  | ||||
|  | ||||
| @pytest.mark.skipif(os.name == "nt", reason="May hang CI on py38/windows") | ||||
| def test_signals_with_invalid_invocation(app): | ||||
|     """Test if sanic register fails with invalid invocation""" | ||||
|  | ||||
|     @app.route("/hello") | ||||
|     async def hello_route(request): | ||||
|         return HTTPResponse() | ||||
|  | ||||
|     with pytest.raises( | ||||
|         InvalidUsage, match="Invalid event registration: Missing event name" | ||||
|     ): | ||||
|         app.listener(stop) | ||||
|   | ||||
| @@ -80,6 +80,18 @@ async def test_purge_tasks(app: Sanic): | ||||
|     assert len(app._task_registry) == 0 | ||||
|  | ||||
|  | ||||
| async def test_purge_tasks_with_create_task(app: Sanic): | ||||
|     app.add_task(asyncio.create_task(dummy(3)), name="dummy") | ||||
|  | ||||
|     await app.cancel_task("dummy") | ||||
|  | ||||
|     assert len(app._task_registry) == 1 | ||||
|  | ||||
|     app.purge_tasks() | ||||
|  | ||||
|     assert len(app._task_registry) == 0 | ||||
|  | ||||
|  | ||||
| def test_shutdown_tasks_on_app_stop(): | ||||
|     class TestSanic(Sanic): | ||||
|         shutdown_tasks = Mock() | ||||
|   | ||||
| @@ -2,6 +2,8 @@ import logging | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| from sanic_routing.exceptions import NotFound | ||||
|  | ||||
| from sanic.signals import RESERVED_NAMESPACES | ||||
| from sanic.touchup import TouchUp | ||||
|  | ||||
| @@ -28,3 +30,50 @@ async def test_ode_removes_dispatch_events(app, caplog, verbosity, result): | ||||
|             ) | ||||
|             in logs | ||||
|         ) is result | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("skip_it,result", ((False, True), (True, False))) | ||||
| async def test_skip_touchup(app, caplog, skip_it, result): | ||||
|     app.config.TOUCHUP = not skip_it | ||||
|     with caplog.at_level(logging.DEBUG, logger="sanic.root"): | ||||
|         app.state.verbosity = 2 | ||||
|         await app._startup() | ||||
|     assert app.signal_router.allow_fail_builtin is (not skip_it) | ||||
|     logs = caplog.record_tuples | ||||
|  | ||||
|     for signal in RESERVED_NAMESPACES["http"]: | ||||
|         assert ( | ||||
|             ( | ||||
|                 "sanic.root", | ||||
|                 logging.DEBUG, | ||||
|                 f"Disabling event: {signal}", | ||||
|             ) | ||||
|             in logs | ||||
|         ) is result | ||||
|     not_found_exceptions = 0 | ||||
|     # Skip-touchup disables NotFound exceptions on the dispatcher | ||||
|     for signal in RESERVED_NAMESPACES["http"]: | ||||
|         try: | ||||
|             await app.dispatch(event=signal, inline=True) | ||||
|         except NotFound: | ||||
|             not_found_exceptions += 1 | ||||
|     assert (not_found_exceptions > 0) is result | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("skip_it,result", ((False, True), (True, True))) | ||||
| async def test_skip_touchup_non_reserved(app, caplog, skip_it, result): | ||||
|     app.config.TOUCHUP = not skip_it | ||||
|  | ||||
|     @app.signal("foo.bar.one") | ||||
|     def sync_signal(*_): | ||||
|         ... | ||||
|  | ||||
|     await app._startup() | ||||
|     assert app.signal_router.allow_fail_builtin is (not skip_it) | ||||
|     not_found_exception = False | ||||
|     # Skip-touchup doesn't disable NotFound exceptions for user-defined signals | ||||
|     try: | ||||
|         await app.dispatch(event="foo.baz.two", inline=True) | ||||
|     except NotFound: | ||||
|         not_found_exception = True | ||||
|     assert not_found_exception is result | ||||
|   | ||||
| @@ -199,7 +199,7 @@ async def test_zero_downtime(): | ||||
|         for _ in range(40): | ||||
|             async with httpx.AsyncClient(transport=transport) as client: | ||||
|                 r = await client.get("http://localhost/sleep/0.1") | ||||
|                 assert r.status_code == 200, r.content | ||||
|                 assert r.status_code == 200, r.text | ||||
|                 assert r.text == "Slept 0.1 seconds.\n" | ||||
|  | ||||
|     def spawn(): | ||||
|   | ||||
		Reference in New Issue
	
	Block a user