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>`_ | #. `isort <https://github.com/timothycrosley/isort>`_ | ||||||
| #. `black <https://github.com/python/black>`_ | #. `black <https://github.com/python/black>`_ | ||||||
| #. `flake8 <https://github.com/PyCQA/flake8>`_ | #. `flake8 <https://github.com/PyCQA/flake8>`_ | ||||||
|  | #. `slotscheck <https://github.com/ariebovenberg/slotscheck>`_ | ||||||
|  |  | ||||||
| isort | isort | ||||||
| ***** | ***** | ||||||
| @@ -167,7 +168,13 @@ flake8 | |||||||
| #. pycodestyle | #. pycodestyle | ||||||
| #. Ned Batchelder's McCabe script | #. Ned Batchelder's McCabe script | ||||||
|  |  | ||||||
| ``isort``\ , ``black`` and ``flake8`` checks are performed during ``tox`` lint checks. | slotscheck | ||||||
|  | ********** | ||||||
|  |  | ||||||
|  | ``slotscheck`` ensures that there are no problems with ``__slots__`` | ||||||
|  | (e.g. overlaps, or missing slots in base classes). | ||||||
|  |  | ||||||
|  | ``isort``\ , ``black``\ , ``flake8`` and ``slotscheck`` checks are performed during ``tox`` lint checks. | ||||||
|  |  | ||||||
| The **easiest** way to make your code conform is to run the following before committing. | The **easiest** way to make your code conform is to run the following before committing. | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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") | ||||||
|   | |||||||
| @@ -6,5 +6,5 @@ data = "" | |||||||
| for i in range(1, 250000): | for i in range(1, 250000): | ||||||
|     data += str(i) |     data += str(i) | ||||||
|  |  | ||||||
| r = requests.post("http://0.0.0.0:8000/stream", data=data) | r = requests.post('http://0.0.0.0:8000/stream', data=data) | ||||||
| print(r.text) | print(r.text) | ||||||
|   | |||||||
| @@ -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, |     CancelledError, | ||||||
|     Task, |     Task, | ||||||
|     ensure_future, |     ensure_future, | ||||||
|     get_event_loop, |  | ||||||
|     get_running_loop, |     get_running_loop, | ||||||
|     wait_for, |     wait_for, | ||||||
| ) | ) | ||||||
| @@ -142,7 +141,6 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta): | |||||||
|         "error_handler", |         "error_handler", | ||||||
|         "go_fast", |         "go_fast", | ||||||
|         "listeners", |         "listeners", | ||||||
|         "name", |  | ||||||
|         "named_request_middleware", |         "named_request_middleware", | ||||||
|         "named_response_middleware", |         "named_response_middleware", | ||||||
|         "request_class", |         "request_class", | ||||||
| @@ -254,7 +252,13 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta): | |||||||
|                 "Loop can only be retrieved after the app has started " |                 "Loop can only be retrieved after the app has started " | ||||||
|                 "running. Not supported with `create_server` function" |                 "running. Not supported with `create_server` function" | ||||||
|             ) |             ) | ||||||
|         return get_event_loop() |         try: | ||||||
|  |             return get_running_loop() | ||||||
|  |         except RuntimeError: | ||||||
|  |             if sys.version_info > (3, 10): | ||||||
|  |                 return asyncio.get_event_loop_policy().get_event_loop() | ||||||
|  |             else: | ||||||
|  |                 return asyncio.get_event_loop() | ||||||
|  |  | ||||||
|     # -------------------------------------------------------------------- # |     # -------------------------------------------------------------------- # | ||||||
|     # Registration |     # Registration | ||||||
| @@ -1132,7 +1136,10 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta): | |||||||
|     async def _listener( |     async def _listener( | ||||||
|         app: Sanic, loop: AbstractEventLoop, listener: ListenerType |         app: Sanic, loop: AbstractEventLoop, listener: ListenerType | ||||||
|     ): |     ): | ||||||
|         maybe_coro = listener(app, loop) |         try: | ||||||
|  |             maybe_coro = listener(app)  # type: ignore | ||||||
|  |         except TypeError: | ||||||
|  |             maybe_coro = listener(app, loop)  # type: ignore | ||||||
|         if maybe_coro and isawaitable(maybe_coro): |         if maybe_coro and isawaitable(maybe_coro): | ||||||
|             await maybe_coro |             await maybe_coro | ||||||
|  |  | ||||||
| @@ -1268,10 +1275,9 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta): | |||||||
|                 ... |                 ... | ||||||
|  |  | ||||||
|     def purge_tasks(self): |     def purge_tasks(self): | ||||||
|         for task in self.tasks: |         for key, task in self._task_registry.items(): | ||||||
|             if task.done() or task.cancelled(): |             if task.done() or task.cancelled(): | ||||||
|                 name = task.get_name() |                 self._task_registry[key] = None | ||||||
|                 self._task_registry[name] = None |  | ||||||
|  |  | ||||||
|         self._task_registry = { |         self._task_registry = { | ||||||
|             k: v for k, v in self._task_registry.items() if v is not None |             k: v for k, v in self._task_registry.items() if v is not None | ||||||
| @@ -1510,7 +1516,8 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta): | |||||||
|             if not Sanic.test_mode: |             if not Sanic.test_mode: | ||||||
|                 raise e |                 raise e | ||||||
|  |  | ||||||
|     def signalize(self): |     def signalize(self, allow_fail_builtin=True): | ||||||
|  |         self.signal_router.allow_fail_builtin = allow_fail_builtin | ||||||
|         try: |         try: | ||||||
|             self.signal_router.finalize() |             self.signal_router.finalize() | ||||||
|         except FinalizationError as e: |         except FinalizationError as e: | ||||||
| @@ -1525,8 +1532,11 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta): | |||||||
|         if hasattr(self, "_ext"): |         if hasattr(self, "_ext"): | ||||||
|             self.ext._display() |             self.ext._display() | ||||||
|  |  | ||||||
|  |         if self.state.is_debug: | ||||||
|  |             self.config.TOUCHUP = False | ||||||
|  |  | ||||||
|         # Setup routers |         # Setup routers | ||||||
|         self.signalize() |         self.signalize(self.config.TOUCHUP) | ||||||
|         self.finalize() |         self.finalize() | ||||||
|  |  | ||||||
|         # TODO: Replace in v22.6 to check against apps in app registry |         # TODO: Replace in v22.6 to check against apps in app registry | ||||||
| @@ -1546,6 +1556,7 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta): | |||||||
|             # TODO: |             # TODO: | ||||||
|             # - Raise warning if secondary apps have error handler config |             # - Raise warning if secondary apps have error handler config | ||||||
|             ErrorHandler.finalize(self.error_handler, config=self.config) |             ErrorHandler.finalize(self.error_handler, config=self.config) | ||||||
|  |             if self.config.TOUCHUP: | ||||||
|                 TouchUp.run(self) |                 TouchUp.run(self) | ||||||
|  |  | ||||||
|         self.state.is_started = True |         self.state.is_started = True | ||||||
|   | |||||||
| @@ -68,6 +68,13 @@ Or, a path to a directory to run as a simple HTTP server: | |||||||
|         legacy_version = len(sys.argv) == 2 and sys.argv[-1] == "-v" |         legacy_version = len(sys.argv) == 2 and sys.argv[-1] == "-v" | ||||||
|         parse_args = ["--version"] if legacy_version else None |         parse_args = ["--version"] if legacy_version else None | ||||||
|  |  | ||||||
|  |         if not parse_args: | ||||||
|  |             parsed, unknown = self.parser.parse_known_args() | ||||||
|  |             if unknown and parsed.factory: | ||||||
|  |                 for arg in unknown: | ||||||
|  |                     if arg.startswith("--"): | ||||||
|  |                         self.parser.add_argument(arg.split("=")[0]) | ||||||
|  |  | ||||||
|         self.args = self.parser.parse_args(args=parse_args) |         self.args = self.parser.parse_args(args=parse_args) | ||||||
|         self._precheck() |         self._precheck() | ||||||
|  |  | ||||||
| @@ -113,6 +120,14 @@ Or, a path to a directory to run as a simple HTTP server: | |||||||
|                 delimiter = ":" if ":" in self.args.module else "." |                 delimiter = ":" if ":" in self.args.module else "." | ||||||
|                 module_name, app_name = self.args.module.rsplit(delimiter, 1) |                 module_name, app_name = self.args.module.rsplit(delimiter, 1) | ||||||
|  |  | ||||||
|  |                 if module_name == "" and os.path.isdir(self.args.module): | ||||||
|  |                     raise ValueError( | ||||||
|  |                         "App not found.\n" | ||||||
|  |                         "   Please use --simple if you are passing a " | ||||||
|  |                         "directory to sanic.\n" | ||||||
|  |                         f"   eg. sanic {self.args.module} --simple" | ||||||
|  |                     ) | ||||||
|  |  | ||||||
|                 if app_name.endswith("()"): |                 if app_name.endswith("()"): | ||||||
|                     self.args.factory = True |                     self.args.factory = True | ||||||
|                     app_name = app_name[:-2] |                     app_name = app_name[:-2] | ||||||
| @@ -120,14 +135,26 @@ Or, a path to a directory to run as a simple HTTP server: | |||||||
|                 module = import_module(module_name) |                 module = import_module(module_name) | ||||||
|                 app = getattr(module, app_name, None) |                 app = getattr(module, app_name, None) | ||||||
|                 if self.args.factory: |                 if self.args.factory: | ||||||
|  |                     try: | ||||||
|  |                         app = app(self.args) | ||||||
|  |                     except TypeError: | ||||||
|                         app = app() |                         app = app() | ||||||
|  |  | ||||||
|                 app_type_name = type(app).__name__ |                 app_type_name = type(app).__name__ | ||||||
|  |  | ||||||
|                 if not isinstance(app, Sanic): |                 if not isinstance(app, Sanic): | ||||||
|  |                     if callable(app): | ||||||
|  |                         solution = f"sanic {self.args.module} --factory" | ||||||
|  |                         raise ValueError( | ||||||
|  |                             "Module is not a Sanic app, it is a" | ||||||
|  |                             f"{app_type_name}\n" | ||||||
|  |                             "  If this callable returns a" | ||||||
|  |                             f"Sanic instance try: \n{solution}" | ||||||
|  |                         ) | ||||||
|  |  | ||||||
|                     raise ValueError( |                     raise ValueError( | ||||||
|                         f"Module is not a Sanic app, it is a {app_type_name}\n" |                         f"Module is not a Sanic app, it is a {app_type_name}\n" | ||||||
|                         f"  Perhaps you meant {self.args.module}.app?" |                         f"  Perhaps you meant {self.args.module}:app?" | ||||||
|                     ) |                     ) | ||||||
|         except ImportError as e: |         except ImportError as e: | ||||||
|             if module_name.startswith(e.name): |             if module_name.startswith(e.name): | ||||||
|   | |||||||
| @@ -72,7 +72,7 @@ def ctrlc_workaround_for_windows(app): | |||||||
|         """Asyncio wakeups to allow receiving SIGINT in Python""" |         """Asyncio wakeups to allow receiving SIGINT in Python""" | ||||||
|         while not die: |         while not die: | ||||||
|             # If someone else stopped the app, just exit |             # If someone else stopped the app, just exit | ||||||
|             if app.is_stopping: |             if app.state.is_stopping: | ||||||
|                 return |                 return | ||||||
|             # Windows Python blocks signal handlers while the event loop is |             # Windows Python blocks signal handlers while the event loop is | ||||||
|             # waiting for I/O. Frequent wakeups keep interrupts flowing. |             # waiting for I/O. Frequent wakeups keep interrupts flowing. | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -78,7 +78,7 @@ class ErrorHandler: | |||||||
|     @classmethod |     @classmethod | ||||||
|     def _get_fallback_value(cls, error_handler: ErrorHandler, config: Config): |     def _get_fallback_value(cls, error_handler: ErrorHandler, config: Config): | ||||||
|         if error_handler._fallback is not _default: |         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 |                 return error_handler.fallback | ||||||
|  |  | ||||||
|             error_logger.warning( |             error_logger.warning( | ||||||
|   | |||||||
| @@ -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]: | ||||||
|   | |||||||
| @@ -16,9 +16,9 @@ class MiddlewareMixin(metaclass=SanicMeta): | |||||||
|         self, middleware_or_request, attach_to="request", apply=True |         self, middleware_or_request, attach_to="request", apply=True | ||||||
|     ): |     ): | ||||||
|         """ |         """ | ||||||
|         Decorate and register middleware to be called before a request. |         Decorate and register middleware to be called before a request | ||||||
|         Can either be called as *@app.middleware* or |         is handled or after a response is created. Can either be called as | ||||||
|         *@app.middleware('request')* |         *@app.middleware* or *@app.middleware('request')*. | ||||||
|  |  | ||||||
|         `See user guide re: middleware |         `See user guide re: middleware | ||||||
|         <https://sanicframework.org/guide/basics/middleware.html>`__ |         <https://sanicframework.org/guide/basics/middleware.html>`__ | ||||||
| @@ -47,12 +47,25 @@ class MiddlewareMixin(metaclass=SanicMeta): | |||||||
|             ) |             ) | ||||||
|  |  | ||||||
|     def on_request(self, middleware=None): |     def on_request(self, middleware=None): | ||||||
|  |         """Register a middleware to be called before a request is handled. | ||||||
|  |  | ||||||
|  |         This is the same as *@app.middleware('request')*. | ||||||
|  |  | ||||||
|  |         :param: middleware: A callable that takes in request. | ||||||
|  |         """ | ||||||
|         if callable(middleware): |         if callable(middleware): | ||||||
|             return self.middleware(middleware, "request") |             return self.middleware(middleware, "request") | ||||||
|         else: |         else: | ||||||
|             return partial(self.middleware, attach_to="request") |             return partial(self.middleware, attach_to="request") | ||||||
|  |  | ||||||
|     def on_response(self, middleware=None): |     def on_response(self, middleware=None): | ||||||
|  |         """Register a middleware to be called after a response is created. | ||||||
|  |  | ||||||
|  |         This is the same as *@app.middleware('response')*. | ||||||
|  |  | ||||||
|  |         :param: middleware: | ||||||
|  |             A callable that takes in a request and its response. | ||||||
|  |         """ | ||||||
|         if callable(middleware): |         if callable(middleware): | ||||||
|             return self.middleware(middleware, "response") |             return self.middleware(middleware, "response") | ||||||
|         else: |         else: | ||||||
|   | |||||||
| @@ -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.") | ||||||
|   | |||||||
| @@ -50,6 +50,16 @@ class BaseHTTPResponse: | |||||||
|     The base class for all HTTP Responses |     The base class for all HTTP Responses | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|  |     __slots__ = ( | ||||||
|  |         "asgi", | ||||||
|  |         "body", | ||||||
|  |         "content_type", | ||||||
|  |         "stream", | ||||||
|  |         "status", | ||||||
|  |         "headers", | ||||||
|  |         "_cookies", | ||||||
|  |     ) | ||||||
|  |  | ||||||
|     _dumps = json_dumps |     _dumps = json_dumps | ||||||
|  |  | ||||||
|     def __init__(self): |     def __init__(self): | ||||||
| @@ -156,7 +166,7 @@ class HTTPResponse(BaseHTTPResponse): | |||||||
|     :type content_type: Optional[str] |     :type content_type: Optional[str] | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     __slots__ = ("body", "status", "content_type", "headers", "_cookies") |     __slots__ = () | ||||||
|  |  | ||||||
|     def __init__( |     def __init__( | ||||||
|         self, |         self, | ||||||
|   | |||||||
| @@ -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) | ||||||
|   | |||||||
| @@ -1,26 +1,12 @@ | |||||||
| import asyncio | import asyncio | ||||||
|  |  | ||||||
|  | from distutils.util import strtobool | ||||||
| from os import getenv | from os import getenv | ||||||
|  |  | ||||||
| from sanic.compat import OS_IS_WINDOWS | from sanic.compat import OS_IS_WINDOWS | ||||||
| from sanic.log import error_logger | from sanic.log import error_logger | ||||||
|  |  | ||||||
|  |  | ||||||
| def strtobool(query: str) -> bool: |  | ||||||
|     """ |  | ||||||
|     reimplement strtobool per PEP 632 and python 3.12 deprecation |  | ||||||
|  |  | ||||||
|     True values are y, yes, t, true, on and 1; false values are n, no, f, |  | ||||||
|     false, off and 0. Raises ValueError if val is anything else. |  | ||||||
|     """ |  | ||||||
|     if query.lower() in ["y", "yes", "t", "true", "on", "1"]: |  | ||||||
|         return True |  | ||||||
|     elif query.lower() in ["n", "no", "f", "false", "off", "0"]: |  | ||||||
|         return False |  | ||||||
|     else: |  | ||||||
|         raise ValueError(f"String value {query} cannot be converted to bool") |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def try_use_uvloop() -> None: | def try_use_uvloop() -> None: | ||||||
|     """ |     """ | ||||||
|     Use uvloop instead of the default asyncio loop. |     Use uvloop instead of the default asyncio loop. | ||||||
|   | |||||||
| @@ -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: | ||||||
|   | |||||||
| @@ -11,18 +11,35 @@ from sanic.helpers import import_string | |||||||
|  |  | ||||||
|  |  | ||||||
| def str_to_bool(val: str) -> bool: | def str_to_bool(val: str) -> bool: | ||||||
|     """ |     """Takes string and tries to turn it into bool as human would do. | ||||||
|     reimplement strtobool per PEP 632 and python 3.12 deprecation |  | ||||||
|  |  | ||||||
|     True values are y, yes, t, true, on and 1; false values are n, no, f, |     If val is in case insensitive ( | ||||||
|     false, off and 0. Raises ValueError if val is anything else. |         "y", "yes", "yep", "yup", "t", | ||||||
|     """ |         "true", "on", "enable", "enabled", "1" | ||||||
|     if val.lower() in ["y", "yes", "t", "true", "on", "1"]: |     ) returns True. | ||||||
|  |     If val is in case insensitive ( | ||||||
|  |         "n", "no", "f", "false", "off", "disable", "disabled", "0" | ||||||
|  |     ) returns False. | ||||||
|  |     Else Raise ValueError.""" | ||||||
|  |  | ||||||
|  |     val = val.lower() | ||||||
|  |     if val in { | ||||||
|  |         "y", | ||||||
|  |         "yes", | ||||||
|  |         "yep", | ||||||
|  |         "yup", | ||||||
|  |         "t", | ||||||
|  |         "true", | ||||||
|  |         "on", | ||||||
|  |         "enable", | ||||||
|  |         "enabled", | ||||||
|  |         "1", | ||||||
|  |     }: | ||||||
|         return True |         return True | ||||||
|     elif val.lower() in ["n", "no", "f", "false", "off", "0"]: |     elif val in {"n", "no", "f", "false", "off", "disable", "disabled", "0"}: | ||||||
|         return False |         return False | ||||||
|     else: |     else: | ||||||
|         raise ValueError(f"String value {val} cannot be converted to bool") |         raise ValueError(f"Invalid truth value {val}") | ||||||
|  |  | ||||||
|  |  | ||||||
| def load_module_from_file_location( | def load_module_from_file_location( | ||||||
|   | |||||||
							
								
								
									
										28
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								setup.py
									
									
									
									
									
								
							| @@ -6,6 +6,8 @@ import os | |||||||
| import re | import re | ||||||
| import sys | import sys | ||||||
|  |  | ||||||
|  | from distutils.util import strtobool | ||||||
|  |  | ||||||
| from setuptools import find_packages, setup | from setuptools import find_packages, setup | ||||||
| from setuptools.command.test import test as TestCommand | from setuptools.command.test import test as TestCommand | ||||||
|  |  | ||||||
| @@ -59,7 +61,7 @@ setup_kwargs = { | |||||||
|         "Build fast. Run fast." |         "Build fast. Run fast." | ||||||
|     ), |     ), | ||||||
|     "long_description": long_description, |     "long_description": long_description, | ||||||
|     "packages": find_packages(include=[]), |     "packages": find_packages(), | ||||||
|     "package_data": {"sanic": ["py.typed"]}, |     "package_data": {"sanic": ["py.typed"]}, | ||||||
|     "platforms": "any", |     "platforms": "any", | ||||||
|     "python_requires": ">=3.7", |     "python_requires": ">=3.7", | ||||||
| @@ -82,17 +84,17 @@ ujson = "ujson>=1.35" + env_dependency | |||||||
| uvloop = "uvloop>=0.5.3" + env_dependency | uvloop = "uvloop>=0.5.3" + env_dependency | ||||||
| types_ujson = "types-ujson" + env_dependency | types_ujson = "types-ujson" + env_dependency | ||||||
| requirements = [ | requirements = [ | ||||||
|     "sanic-routing~=0.7", |     "sanic-routing>=22.3.0,<22.6.0", | ||||||
|     "httptools>=0.0.10", |     "httptools>=0.0.10", | ||||||
|     uvloop, |     uvloop, | ||||||
|     ujson, |     ujson, | ||||||
|     "aiofiles>=0.6.0", |     "aiofiles>=0.6.0", | ||||||
|     "websockets>=10.0", |     "websockets>=10.0", | ||||||
|     "multidict>=5.0,<6.0", |     "multidict>=5.0,<7.0", | ||||||
| ] | ] | ||||||
|  |  | ||||||
| tests_require = [ | tests_require = [ | ||||||
|     "sanic-testing>=0.7.0", |     "sanic-testing>=22.3.0", | ||||||
|     "pytest==6.2.5", |     "pytest==6.2.5", | ||||||
|     "coverage==5.3", |     "coverage==5.3", | ||||||
|     "gunicorn==20.0.4", |     "gunicorn==20.0.4", | ||||||
| @@ -110,6 +112,7 @@ tests_require = [ | |||||||
|     "docutils", |     "docutils", | ||||||
|     "pygments", |     "pygments", | ||||||
|     "uvicorn<0.15.0", |     "uvicorn<0.15.0", | ||||||
|  |     "slotscheck>=0.8.0,<1", | ||||||
|     types_ujson, |     types_ujson, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| @@ -130,23 +133,6 @@ dev_require = tests_require + [ | |||||||
|  |  | ||||||
| all_require = list(set(dev_require + docs_require)) | all_require = list(set(dev_require + docs_require)) | ||||||
|  |  | ||||||
| # trying to self-refernce this from within sanic prior to install is |  | ||||||
| # problematic |  | ||||||
| def strtobool(val: str) -> bool: |  | ||||||
|     """ |  | ||||||
|     reimplement strtobool per PEP 632 and python 3.12 deprecation |  | ||||||
|  |  | ||||||
|     True values are y, yes, t, true, on and 1; false values are n, no, f, |  | ||||||
|     false, off and 0. Raises ValueError if val is anything else. |  | ||||||
|     """ |  | ||||||
|     if val.lower() in ["y", "yes", "t", "true", "on", "1"]: |  | ||||||
|         return True |  | ||||||
|     elif val.lower() in ["n", "no", "f", "false", "off", "0"]: |  | ||||||
|         return False |  | ||||||
|     else: |  | ||||||
|         raise ValueError(f"String value {val} cannot be converted to bool") |  | ||||||
|  |  | ||||||
|  |  | ||||||
| if strtobool(os.environ.get("SANIC_NO_UJSON", "no")): | if strtobool(os.environ.get("SANIC_NO_UJSON", "no")): | ||||||
|     print("Installing without uJSON") |     print("Installing without uJSON") | ||||||
|     requirements.remove(ujson) |     requirements.remove(ujson) | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ import os | |||||||
| import signal | import signal | ||||||
|  |  | ||||||
| from queue import Queue | from queue import Queue | ||||||
|  | from types import SimpleNamespace | ||||||
| from unittest.mock import MagicMock | from unittest.mock import MagicMock | ||||||
|  |  | ||||||
| import pytest | import pytest | ||||||
| @@ -10,6 +11,7 @@ import pytest | |||||||
| from sanic_testing.testing import HOST, PORT | from sanic_testing.testing import HOST, PORT | ||||||
|  |  | ||||||
| from sanic.compat import ctrlc_workaround_for_windows | from sanic.compat import ctrlc_workaround_for_windows | ||||||
|  | from sanic.exceptions import InvalidUsage | ||||||
| from sanic.response import HTTPResponse | from sanic.response import HTTPResponse | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -73,11 +75,12 @@ def test_windows_workaround(): | |||||||
|     # Windows... |     # Windows... | ||||||
|     class MockApp: |     class MockApp: | ||||||
|         def __init__(self): |         def __init__(self): | ||||||
|             self.is_stopping = False |             self.state = SimpleNamespace() | ||||||
|  |             self.state.is_stopping = False | ||||||
|  |  | ||||||
|         def stop(self): |         def stop(self): | ||||||
|             assert not self.is_stopping |             assert not self.state.is_stopping | ||||||
|             self.is_stopping = True |             self.state.is_stopping = True | ||||||
|  |  | ||||||
|         def add_task(self, func): |         def add_task(self, func): | ||||||
|             loop = asyncio.get_event_loop() |             loop = asyncio.get_event_loop() | ||||||
| @@ -90,11 +93,11 @@ def test_windows_workaround(): | |||||||
|         if stop_first: |         if stop_first: | ||||||
|             app.stop() |             app.stop() | ||||||
|             await asyncio.sleep(0.2) |             await asyncio.sleep(0.2) | ||||||
|         assert app.is_stopping == stop_first |         assert app.state.is_stopping == stop_first | ||||||
|         # First Ctrl+C: should call app.stop() within 0.1 seconds |         # First Ctrl+C: should call app.stop() within 0.1 seconds | ||||||
|         os.kill(os.getpid(), signal.SIGINT) |         os.kill(os.getpid(), signal.SIGINT) | ||||||
|         await asyncio.sleep(0.2) |         await asyncio.sleep(0.2) | ||||||
|         assert app.is_stopping |         assert app.state.is_stopping | ||||||
|         assert app.stay_active_task.result() is None |         assert app.stay_active_task.result() is None | ||||||
|         # Second Ctrl+C should raise |         # Second Ctrl+C should raise | ||||||
|         with pytest.raises(KeyboardInterrupt): |         with pytest.raises(KeyboardInterrupt): | ||||||
| @@ -108,3 +111,17 @@ def test_windows_workaround(): | |||||||
|     assert res == "OK" |     assert res == "OK" | ||||||
|     res = loop.run_until_complete(atest(True)) |     res = loop.run_until_complete(atest(True)) | ||||||
|     assert res == "OK" |     assert res == "OK" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.skipif(os.name == "nt", reason="May hang CI on py38/windows") | ||||||
|  | def test_signals_with_invalid_invocation(app): | ||||||
|  |     """Test if sanic register fails with invalid invocation""" | ||||||
|  |  | ||||||
|  |     @app.route("/hello") | ||||||
|  |     async def hello_route(request): | ||||||
|  |         return HTTPResponse() | ||||||
|  |  | ||||||
|  |     with pytest.raises( | ||||||
|  |         InvalidUsage, match="Invalid event registration: Missing event name" | ||||||
|  |     ): | ||||||
|  |         app.listener(stop) | ||||||
|   | |||||||
| @@ -80,6 +80,18 @@ async def test_purge_tasks(app: Sanic): | |||||||
|     assert len(app._task_registry) == 0 |     assert len(app._task_registry) == 0 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def test_purge_tasks_with_create_task(app: Sanic): | ||||||
|  |     app.add_task(asyncio.create_task(dummy(3)), name="dummy") | ||||||
|  |  | ||||||
|  |     await app.cancel_task("dummy") | ||||||
|  |  | ||||||
|  |     assert len(app._task_registry) == 1 | ||||||
|  |  | ||||||
|  |     app.purge_tasks() | ||||||
|  |  | ||||||
|  |     assert len(app._task_registry) == 0 | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_shutdown_tasks_on_app_stop(): | def test_shutdown_tasks_on_app_stop(): | ||||||
|     class TestSanic(Sanic): |     class TestSanic(Sanic): | ||||||
|         shutdown_tasks = Mock() |         shutdown_tasks = Mock() | ||||||
|   | |||||||
| @@ -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(): | ||||||
|   | |||||||
| @@ -6,7 +6,6 @@ import pytest | |||||||
|  |  | ||||||
| from sanic.exceptions import LoadFileException | from sanic.exceptions import LoadFileException | ||||||
| from sanic.utils import load_module_from_file_location | from sanic.utils import load_module_from_file_location | ||||||
| from sanic.utils import str_to_bool as strtobool |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.parametrize( | @pytest.mark.parametrize( | ||||||
| @@ -49,20 +48,3 @@ def test_load_module_from_file_location_using_env(): | |||||||
|     module = load_module_from_file_location(location) |     module = load_module_from_file_location(location) | ||||||
|  |  | ||||||
|     assert isinstance(module, ModuleType) |     assert isinstance(module, ModuleType) | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.parametrize( |  | ||||||
|     "valid,values", |  | ||||||
|     ( |  | ||||||
|         (True, ["y", "yes", "t", "true", "on", "1", "Y", "yEs", "True"]), |  | ||||||
|         (False, ["n", "no", "f", "false", "off", "0", "N", "No", "False"]), |  | ||||||
|         (None, ["yyy", "foo"]), |  | ||||||
|     ), |  | ||||||
| ) |  | ||||||
| def test_strtobool(valid, values): |  | ||||||
|     for value in values: |  | ||||||
|         if valid is None: |  | ||||||
|             with pytest.raises(ValueError): |  | ||||||
|                 strtobool(value) |  | ||||||
|         else: |  | ||||||
|             assert strtobool(value) is valid |  | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								tox.ini
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								tox.ini
									
									
									
									
									
								
							| @@ -21,6 +21,7 @@ commands = | |||||||
|     flake8 sanic |     flake8 sanic | ||||||
|     black --config ./.black.toml --check --verbose sanic/ |     black --config ./.black.toml --check --verbose sanic/ | ||||||
|     isort --check-only sanic --profile=black |     isort --check-only sanic --profile=black | ||||||
|  |     slotscheck --verbose -m sanic | ||||||
|  |  | ||||||
| [testenv:type-checking] | [testenv:type-checking] | ||||||
| commands = | commands = | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user