Compare commits
	
		
			17 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 7b96d633db | ||
|   | 761eef7d96 | ||
|   | 83511a0ba7 | ||
|   | cf9ccdae47 | ||
|   | d81096fdc0 | ||
|   | 6c8e20a859 | ||
|   | 6239fa4f56 | ||
|   | 1b324ae981 | ||
|   | bedf68a9b2 | ||
|   | 496e87e4ba | ||
|   | fa4f85eb32 | ||
|   | 1b1dfedc74 | ||
|   | 230941ff4f | ||
|   | 4658e0f2f3 | ||
|   | 7c3c532dae | ||
|   | 6aaccd1e8b | ||
|   | aacbd022cf | 
| @@ -71,14 +71,14 @@ matrix: | |||||||
|       name: "Python nightly with Extensions" |       name: "Python nightly with Extensions" | ||||||
|     - env: TOX_ENV=pyNightly-no-ext |     - env: TOX_ENV=pyNightly-no-ext | ||||||
|       python: 'nightly' |       python: 'nightly' | ||||||
|       name: "Python nightly Extensions" |       name: "Python nightly without Extensions" | ||||||
|   allow_failures: |   allow_failures: | ||||||
|     - env: TOX_ENV=pyNightly |     - env: TOX_ENV=pyNightly | ||||||
|       python: 'nightly' |       python: 'nightly' | ||||||
|       name: "Python nightly with Extensions" |       name: "Python nightly with Extensions" | ||||||
|     - env: TOX_ENV=pyNightly-no-ext |     - env: TOX_ENV=pyNightly-no-ext | ||||||
|       python: 'nightly' |       python: 'nightly' | ||||||
|       name: "Python nightly Extensions" |       name: "Python nightly without Extensions" | ||||||
| install: | install: | ||||||
|   - pip install -U tox |   - pip install -U tox | ||||||
|   - pip install codecov |   - pip install codecov | ||||||
|   | |||||||
							
								
								
									
										131
									
								
								CHANGELOG.rst
									
									
									
									
									
								
							
							
						
						
									
										131
									
								
								CHANGELOG.rst
									
									
									
									
									
								
							| @@ -1,3 +1,134 @@ | |||||||
|  | Version 20.3.0 | ||||||
|  | =============== | ||||||
|  |  | ||||||
|  | Features | ||||||
|  | ******** | ||||||
|  |  | ||||||
|  |   *  | ||||||
|  |     `#1762 <https://github.com/huge-success/sanic/pull/1762>`_ | ||||||
|  |     Add ``srv.start_serving()`` and ``srv.serve_forever()`` to ``AsyncioServer`` | ||||||
|  |  | ||||||
|  |   *  | ||||||
|  |     `#1767 <https://github.com/huge-success/sanic/pull/1767>`_ | ||||||
|  |     Make Sanic usable on ``hypercorn -k trio myweb.app`` | ||||||
|  |  | ||||||
|  |   *  | ||||||
|  |     `#1768 <https://github.com/huge-success/sanic/pull/1768>`_ | ||||||
|  |     No tracebacks on normal errors and prettier error pages | ||||||
|  |  | ||||||
|  |   *  | ||||||
|  |     `#1769 <https://github.com/huge-success/sanic/pull/1769>`_ | ||||||
|  |     Code cleanup in file responses | ||||||
|  |  | ||||||
|  |   *  | ||||||
|  |     `#1793 <https://github.com/huge-success/sanic/pull/1793>`_ and | ||||||
|  |     `#1819 <https://github.com/huge-success/sanic/pull/1819>`_  | ||||||
|  |     Upgrade ``str.format()`` to f-strings | ||||||
|  |  | ||||||
|  |   *  | ||||||
|  |     `#1798 <https://github.com/huge-success/sanic/pull/1798>`_ | ||||||
|  |     Allow multiple workers on MacOS with Python 3.8 | ||||||
|  |  | ||||||
|  |   * | ||||||
|  |     `#1820 <https://github.com/huge-success/sanic/pull/1820>`_ | ||||||
|  |     Do not set content-type and content-length headers in exceptions | ||||||
|  |  | ||||||
|  | Bugfixes | ||||||
|  | ******** | ||||||
|  |  | ||||||
|  |   *  | ||||||
|  |     `#1748 <https://github.com/huge-success/sanic/pull/1748>`_ | ||||||
|  |     Remove loop argument in ``asyncio.Event`` in Python 3.8 | ||||||
|  |  | ||||||
|  |   *  | ||||||
|  |     `#1764 <https://github.com/huge-success/sanic/pull/1764>`_ | ||||||
|  |     Allow route decorators to stack up again | ||||||
|  |  | ||||||
|  |   *  | ||||||
|  |     `#1789 <https://github.com/huge-success/sanic/pull/1789>`_ | ||||||
|  |     Fix tests using hosts yielding incorrect ``url_for`` | ||||||
|  |  | ||||||
|  |   *  | ||||||
|  |     `#1808 <https://github.com/huge-success/sanic/pull/1808>`_ | ||||||
|  |      Fix Ctrl+C and tests on Windows | ||||||
|  |  | ||||||
|  | Deprecations and Removals | ||||||
|  | ************************* | ||||||
|  |  | ||||||
|  |   * | ||||||
|  |     `#1800 <https://github.com/huge-success/sanic/pull/1800>`_ | ||||||
|  |     Begin deprecation in way of first-class streaming, removal of ``body_init``, ``body_push``, and ``body_finish`` | ||||||
|  |  | ||||||
|  |   * | ||||||
|  |     `#1801 <https://github.com/huge-success/sanic/pull/1801>`_ | ||||||
|  |     Complete deprecation from `#1666 <https://github.com/huge-success/sanic/pull/1666>`_ of dictionary context on ``request`` objects. | ||||||
|  |      | ||||||
|  |   * | ||||||
|  |     `#1807 <https://github.com/huge-success/sanic/pull/1807>`_ | ||||||
|  |     Remove server config args that can be read directly from app | ||||||
|  |  | ||||||
|  |   * | ||||||
|  |     `#1818 <https://github.com/huge-success/sanic/pull/1818>`_ | ||||||
|  |     Complete deprecation of ``app.remove_route`` and ``request.raw_args`` | ||||||
|  |  | ||||||
|  | Dependencies | ||||||
|  | ************ | ||||||
|  |  | ||||||
|  |   * | ||||||
|  |     `#1794 <https://github.com/huge-success/sanic/pull/1794>`_ | ||||||
|  |     Bump ``httpx`` to 0.11.1 | ||||||
|  |  | ||||||
|  |   * | ||||||
|  |     `#1806 <https://github.com/huge-success/sanic/pull/1806>`_ | ||||||
|  |     Import ``ASGIDispatch`` from top-level ``httpx`` (from third-party deprecation) | ||||||
|  |  | ||||||
|  | Developer infrastructure | ||||||
|  | ************************ | ||||||
|  |  | ||||||
|  |   *  | ||||||
|  |     `#1833 <https://github.com/huge-success/sanic/pull/1833>`_ | ||||||
|  |     Resolve broken documentation builds | ||||||
|  |  | ||||||
|  | Improved Documentation | ||||||
|  | ********************** | ||||||
|  |  | ||||||
|  |   *  | ||||||
|  |     `#1755 <https://github.com/huge-success/sanic/pull/1755>`_ | ||||||
|  |     Usage of ``response.empty()`` | ||||||
|  |  | ||||||
|  |   *  | ||||||
|  |     `#1778 <https://github.com/huge-success/sanic/pull/1778>`_ | ||||||
|  |     Update README | ||||||
|  |  | ||||||
|  |   *  | ||||||
|  |     `#1783 <https://github.com/huge-success/sanic/pull/1783>`_ | ||||||
|  |     Fix typo | ||||||
|  |  | ||||||
|  |   * | ||||||
|  |     `#1784 <https://github.com/huge-success/sanic/pull/1784>`_ | ||||||
|  |     Corrected changelog for docs move of MD to RST (`#1691 <https://github.com/huge-success/sanic/pull/1691>`_) | ||||||
|  |  | ||||||
|  |   * | ||||||
|  |     `#1803 <https://github.com/huge-success/sanic/pull/1803>`_ | ||||||
|  |     Update config docs to match DEFAULT_CONFIG | ||||||
|  |  | ||||||
|  |   * | ||||||
|  |     `#1814 <https://github.com/huge-success/sanic/pull/1814>`_ | ||||||
|  |     Update getting_started.rst | ||||||
|  |  | ||||||
|  |   * | ||||||
|  |     `#1821 <https://github.com/huge-success/sanic/pull/1821>`_ | ||||||
|  |     Update to deployment | ||||||
|  |  | ||||||
|  |   * | ||||||
|  |     `#1822 <https://github.com/huge-success/sanic/pull/1822>`_ | ||||||
|  |     Update docs with changes done in 20.3 | ||||||
|  |  | ||||||
|  |   * | ||||||
|  |     `#1834 <https://github.com/huge-success/sanic/pull/1834>`_ | ||||||
|  |     Order of listeners | ||||||
|  |      | ||||||
|  |  | ||||||
| Version 19.12.0 | Version 19.12.0 | ||||||
| =============== | =============== | ||||||
|  |  | ||||||
|   | |||||||
| @@ -50,7 +50,9 @@ If you like using command line arguments, you can launch a Sanic webserver by | |||||||
| executing the module. For example, if you initialized Sanic as `app` in a file | executing the module. For example, if you initialized Sanic as `app` in a file | ||||||
| named `server.py`, you could run the server like so: | named `server.py`, you could run the server like so: | ||||||
|  |  | ||||||
| .. python -m sanic server.app --host=0.0.0.0 --port=1337 --workers=4 | :: | ||||||
|  |  | ||||||
|  |     python -m sanic server.app --host=0.0.0.0 --port=1337 --workers=4 | ||||||
|  |  | ||||||
| With this way of running sanic, it is not necessary to invoke `app.run` in your | With this way of running sanic, it is not necessary to invoke `app.run` in your | ||||||
| Python file. If you do, make sure you wrap it so that it only executes when | Python file. If you do, make sure you wrap it so that it only executes when | ||||||
|   | |||||||
| @@ -14,8 +14,8 @@ There are two types of middleware: request and response. Both are declared | |||||||
| using the `@app.middleware` decorator, with the decorator's parameter being a | using the `@app.middleware` decorator, with the decorator's parameter being a | ||||||
| string representing its type: `'request'` or `'response'`. | string representing its type: `'request'` or `'response'`. | ||||||
|  |  | ||||||
| * Request middleware receives only the `request` as argument. | * Request middleware receives only the `request` as an argument and are executed in the order they were added. | ||||||
| * Response middleware receives both the `request` and `response`. | * Response middleware receives both the `request` and `response` and are executed in *reverse* order. | ||||||
|  |  | ||||||
| The simplest middleware doesn't modify the request or response at all: | The simplest middleware doesn't modify the request or response at all: | ||||||
|  |  | ||||||
| @@ -64,12 +64,12 @@ this. | |||||||
|  |  | ||||||
|     app.run(host="0.0.0.0", port=8000) |     app.run(host="0.0.0.0", port=8000) | ||||||
|  |  | ||||||
| The three middlewares are executed in order: | The three middlewares are executed in the following order: | ||||||
|  |  | ||||||
| 1. The first request middleware **add_key** adds a new key `foo` into request context. | 1. The first request middleware **add_key** adds a new key `foo` into request context. | ||||||
| 2. Request is routed to handler **index**, which gets the key from context and returns a text response. | 2. Request is routed to handler **index**, which gets the key from context and returns a text response. | ||||||
| 3. The first response middleware **custom_banner** changes the HTTP response header *Server* to say *Fake-Server* | 3. The second response middleware **prevent_xss** adds the HTTP header for preventing Cross-Site-Scripting (XSS) attacks. | ||||||
| 4. The second response middleware **prevent_xss** adds the HTTP header for preventing Cross-Site-Scripting (XSS) attacks. | 4. The first response middleware **custom_banner** changes the HTTP response header *Server* to say *Fake-Server* | ||||||
|  |  | ||||||
| Responding early | Responding early | ||||||
| ---------------- | ---------------- | ||||||
|   | |||||||
| @@ -1,3 +1,6 @@ | |||||||
|  | import os | ||||||
|  | import sys | ||||||
|  |  | ||||||
| from argparse import ArgumentParser | from argparse import ArgumentParser | ||||||
| from importlib import import_module | from importlib import import_module | ||||||
| from typing import Any, Dict, Optional | from typing import Any, Dict, Optional | ||||||
| @@ -6,7 +9,7 @@ from sanic.app import Sanic | |||||||
| from sanic.log import logger | from sanic.log import logger | ||||||
|  |  | ||||||
|  |  | ||||||
| if __name__ == "__main__": | def main(): | ||||||
|     parser = ArgumentParser(prog="sanic") |     parser = ArgumentParser(prog="sanic") | ||||||
|     parser.add_argument("--host", dest="host", type=str, default="127.0.0.1") |     parser.add_argument("--host", dest="host", type=str, default="127.0.0.1") | ||||||
|     parser.add_argument("--port", dest="port", type=int, default=8000) |     parser.add_argument("--port", dest="port", type=int, default=8000) | ||||||
| @@ -22,6 +25,10 @@ if __name__ == "__main__": | |||||||
|     args = parser.parse_args() |     args = parser.parse_args() | ||||||
|  |  | ||||||
|     try: |     try: | ||||||
|  |         module_path = os.path.abspath(os.getcwd()) | ||||||
|  |         if module_path not in sys.path: | ||||||
|  |             sys.path.append(module_path) | ||||||
|  |  | ||||||
|         module_parts = args.module.split(".") |         module_parts = args.module.split(".") | ||||||
|         module_name = ".".join(module_parts[:-1]) |         module_name = ".".join(module_parts[:-1]) | ||||||
|         app_name = module_parts[-1] |         app_name = module_parts[-1] | ||||||
| @@ -58,3 +65,7 @@ if __name__ == "__main__": | |||||||
|         ) |         ) | ||||||
|     except ValueError: |     except ValueError: | ||||||
|         logger.exception("Failed to run app") |         logger.exception("Failed to run app") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     main() | ||||||
|   | |||||||
| @@ -1 +1 @@ | |||||||
| __version__ = "20.3.0" | __version__ = "20.6.0" | ||||||
|   | |||||||
							
								
								
									
										160
									
								
								sanic/app.py
									
									
									
									
									
								
							
							
						
						
									
										160
									
								
								sanic/app.py
									
									
									
									
									
								
							| @@ -117,24 +117,12 @@ class Sanic: | |||||||
|         :param task: future, couroutine or awaitable |         :param task: future, couroutine or awaitable | ||||||
|         """ |         """ | ||||||
|         try: |         try: | ||||||
|             if callable(task): |             loop = self.loop  # Will raise SanicError if loop is not started | ||||||
|                 try: |             self._loop_add_task(task, self, loop) | ||||||
|                     self.loop.create_task(task(self)) |  | ||||||
|                 except TypeError: |  | ||||||
|                     self.loop.create_task(task()) |  | ||||||
|             else: |  | ||||||
|                 self.loop.create_task(task) |  | ||||||
|         except SanicException: |         except SanicException: | ||||||
|  |             self.listener("before_server_start")( | ||||||
|             @self.listener("before_server_start") |                 partial(self._loop_add_task, task) | ||||||
|             def run(app, loop): |             ) | ||||||
|                 if callable(task): |  | ||||||
|                     try: |  | ||||||
|                         loop.create_task(task(self)) |  | ||||||
|                     except TypeError: |  | ||||||
|                         loop.create_task(task()) |  | ||||||
|                 else: |  | ||||||
|                     loop.create_task(task) |  | ||||||
|  |  | ||||||
|     # Decorator |     # Decorator | ||||||
|     def listener(self, event): |     def listener(self, event): | ||||||
| @@ -462,7 +450,13 @@ class Sanic: | |||||||
|  |  | ||||||
|     # Decorator |     # Decorator | ||||||
|     def websocket( |     def websocket( | ||||||
|         self, uri, host=None, strict_slashes=None, subprotocols=None, name=None |         self, | ||||||
|  |         uri, | ||||||
|  |         host=None, | ||||||
|  |         strict_slashes=None, | ||||||
|  |         subprotocols=None, | ||||||
|  |         version=None, | ||||||
|  |         name=None, | ||||||
|     ): |     ): | ||||||
|         """ |         """ | ||||||
|         Decorate a function to be registered as a websocket route |         Decorate a function to be registered as a websocket route | ||||||
| @@ -493,42 +487,12 @@ class Sanic: | |||||||
|                 routes, handler = handler |                 routes, handler = handler | ||||||
|             else: |             else: | ||||||
|                 routes = [] |                 routes = [] | ||||||
|  |             websocket_handler = partial( | ||||||
|             async def websocket_handler(request, *args, **kwargs): |                 self._websocket_handler, handler, subprotocols=subprotocols | ||||||
|                 request.app = self |  | ||||||
|                 if not getattr(handler, "__blueprintname__", False): |  | ||||||
|                     request.endpoint = handler.__name__ |  | ||||||
|                 else: |  | ||||||
|                     request.endpoint = ( |  | ||||||
|                         getattr(handler, "__blueprintname__", "") |  | ||||||
|                         + handler.__name__ |  | ||||||
|             ) |             ) | ||||||
|  |             websocket_handler.__name__ = ( | ||||||
|                     pass |                 "websocket_handler_" + handler.__name__ | ||||||
|  |  | ||||||
|                 if self.asgi: |  | ||||||
|                     ws = request.transport.get_websocket_connection() |  | ||||||
|                 else: |  | ||||||
|                     protocol = request.transport.get_protocol() |  | ||||||
|                     protocol.app = self |  | ||||||
|  |  | ||||||
|                     ws = await protocol.websocket_handshake( |  | ||||||
|                         request, subprotocols |  | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|                 # schedule the application handler |  | ||||||
|                 # its future is kept in self.websocket_tasks in case it |  | ||||||
|                 # needs to be cancelled due to the server being stopped |  | ||||||
|                 fut = ensure_future(handler(request, ws, *args, **kwargs)) |  | ||||||
|                 self.websocket_tasks.add(fut) |  | ||||||
|                 try: |  | ||||||
|                     await fut |  | ||||||
|                 except (CancelledError, ConnectionClosed): |  | ||||||
|                     pass |  | ||||||
|                 finally: |  | ||||||
|                     self.websocket_tasks.remove(fut) |  | ||||||
|                 await ws.close() |  | ||||||
|  |  | ||||||
|             routes.extend( |             routes.extend( | ||||||
|                 self.router.add( |                 self.router.add( | ||||||
|                     uri=uri, |                     uri=uri, | ||||||
| @@ -536,6 +500,7 @@ class Sanic: | |||||||
|                     methods=frozenset({"GET"}), |                     methods=frozenset({"GET"}), | ||||||
|                     host=host, |                     host=host, | ||||||
|                     strict_slashes=strict_slashes, |                     strict_slashes=strict_slashes, | ||||||
|  |                     version=version, | ||||||
|                     name=name, |                     name=name, | ||||||
|                 ) |                 ) | ||||||
|             ) |             ) | ||||||
| @@ -550,6 +515,7 @@ class Sanic: | |||||||
|         host=None, |         host=None, | ||||||
|         strict_slashes=None, |         strict_slashes=None, | ||||||
|         subprotocols=None, |         subprotocols=None, | ||||||
|  |         version=None, | ||||||
|         name=None, |         name=None, | ||||||
|     ): |     ): | ||||||
|         """ |         """ | ||||||
| @@ -577,6 +543,7 @@ class Sanic: | |||||||
|             host=host, |             host=host, | ||||||
|             strict_slashes=strict_slashes, |             strict_slashes=strict_slashes, | ||||||
|             subprotocols=subprotocols, |             subprotocols=subprotocols, | ||||||
|  |             version=version, | ||||||
|             name=name, |             name=name, | ||||||
|         )(handler) |         )(handler) | ||||||
|  |  | ||||||
| @@ -589,10 +556,7 @@ class Sanic: | |||||||
|         if not self.websocket_enabled: |         if not self.websocket_enabled: | ||||||
|             # if the server is stopped, we want to cancel any ongoing |             # if the server is stopped, we want to cancel any ongoing | ||||||
|             # websocket tasks, to allow the server to exit promptly |             # websocket tasks, to allow the server to exit promptly | ||||||
|             @self.listener("before_server_stop") |             self.listener("before_server_stop")(self._cancel_websocket_tasks) | ||||||
|             def cancel_websocket_tasks(app, loop): |  | ||||||
|                 for task in self.websocket_tasks: |  | ||||||
|                     task.cancel() |  | ||||||
|  |  | ||||||
|         self.websocket_enabled = enable |         self.websocket_enabled = enable | ||||||
|  |  | ||||||
| @@ -1058,16 +1022,18 @@ class Sanic: | |||||||
|         self, |         self, | ||||||
|         host: Optional[str] = None, |         host: Optional[str] = None, | ||||||
|         port: Optional[int] = None, |         port: Optional[int] = None, | ||||||
|  |         *, | ||||||
|         debug: bool = False, |         debug: bool = False, | ||||||
|  |         auto_reload: Optional[bool] = None, | ||||||
|         ssl: Union[dict, SSLContext, None] = None, |         ssl: Union[dict, SSLContext, None] = None, | ||||||
|         sock: Optional[socket] = None, |         sock: Optional[socket] = None, | ||||||
|         workers: int = 1, |         workers: int = 1, | ||||||
|         protocol: Type[Protocol] = None, |         protocol: Optional[Type[Protocol]] = None, | ||||||
|         backlog: int = 100, |         backlog: int = 100, | ||||||
|         stop_event: Any = None, |         stop_event: Any = None, | ||||||
|         register_sys_signals: bool = True, |         register_sys_signals: bool = True, | ||||||
|         access_log: Optional[bool] = None, |         access_log: Optional[bool] = None, | ||||||
|         **kwargs: Any, |         loop: None = None, | ||||||
|     ) -> None: |     ) -> None: | ||||||
|         """Run the HTTP Server and listen until keyboard interrupt or term |         """Run the HTTP Server and listen until keyboard interrupt or term | ||||||
|         signal. On termination, drain connections before closing. |         signal. On termination, drain connections before closing. | ||||||
| @@ -1078,6 +1044,9 @@ class Sanic: | |||||||
|         :type port: int |         :type port: int | ||||||
|         :param debug: Enables debug output (slows server) |         :param debug: Enables debug output (slows server) | ||||||
|         :type debug: bool |         :type debug: bool | ||||||
|  |         :param auto_reload: Reload app whenever its source code is changed. | ||||||
|  |                             Enabled by default in debug mode. | ||||||
|  |         :type auto_relaod: bool | ||||||
|         :param ssl: SSLContext, or location of certificate and key |         :param ssl: SSLContext, or location of certificate and key | ||||||
|                     for SSL encryption of worker(s) |                     for SSL encryption of worker(s) | ||||||
|         :type ssl: SSLContext or dict |         :type ssl: SSLContext or dict | ||||||
| @@ -1099,7 +1068,7 @@ class Sanic: | |||||||
|         :type access_log: bool |         :type access_log: bool | ||||||
|         :return: Nothing |         :return: Nothing | ||||||
|         """ |         """ | ||||||
|         if "loop" in kwargs: |         if loop is not None: | ||||||
|             raise TypeError( |             raise TypeError( | ||||||
|                 "loop is not a valid argument. To use an existing loop, " |                 "loop is not a valid argument. To use an existing loop, " | ||||||
|                 "change to create_server().\nSee more: " |                 "change to create_server().\nSee more: " | ||||||
| @@ -1107,13 +1076,9 @@ class Sanic: | |||||||
|                 "#asynchronous-support" |                 "#asynchronous-support" | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|         # Default auto_reload to false |         if auto_reload or auto_reload is None and debug: | ||||||
|         auto_reload = False |             if os.environ.get("SANIC_SERVER_RUNNING") != "true": | ||||||
|         # If debug is set, default it to true (unless on windows) |                 return reloader_helpers.watchdog(1.0) | ||||||
|         if debug and os.name == "posix": |  | ||||||
|             auto_reload = True |  | ||||||
|         # Allow for overriding either of the defaults |  | ||||||
|         auto_reload = kwargs.get("auto_reload", auto_reload) |  | ||||||
|  |  | ||||||
|         if sock is None: |         if sock is None: | ||||||
|             host, port = host or "127.0.0.1", port or 8000 |             host, port = host or "127.0.0.1", port or 8000 | ||||||
| @@ -1156,17 +1121,6 @@ class Sanic: | |||||||
|                 ) |                 ) | ||||||
|                 workers = 1 |                 workers = 1 | ||||||
|             if workers == 1: |             if workers == 1: | ||||||
|                 if auto_reload and os.name != "posix": |  | ||||||
|                     # This condition must be removed after implementing |  | ||||||
|                     # auto reloader for other operating systems. |  | ||||||
|                     raise NotImplementedError |  | ||||||
|  |  | ||||||
|                 if ( |  | ||||||
|                     auto_reload |  | ||||||
|                     and os.environ.get("SANIC_SERVER_RUNNING") != "true" |  | ||||||
|                 ): |  | ||||||
|                     reloader_helpers.watchdog(2) |  | ||||||
|                 else: |  | ||||||
|                 serve(**server_settings) |                 serve(**server_settings) | ||||||
|             else: |             else: | ||||||
|                 serve_multiple(server_settings, workers) |                 serve_multiple(server_settings, workers) | ||||||
| @@ -1189,6 +1143,7 @@ class Sanic: | |||||||
|         self, |         self, | ||||||
|         host: Optional[str] = None, |         host: Optional[str] = None, | ||||||
|         port: Optional[int] = None, |         port: Optional[int] = None, | ||||||
|  |         *, | ||||||
|         debug: bool = False, |         debug: bool = False, | ||||||
|         ssl: Union[dict, SSLContext, None] = None, |         ssl: Union[dict, SSLContext, None] = None, | ||||||
|         sock: Optional[socket] = None, |         sock: Optional[socket] = None, | ||||||
| @@ -1413,7 +1368,7 @@ class Sanic: | |||||||
|             server_settings["run_async"] = True |             server_settings["run_async"] = True | ||||||
|  |  | ||||||
|         # Serve |         # Serve | ||||||
|         if host and port and os.environ.get("SANIC_SERVER_RUNNING") != "true": |         if host and port: | ||||||
|             proto = "http" |             proto = "http" | ||||||
|             if ssl is not None: |             if ssl is not None: | ||||||
|                 proto = "https" |                 proto = "https" | ||||||
| @@ -1425,6 +1380,55 @@ class Sanic: | |||||||
|         parts = [self.name, *parts] |         parts = [self.name, *parts] | ||||||
|         return ".".join(parts) |         return ".".join(parts) | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def _loop_add_task(cls, task, app, loop): | ||||||
|  |         if callable(task): | ||||||
|  |             try: | ||||||
|  |                 loop.create_task(task(app)) | ||||||
|  |             except TypeError: | ||||||
|  |                 loop.create_task(task()) | ||||||
|  |         else: | ||||||
|  |             loop.create_task(task) | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def _cancel_websocket_tasks(cls, app, loop): | ||||||
|  |         for task in app.websocket_tasks: | ||||||
|  |             task.cancel() | ||||||
|  |  | ||||||
|  |     async def _websocket_handler( | ||||||
|  |         self, handler, request, *args, subprotocols=None, **kwargs | ||||||
|  |     ): | ||||||
|  |         request.app = self | ||||||
|  |         if not getattr(handler, "__blueprintname__", False): | ||||||
|  |             request.endpoint = handler.__name__ | ||||||
|  |         else: | ||||||
|  |             request.endpoint = ( | ||||||
|  |                 getattr(handler, "__blueprintname__", "") + handler.__name__ | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |             pass | ||||||
|  |  | ||||||
|  |         if self.asgi: | ||||||
|  |             ws = request.transport.get_websocket_connection() | ||||||
|  |         else: | ||||||
|  |             protocol = request.transport.get_protocol() | ||||||
|  |             protocol.app = self | ||||||
|  |  | ||||||
|  |             ws = await protocol.websocket_handshake(request, subprotocols) | ||||||
|  |  | ||||||
|  |         # schedule the application handler | ||||||
|  |         # its future is kept in self.websocket_tasks in case it | ||||||
|  |         # needs to be cancelled due to the server being stopped | ||||||
|  |         fut = ensure_future(handler(request, ws, *args, **kwargs)) | ||||||
|  |         self.websocket_tasks.add(fut) | ||||||
|  |         try: | ||||||
|  |             await fut | ||||||
|  |         except (CancelledError, ConnectionClosed): | ||||||
|  |             pass | ||||||
|  |         finally: | ||||||
|  |             self.websocket_tasks.remove(fut) | ||||||
|  |         await ws.close() | ||||||
|  |  | ||||||
|     # -------------------------------------------------------------------- # |     # -------------------------------------------------------------------- # | ||||||
|     # ASGI |     # ASGI | ||||||
|     # -------------------------------------------------------------------- # |     # -------------------------------------------------------------------- # | ||||||
|   | |||||||
| @@ -143,7 +143,7 @@ class Blueprint: | |||||||
|             if _routes: |             if _routes: | ||||||
|                 routes += _routes |                 routes += _routes | ||||||
|  |  | ||||||
|         route_names = [route.name for route in routes] |         route_names = [route.name for route in routes if route] | ||||||
|         # Middleware |         # Middleware | ||||||
|         for future in self.middlewares: |         for future in self.middlewares: | ||||||
|             if future.args or future.kwargs: |             if future.args or future.kwargs: | ||||||
|   | |||||||
| @@ -3,7 +3,6 @@ import signal | |||||||
| import subprocess | import subprocess | ||||||
| import sys | import sys | ||||||
|  |  | ||||||
| from multiprocessing import Process |  | ||||||
| from time import sleep | from time import sleep | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -35,101 +34,26 @@ def _iter_module_files(): | |||||||
|  |  | ||||||
| def _get_args_for_reloading(): | def _get_args_for_reloading(): | ||||||
|     """Returns the executable.""" |     """Returns the executable.""" | ||||||
|     rv = [sys.executable] |  | ||||||
|     main_module = sys.modules["__main__"] |     main_module = sys.modules["__main__"] | ||||||
|     mod_spec = getattr(main_module, "__spec__", None) |     mod_spec = getattr(main_module, "__spec__", None) | ||||||
|  |     if sys.argv[0] in ("", "-c"): | ||||||
|  |         raise RuntimeError( | ||||||
|  |             f"Autoreloader cannot work with argv[0]={sys.argv[0]!r}" | ||||||
|  |         ) | ||||||
|     if mod_spec: |     if mod_spec: | ||||||
|         # Parent exe was launched as a module rather than a script |         # Parent exe was launched as a module rather than a script | ||||||
|         rv.extend(["-m", mod_spec.name]) |         return [sys.executable, "-m", mod_spec.name] + sys.argv[1:] | ||||||
|         if len(sys.argv) > 1: |     return [sys.executable] + sys.argv | ||||||
|             rv.extend(sys.argv[1:]) |  | ||||||
|     else: |  | ||||||
|         rv.extend(sys.argv) |  | ||||||
|     return rv |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def restart_with_reloader(): | def restart_with_reloader(): | ||||||
|     """Create a new process and a subprocess in it with the same arguments as |     """Create a new process and a subprocess in it with the same arguments as | ||||||
|     this one. |     this one. | ||||||
|     """ |     """ | ||||||
|     cwd = os.getcwd() |     return subprocess.Popen( | ||||||
|     args = _get_args_for_reloading() |         _get_args_for_reloading(), | ||||||
|     new_environ = os.environ.copy() |         env={**os.environ, "SANIC_SERVER_RUNNING": "true"}, | ||||||
|     new_environ["SANIC_SERVER_RUNNING"] = "true" |  | ||||||
|     cmd = " ".join(args) |  | ||||||
|     worker_process = Process( |  | ||||||
|         target=subprocess.call, |  | ||||||
|         args=(cmd,), |  | ||||||
|         kwargs={"cwd": cwd, "shell": True, "env": new_environ}, |  | ||||||
|     ) |     ) | ||||||
|     worker_process.start() |  | ||||||
|     return worker_process |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def kill_process_children_unix(pid): |  | ||||||
|     """Find and kill child processes of a process (maximum two level). |  | ||||||
|  |  | ||||||
|     :param pid: PID of parent process (process ID) |  | ||||||
|     :return: Nothing |  | ||||||
|     """ |  | ||||||
|     root_process_path = f"/proc/{pid}/task/{pid}/children" |  | ||||||
|     if not os.path.isfile(root_process_path): |  | ||||||
|         return |  | ||||||
|     with open(root_process_path) as children_list_file: |  | ||||||
|         children_list_pid = children_list_file.read().split() |  | ||||||
|  |  | ||||||
|     for child_pid in children_list_pid: |  | ||||||
|         children_proc_path = "/proc/%s/task/%s/children" % ( |  | ||||||
|             child_pid, |  | ||||||
|             child_pid, |  | ||||||
|         ) |  | ||||||
|         if not os.path.isfile(children_proc_path): |  | ||||||
|             continue |  | ||||||
|         with open(children_proc_path) as children_list_file_2: |  | ||||||
|             children_list_pid_2 = children_list_file_2.read().split() |  | ||||||
|         for _pid in children_list_pid_2: |  | ||||||
|             try: |  | ||||||
|                 os.kill(int(_pid), signal.SIGTERM) |  | ||||||
|             except ProcessLookupError: |  | ||||||
|                 continue |  | ||||||
|         try: |  | ||||||
|             os.kill(int(child_pid), signal.SIGTERM) |  | ||||||
|         except ProcessLookupError: |  | ||||||
|             continue |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def kill_process_children_osx(pid): |  | ||||||
|     """Find and kill child processes of a process. |  | ||||||
|  |  | ||||||
|     :param pid: PID of parent process (process ID) |  | ||||||
|     :return: Nothing |  | ||||||
|     """ |  | ||||||
|     subprocess.run(["pkill", "-P", str(pid)]) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def kill_process_children(pid): |  | ||||||
|     """Find and kill child processes of a process. |  | ||||||
|  |  | ||||||
|     :param pid: PID of parent process (process ID) |  | ||||||
|     :return: Nothing |  | ||||||
|     """ |  | ||||||
|     if sys.platform == "darwin": |  | ||||||
|         kill_process_children_osx(pid) |  | ||||||
|     elif sys.platform == "linux": |  | ||||||
|         kill_process_children_unix(pid) |  | ||||||
|     else: |  | ||||||
|         pass  # should signal error here |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def kill_program_completly(proc): |  | ||||||
|     """Kill worker and it's child processes and exit. |  | ||||||
|  |  | ||||||
|     :param proc: worker process (process ID) |  | ||||||
|     :return: Nothing |  | ||||||
|     """ |  | ||||||
|     kill_process_children(proc.pid) |  | ||||||
|     proc.terminate() |  | ||||||
|     os._exit(0) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def watchdog(sleep_interval): | def watchdog(sleep_interval): | ||||||
| @@ -138,15 +62,21 @@ def watchdog(sleep_interval): | |||||||
|     :param sleep_interval: interval in second. |     :param sleep_interval: interval in second. | ||||||
|     :return: Nothing |     :return: Nothing | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|  |     def interrupt_self(*args): | ||||||
|  |         raise KeyboardInterrupt | ||||||
|  |  | ||||||
|     mtimes = {} |     mtimes = {} | ||||||
|  |     signal.signal(signal.SIGTERM, interrupt_self) | ||||||
|  |     if os.name == "nt": | ||||||
|  |         signal.signal(signal.SIGBREAK, interrupt_self) | ||||||
|  |  | ||||||
|     worker_process = restart_with_reloader() |     worker_process = restart_with_reloader() | ||||||
|     signal.signal( |  | ||||||
|         signal.SIGTERM, lambda *args: kill_program_completly(worker_process) |     try: | ||||||
|     ) |  | ||||||
|     signal.signal( |  | ||||||
|         signal.SIGINT, lambda *args: kill_program_completly(worker_process) |  | ||||||
|     ) |  | ||||||
|         while True: |         while True: | ||||||
|  |             need_reload = False | ||||||
|  |  | ||||||
|             for filename in _iter_module_files(): |             for filename in _iter_module_files(): | ||||||
|                 try: |                 try: | ||||||
|                     mtime = os.stat(filename).st_mtime |                     mtime = os.stat(filename).st_mtime | ||||||
| @@ -156,12 +86,18 @@ def watchdog(sleep_interval): | |||||||
|                 old_time = mtimes.get(filename) |                 old_time = mtimes.get(filename) | ||||||
|                 if old_time is None: |                 if old_time is None: | ||||||
|                     mtimes[filename] = mtime |                     mtimes[filename] = mtime | ||||||
|                 continue |  | ||||||
|                 elif mtime > old_time: |                 elif mtime > old_time: | ||||||
|                 kill_process_children(worker_process.pid) |  | ||||||
|                 worker_process.terminate() |  | ||||||
|                 worker_process = restart_with_reloader() |  | ||||||
|                     mtimes[filename] = mtime |                     mtimes[filename] = mtime | ||||||
|                 break |                     need_reload = True | ||||||
|  |  | ||||||
|  |             if need_reload: | ||||||
|  |                 worker_process.terminate() | ||||||
|  |                 worker_process.wait() | ||||||
|  |                 worker_process = restart_with_reloader() | ||||||
|  |  | ||||||
|             sleep(sleep_interval) |             sleep(sleep_interval) | ||||||
|  |     except KeyboardInterrupt: | ||||||
|  |         pass | ||||||
|  |     finally: | ||||||
|  |         worker_process.terminate() | ||||||
|  |         worker_process.wait() | ||||||
|   | |||||||
| @@ -149,6 +149,12 @@ class HTTPResponse(BaseHTTPResponse): | |||||||
|         self.headers = Header(headers or {}) |         self.headers = Header(headers or {}) | ||||||
|         self._cookies = None |         self._cookies = None | ||||||
|  |  | ||||||
|  |         if body_bytes: | ||||||
|  |             warnings.warn( | ||||||
|  |                 "Parameter `body_bytes` is deprecated, use `body` instead", | ||||||
|  |                 DeprecationWarning, | ||||||
|  |             ) | ||||||
|  |  | ||||||
|     def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None): |     def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None): | ||||||
|         body = b"" |         body = b"" | ||||||
|         if has_message_body(self.status): |         if has_message_body(self.status): | ||||||
| @@ -173,7 +179,7 @@ def empty(status=204, headers=None): | |||||||
|     :param status Response code. |     :param status Response code. | ||||||
|     :param headers Custom Headers. |     :param headers Custom Headers. | ||||||
|     """ |     """ | ||||||
|     return HTTPResponse(body_bytes=b"", status=status, headers=headers) |     return HTTPResponse(body=b"", status=status, headers=headers) | ||||||
|  |  | ||||||
|  |  | ||||||
| def json( | def json( | ||||||
| @@ -243,10 +249,7 @@ def raw( | |||||||
|     :param content_type: the content type (string) of the response. |     :param content_type: the content type (string) of the response. | ||||||
|     """ |     """ | ||||||
|     return HTTPResponse( |     return HTTPResponse( | ||||||
|         body_bytes=body, |         body=body, status=status, headers=headers, content_type=content_type, | ||||||
|         status=status, |  | ||||||
|         headers=headers, |  | ||||||
|         content_type=content_type, |  | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -306,10 +309,10 @@ async def file( | |||||||
|  |  | ||||||
|     mime_type = mime_type or guess_type(filename)[0] or "text/plain" |     mime_type = mime_type or guess_type(filename)[0] or "text/plain" | ||||||
|     return HTTPResponse( |     return HTTPResponse( | ||||||
|  |         body=out_stream, | ||||||
|         status=status, |         status=status, | ||||||
|         headers=headers, |         headers=headers, | ||||||
|         content_type=mime_type, |         content_type=mime_type, | ||||||
|         body_bytes=out_stream, |  | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -927,7 +927,7 @@ def serve_multiple(server_settings, workers): | |||||||
|  |  | ||||||
|     signal_func(SIGINT, lambda s, f: sig_handler(s, f)) |     signal_func(SIGINT, lambda s, f: sig_handler(s, f)) | ||||||
|     signal_func(SIGTERM, lambda s, f: sig_handler(s, f)) |     signal_func(SIGTERM, lambda s, f: sig_handler(s, f)) | ||||||
|     mp = multiprocessing.get_context("fork") |     mp = multiprocessing.get_context("spawn") | ||||||
|  |  | ||||||
|     for _ in range(workers): |     for _ in range(workers): | ||||||
|         process = mp.Process(target=serve, kwargs=server_settings) |         process = mp.Process(target=serve, kwargs=server_settings) | ||||||
|   | |||||||
| @@ -1,3 +1,4 @@ | |||||||
|  | from functools import partial, wraps | ||||||
| from mimetypes import guess_type | from mimetypes import guess_type | ||||||
| from os import path | from os import path | ||||||
| from re import sub | from re import sub | ||||||
| @@ -15,48 +16,15 @@ from sanic.handlers import ContentRangeHandler | |||||||
| from sanic.response import HTTPResponse, file, file_stream | from sanic.response import HTTPResponse, file, file_stream | ||||||
|  |  | ||||||
|  |  | ||||||
| def register( | async def _static_request_handler( | ||||||
|     app, |  | ||||||
|     uri, |  | ||||||
|     file_or_directory, |     file_or_directory, | ||||||
|     pattern, |  | ||||||
|     use_modified_since, |     use_modified_since, | ||||||
|     use_content_range, |     use_content_range, | ||||||
|     stream_large_files, |     stream_large_files, | ||||||
|     name="static", |     request, | ||||||
|     host=None, |  | ||||||
|     strict_slashes=None, |  | ||||||
|     content_type=None, |     content_type=None, | ||||||
|  |     file_uri=None, | ||||||
| ): | ): | ||||||
|     # TODO: Though sanic is not a file server, I feel like we should at least |  | ||||||
|     #       make a good effort here.  Modified-since is nice, but we could |  | ||||||
|     #       also look into etags, expires, and caching |  | ||||||
|     """ |  | ||||||
|     Register a static directory handler with Sanic by adding a route to the |  | ||||||
|     router and registering a handler. |  | ||||||
|  |  | ||||||
|     :param app: Sanic |  | ||||||
|     :param file_or_directory: File or directory path to serve from |  | ||||||
|     :param uri: URL to serve from |  | ||||||
|     :param pattern: regular expression used to match files in the URL |  | ||||||
|     :param use_modified_since: If true, send file modified time, and return |  | ||||||
|                                not modified if the browser's matches the |  | ||||||
|                                server's |  | ||||||
|     :param use_content_range: If true, process header for range requests |  | ||||||
|                               and sends the file part that is requested |  | ||||||
|     :param stream_large_files: If true, use the file_stream() handler rather |  | ||||||
|                               than the file() handler to send the file |  | ||||||
|                               If this is an integer, this represents the |  | ||||||
|                               threshold size to switch to file_stream() |  | ||||||
|     :param name: user defined name used for url_for |  | ||||||
|     :param content_type: user defined content type for header |  | ||||||
|     """ |  | ||||||
|     # If we're not trying to match a file directly, |  | ||||||
|     # serve from the folder |  | ||||||
|     if not path.isfile(file_or_directory): |  | ||||||
|         uri += "<file_uri:" + pattern + ">" |  | ||||||
|  |  | ||||||
|     async def _handler(request, file_uri=None): |  | ||||||
|     # Using this to determine if the URL is trying to break out of the path |     # Using this to determine if the URL is trying to break out of the path | ||||||
|     # served.  os.path.realpath seems to be very slow |     # served.  os.path.realpath seems to be very slow | ||||||
|     if file_uri and "../" in file_uri: |     if file_uri and "../" in file_uri: | ||||||
| @@ -66,9 +34,7 @@ def register( | |||||||
|     # from herping a derp and treating the uri as an absolute path |     # from herping a derp and treating the uri as an absolute path | ||||||
|     root_path = file_path = file_or_directory |     root_path = file_path = file_or_directory | ||||||
|     if file_uri: |     if file_uri: | ||||||
|             file_path = path.join( |         file_path = path.join(file_or_directory, sub("^[/]*", "", file_uri)) | ||||||
|                 file_or_directory, sub("^[/]*", "", file_uri) |  | ||||||
|             ) |  | ||||||
|  |  | ||||||
|     # URL decode the path sent by the browser otherwise we won't be able to |     # URL decode the path sent by the browser otherwise we won't be able to | ||||||
|     # match filenames which got encoded (filenames with spaces etc) |     # match filenames which got encoded (filenames with spaces etc) | ||||||
| @@ -132,10 +98,63 @@ def register( | |||||||
|             "File not found", path=file_or_directory, relative_url=file_uri |             "File not found", path=file_or_directory, relative_url=file_uri | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def register( | ||||||
|  |     app, | ||||||
|  |     uri, | ||||||
|  |     file_or_directory, | ||||||
|  |     pattern, | ||||||
|  |     use_modified_since, | ||||||
|  |     use_content_range, | ||||||
|  |     stream_large_files, | ||||||
|  |     name="static", | ||||||
|  |     host=None, | ||||||
|  |     strict_slashes=None, | ||||||
|  |     content_type=None, | ||||||
|  | ): | ||||||
|  |     # TODO: Though sanic is not a file server, I feel like we should at least | ||||||
|  |     #       make a good effort here.  Modified-since is nice, but we could | ||||||
|  |     #       also look into etags, expires, and caching | ||||||
|  |     """ | ||||||
|  |     Register a static directory handler with Sanic by adding a route to the | ||||||
|  |     router and registering a handler. | ||||||
|  |  | ||||||
|  |     :param app: Sanic | ||||||
|  |     :param file_or_directory: File or directory path to serve from | ||||||
|  |     :param uri: URL to serve from | ||||||
|  |     :param pattern: regular expression used to match files in the URL | ||||||
|  |     :param use_modified_since: If true, send file modified time, and return | ||||||
|  |                                not modified if the browser's matches the | ||||||
|  |                                server's | ||||||
|  |     :param use_content_range: If true, process header for range requests | ||||||
|  |                               and sends the file part that is requested | ||||||
|  |     :param stream_large_files: If true, use the file_stream() handler rather | ||||||
|  |                               than the file() handler to send the file | ||||||
|  |                               If this is an integer, this represents the | ||||||
|  |                               threshold size to switch to file_stream() | ||||||
|  |     :param name: user defined name used for url_for | ||||||
|  |     :param content_type: user defined content type for header | ||||||
|  |     """ | ||||||
|  |     # If we're not trying to match a file directly, | ||||||
|  |     # serve from the folder | ||||||
|  |     if not path.isfile(file_or_directory): | ||||||
|  |         uri += "<file_uri:" + pattern + ">" | ||||||
|  |  | ||||||
|     # special prefix for static files |     # special prefix for static files | ||||||
|     if not name.startswith("_static_"): |     if not name.startswith("_static_"): | ||||||
|         name = f"_static_{name}" |         name = f"_static_{name}" | ||||||
|  |  | ||||||
|  |     _handler = wraps(_static_request_handler)( | ||||||
|  |         partial( | ||||||
|  |             _static_request_handler, | ||||||
|  |             file_or_directory, | ||||||
|  |             use_modified_since, | ||||||
|  |             use_content_range, | ||||||
|  |             stream_large_files, | ||||||
|  |             content_type=content_type, | ||||||
|  |         ) | ||||||
|  |     ) | ||||||
|  |  | ||||||
|     app.route( |     app.route( | ||||||
|         uri, |         uri, | ||||||
|         methods=["GET", "HEAD"], |         methods=["GET", "HEAD"], | ||||||
|   | |||||||
| @@ -113,7 +113,7 @@ class WebSocketProtocol(HttpProtocol): | |||||||
|  |  | ||||||
|         # hook up the websocket protocol |         # hook up the websocket protocol | ||||||
|         self.websocket = WebSocketCommonProtocol( |         self.websocket = WebSocketCommonProtocol( | ||||||
|             timeout=self.websocket_timeout, |             close_timeout=self.websocket_timeout, | ||||||
|             max_size=self.websocket_max_size, |             max_size=self.websocket_max_size, | ||||||
|             max_queue=self.websocket_max_queue, |             max_queue=self.websocket_max_queue, | ||||||
|             read_limit=self.websocket_read_limit, |             read_limit=self.websocket_read_limit, | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								setup.py
									
									
									
									
									
								
							| @@ -5,7 +5,6 @@ import codecs | |||||||
| import os | import os | ||||||
| import re | import re | ||||||
| import sys | import sys | ||||||
|  |  | ||||||
| from distutils.util import strtobool | from distutils.util import strtobool | ||||||
|  |  | ||||||
| from setuptools import setup | from setuptools import setup | ||||||
| @@ -39,9 +38,7 @@ def open_local(paths, mode="r", encoding="utf8"): | |||||||
|  |  | ||||||
| with open_local(["sanic", "__version__.py"], encoding="latin1") as fp: | with open_local(["sanic", "__version__.py"], encoding="latin1") as fp: | ||||||
|     try: |     try: | ||||||
|         version = re.findall( |         version = re.findall(r"^__version__ = \"([^']+)\"\r?$", fp.read(), re.M)[0] | ||||||
|             r"^__version__ = \"([^']+)\"\r?$", fp.read(), re.M |  | ||||||
|         )[0] |  | ||||||
|     except IndexError: |     except IndexError: | ||||||
|         raise RuntimeError("Unable to determine version.") |         raise RuntimeError("Unable to determine version.") | ||||||
|  |  | ||||||
| @@ -70,11 +67,10 @@ setup_kwargs = { | |||||||
|         "Programming Language :: Python :: 3.7", |         "Programming Language :: Python :: 3.7", | ||||||
|         "Programming Language :: Python :: 3.8", |         "Programming Language :: Python :: 3.8", | ||||||
|     ], |     ], | ||||||
|  |     "entry_points": {"console_scripts": ["sanic = sanic.__main__:main"]}, | ||||||
| } | } | ||||||
|  |  | ||||||
| env_dependency = ( | env_dependency = '; sys_platform != "win32" ' 'and implementation_name == "cpython"' | ||||||
|     '; sys_platform != "win32" ' 'and implementation_name == "cpython"' |  | ||||||
| ) |  | ||||||
| ujson = "ujson>=1.35" + env_dependency | ujson = "ujson>=1.35" + env_dependency | ||||||
| uvloop = "uvloop>=0.5.3" + env_dependency | uvloop = "uvloop>=0.5.3" + env_dependency | ||||||
|  |  | ||||||
| @@ -83,7 +79,7 @@ requirements = [ | |||||||
|     uvloop, |     uvloop, | ||||||
|     ujson, |     ujson, | ||||||
|     "aiofiles>=0.3.0", |     "aiofiles>=0.3.0", | ||||||
|     "websockets>=7.0,<9.0", |     "websockets>=8.1,<9.0", | ||||||
|     "multidict>=4.0,<5.0", |     "multidict>=4.0,<5.0", | ||||||
|     "httpx==0.11.1", |     "httpx==0.11.1", | ||||||
| ] | ] | ||||||
|   | |||||||
| @@ -56,6 +56,7 @@ def test_asyncio_server_no_start_serving(app): | |||||||
|         srv = loop.run_until_complete(asyncio_srv_coro) |         srv = loop.run_until_complete(asyncio_srv_coro) | ||||||
|         assert srv.is_serving() is False |         assert srv.is_serving() is False | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.skipif( | @pytest.mark.skipif( | ||||||
|     sys.version_info < (3, 7), reason="requires python3.7 or higher" |     sys.version_info < (3, 7), reason="requires python3.7 or higher" | ||||||
| ) | ) | ||||||
| @@ -75,6 +76,7 @@ def test_asyncio_server_start_serving(app): | |||||||
|         loop.run_until_complete(wait_close) |         loop.run_until_complete(wait_close) | ||||||
|         # Looks like we can't easily test `serve_forever()` |         # Looks like we can't easily test `serve_forever()` | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_app_loop_not_running(app): | def test_app_loop_not_running(app): | ||||||
|     with pytest.raises(SanicException) as excinfo: |     with pytest.raises(SanicException) as excinfo: | ||||||
|         app.loop |         app.loop | ||||||
| @@ -125,7 +127,10 @@ def test_app_handle_request_handler_is_none(app, monkeypatch): | |||||||
|  |  | ||||||
|     request, response = app.test_client.get("/test") |     request, response = app.test_client.get("/test") | ||||||
|  |  | ||||||
|     assert "'None' was returned while requesting a handler from the router" in response.text |     assert ( | ||||||
|  |         "'None' was returned while requesting a handler from the router" | ||||||
|  |         in response.text | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.parametrize("websocket_enabled", [True, False]) | @pytest.mark.parametrize("websocket_enabled", [True, False]) | ||||||
|   | |||||||
| @@ -84,8 +84,8 @@ def test_listeners_triggered(app): | |||||||
|  |  | ||||||
|     all_tasks = ( |     all_tasks = ( | ||||||
|         asyncio.Task.all_tasks() |         asyncio.Task.all_tasks() | ||||||
|         if sys.version_info < (3, 7) else |         if sys.version_info < (3, 7) | ||||||
|         asyncio.all_tasks(asyncio.get_event_loop()) |         else asyncio.all_tasks(asyncio.get_event_loop()) | ||||||
|     ) |     ) | ||||||
|     for task in all_tasks: |     for task in all_tasks: | ||||||
|         task.cancel() |         task.cancel() | ||||||
| @@ -134,8 +134,8 @@ def test_listeners_triggered_async(app): | |||||||
|  |  | ||||||
|     all_tasks = ( |     all_tasks = ( | ||||||
|         asyncio.Task.all_tasks() |         asyncio.Task.all_tasks() | ||||||
|         if sys.version_info < (3, 7) else |         if sys.version_info < (3, 7) | ||||||
|         asyncio.all_tasks(asyncio.get_event_loop()) |         else asyncio.all_tasks(asyncio.get_event_loop()) | ||||||
|     ) |     ) | ||||||
|     for task in all_tasks: |     for task in all_tasks: | ||||||
|         task.cancel() |         task.cancel() | ||||||
|   | |||||||
| @@ -252,6 +252,76 @@ def test_several_bp_with_host(app): | |||||||
|     assert response.text == "Hello3" |     assert response.text == "Hello3" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_bp_with_host_list(app): | ||||||
|  |     bp = Blueprint("test_bp_host", url_prefix="/test1", host=["example.com", "sub.example.com"]) | ||||||
|  |  | ||||||
|  |     @bp.route("/") | ||||||
|  |     def handler1(request): | ||||||
|  |         return text("Hello") | ||||||
|  |  | ||||||
|  |     @bp.route("/", host=["sub1.example.com"]) | ||||||
|  |     def handler2(request): | ||||||
|  |         return text("Hello subdomain!") | ||||||
|  |  | ||||||
|  |     app.blueprint(bp) | ||||||
|  |     headers = {"Host": "example.com"} | ||||||
|  |     request, response = app.test_client.get("/test1/", headers=headers) | ||||||
|  |     assert response.text == "Hello" | ||||||
|  |  | ||||||
|  |     headers = {"Host": "sub.example.com"} | ||||||
|  |     request, response = app.test_client.get("/test1/", headers=headers) | ||||||
|  |     assert response.text == "Hello" | ||||||
|  |  | ||||||
|  |     headers = {"Host": "sub1.example.com"} | ||||||
|  |     request, response = app.test_client.get("/test1/", headers=headers) | ||||||
|  |  | ||||||
|  |     assert response.text == "Hello subdomain!" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_several_bp_with_host_list(app): | ||||||
|  |     bp = Blueprint("test_text", url_prefix="/test", host=["example.com", "sub.example.com"]) | ||||||
|  |     bp2 = Blueprint("test_text2", url_prefix="/test", host=["sub1.example.com", "sub2.example.com"]) | ||||||
|  |  | ||||||
|  |     @bp.route("/") | ||||||
|  |     def handler(request): | ||||||
|  |         return text("Hello") | ||||||
|  |  | ||||||
|  |     @bp2.route("/") | ||||||
|  |     def handler1(request): | ||||||
|  |         return text("Hello2") | ||||||
|  |  | ||||||
|  |     @bp2.route("/other/") | ||||||
|  |     def handler2(request): | ||||||
|  |         return text("Hello3") | ||||||
|  |  | ||||||
|  |     app.blueprint(bp) | ||||||
|  |     app.blueprint(bp2) | ||||||
|  |  | ||||||
|  |     assert bp.host == ["example.com", "sub.example.com"] | ||||||
|  |     headers = {"Host": "example.com"} | ||||||
|  |     request, response = app.test_client.get("/test/", headers=headers) | ||||||
|  |     assert response.text == "Hello" | ||||||
|  |  | ||||||
|  |     assert bp.host == ["example.com", "sub.example.com"] | ||||||
|  |     headers = {"Host": "sub.example.com"} | ||||||
|  |     request, response = app.test_client.get("/test/", headers=headers) | ||||||
|  |     assert response.text == "Hello" | ||||||
|  |  | ||||||
|  |     assert bp2.host == ["sub1.example.com", "sub2.example.com"] | ||||||
|  |     headers = {"Host": "sub1.example.com"} | ||||||
|  |     request, response = app.test_client.get("/test/", headers=headers) | ||||||
|  |     assert response.text == "Hello2" | ||||||
|  |     request, response = app.test_client.get("/test/other/", headers=headers) | ||||||
|  |     assert response.text == "Hello3" | ||||||
|  |  | ||||||
|  |     assert bp2.host == ["sub1.example.com", "sub2.example.com"] | ||||||
|  |     headers = {"Host": "sub2.example.com"} | ||||||
|  |     request, response = app.test_client.get("/test/", headers=headers) | ||||||
|  |     assert response.text == "Hello2" | ||||||
|  |     request, response = app.test_client.get("/test/other/", headers=headers) | ||||||
|  |     assert response.text == "Hello3" | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_bp_middleware(app): | def test_bp_middleware(app): | ||||||
|     blueprint = Blueprint("test_bp_middleware") |     blueprint = Blueprint("test_bp_middleware") | ||||||
|  |  | ||||||
| @@ -270,24 +340,31 @@ def test_bp_middleware(app): | |||||||
|     assert response.status == 200 |     assert response.status == 200 | ||||||
|     assert response.text == "FAIL" |     assert response.text == "FAIL" | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_bp_middleware_order(app): | def test_bp_middleware_order(app): | ||||||
|     blueprint = Blueprint("test_bp_middleware_order") |     blueprint = Blueprint("test_bp_middleware_order") | ||||||
|     order = list() |     order = list() | ||||||
|  |  | ||||||
|     @blueprint.middleware("request") |     @blueprint.middleware("request") | ||||||
|     def mw_1(request): |     def mw_1(request): | ||||||
|         order.append(1) |         order.append(1) | ||||||
|  |  | ||||||
|     @blueprint.middleware("request") |     @blueprint.middleware("request") | ||||||
|     def mw_2(request): |     def mw_2(request): | ||||||
|         order.append(2) |         order.append(2) | ||||||
|  |  | ||||||
|     @blueprint.middleware("request") |     @blueprint.middleware("request") | ||||||
|     def mw_3(request): |     def mw_3(request): | ||||||
|         order.append(3) |         order.append(3) | ||||||
|  |  | ||||||
|     @blueprint.middleware("response") |     @blueprint.middleware("response") | ||||||
|     def mw_4(request, response): |     def mw_4(request, response): | ||||||
|         order.append(6) |         order.append(6) | ||||||
|  |  | ||||||
|     @blueprint.middleware("response") |     @blueprint.middleware("response") | ||||||
|     def mw_5(request, response): |     def mw_5(request, response): | ||||||
|         order.append(5) |         order.append(5) | ||||||
|  |  | ||||||
|     @blueprint.middleware("response") |     @blueprint.middleware("response") | ||||||
|     def mw_6(request, response): |     def mw_6(request, response): | ||||||
|         order.append(4) |         order.append(4) | ||||||
| @@ -303,6 +380,7 @@ def test_bp_middleware_order(app): | |||||||
|     assert response.status == 200 |     assert response.status == 200 | ||||||
|     assert order == [1, 2, 3, 4, 5, 6] |     assert order == [1, 2, 3, 4, 5, 6] | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_bp_exception_handler(app): | def test_bp_exception_handler(app): | ||||||
|     blueprint = Blueprint("test_middleware") |     blueprint = Blueprint("test_middleware") | ||||||
|  |  | ||||||
| @@ -585,9 +663,7 @@ def test_bp_group_with_default_url_prefix(app): | |||||||
|     from uuid import uuid4 |     from uuid import uuid4 | ||||||
|  |  | ||||||
|     resource_id = str(uuid4()) |     resource_id = str(uuid4()) | ||||||
|     request, response = app.test_client.get( |     request, response = app.test_client.get(f"/api/v1/resources/{resource_id}") | ||||||
|         f"/api/v1/resources/{resource_id}" |  | ||||||
|     ) |  | ||||||
|     assert response.json == {"resource_id": resource_id} |     assert response.json == {"resource_id": resource_id} | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -9,6 +9,7 @@ from sanic import Sanic, server | |||||||
| from sanic.response import text | from sanic.response import text | ||||||
| from sanic.testing import HOST, SanicTestClient | from sanic.testing import HOST, SanicTestClient | ||||||
|  |  | ||||||
|  |  | ||||||
| CONFIG_FOR_TESTS = {"KEEP_ALIVE_TIMEOUT": 2, "KEEP_ALIVE": True} | CONFIG_FOR_TESTS = {"KEEP_ALIVE_TIMEOUT": 2, "KEEP_ALIVE": True} | ||||||
|  |  | ||||||
| old_conn = None | old_conn = None | ||||||
| @@ -46,7 +47,7 @@ class ReusableSanicConnectionPool( | |||||||
|                 cert=self.cert, |                 cert=self.cert, | ||||||
|                 verify=self.verify, |                 verify=self.verify, | ||||||
|                 trust_env=self.trust_env, |                 trust_env=self.trust_env, | ||||||
|                 http2=self.http2 |                 http2=self.http2, | ||||||
|             ) |             ) | ||||||
|             connection = httpx.dispatch.connection.HTTPConnection( |             connection = httpx.dispatch.connection.HTTPConnection( | ||||||
|                 origin, |                 origin, | ||||||
| @@ -166,9 +167,7 @@ class ReuseableSanicTestClient(SanicTestClient): | |||||||
|             try: |             try: | ||||||
|                 return results[-1] |                 return results[-1] | ||||||
|             except Exception: |             except Exception: | ||||||
|                 raise ValueError( |                 raise ValueError(f"Request object expected, got ({results})") | ||||||
|                     f"Request object expected, got ({results})" |  | ||||||
|                 ) |  | ||||||
|  |  | ||||||
|     def kill_server(self): |     def kill_server(self): | ||||||
|         try: |         try: | ||||||
|   | |||||||
| @@ -87,3 +87,14 @@ def test_pickle_app_with_bp(app, protocol): | |||||||
|     request, response = up_p_app.test_client.get("/") |     request, response = up_p_app.test_client.get("/") | ||||||
|     assert up_p_app.is_request_stream is False |     assert up_p_app.is_request_stream is False | ||||||
|     assert response.text == "Hello" |     assert response.text == "Hello" | ||||||
|  |  | ||||||
|  | @pytest.mark.parametrize("protocol", [3, 4]) | ||||||
|  | def test_pickle_app_with_static(app, protocol): | ||||||
|  |     app.route("/")(handler) | ||||||
|  |     app.static('/static', "/tmp/static") | ||||||
|  |     p_app = pickle.dumps(app, protocol=protocol) | ||||||
|  |     del app | ||||||
|  |     up_p_app = pickle.loads(p_app) | ||||||
|  |     assert up_p_app | ||||||
|  |     request, response = up_p_app.test_client.get("/static/missing.txt") | ||||||
|  |     assert response.status == 404 | ||||||
|   | |||||||
							
								
								
									
										90
									
								
								tests/test_reloader.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								tests/test_reloader.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | |||||||
|  | import os | ||||||
|  | import secrets | ||||||
|  | import sys | ||||||
|  | from contextlib import suppress | ||||||
|  |  | ||||||
|  | from subprocess import PIPE, Popen, TimeoutExpired | ||||||
|  | from tempfile import TemporaryDirectory | ||||||
|  | from textwrap import dedent | ||||||
|  | from threading import Timer | ||||||
|  | from time import sleep | ||||||
|  |  | ||||||
|  | import pytest | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # We need to interrupt the autoreloader without killing it, so that the server gets terminated | ||||||
|  | # https://stefan.sofa-rockers.org/2013/08/15/handling-sub-process-hierarchies-python-linux-os-x/ | ||||||
|  |  | ||||||
|  | try: | ||||||
|  |     from signal import CTRL_BREAK_EVENT | ||||||
|  |     from subprocess import CREATE_NEW_PROCESS_GROUP | ||||||
|  |  | ||||||
|  |     flags = CREATE_NEW_PROCESS_GROUP | ||||||
|  | except ImportError: | ||||||
|  |     flags = 0 | ||||||
|  |  | ||||||
|  | def terminate(proc): | ||||||
|  |     if flags: | ||||||
|  |         proc.send_signal(CTRL_BREAK_EVENT) | ||||||
|  |     else: | ||||||
|  |         proc.terminate() | ||||||
|  |  | ||||||
|  | def write_app(filename, **runargs): | ||||||
|  |     text = secrets.token_urlsafe() | ||||||
|  |     with open(filename, "w") as f: | ||||||
|  |         f.write(dedent(f"""\ | ||||||
|  |             import os | ||||||
|  |             from sanic import Sanic | ||||||
|  |  | ||||||
|  |             app = Sanic(__name__) | ||||||
|  |  | ||||||
|  |             @app.listener("after_server_start") | ||||||
|  |             def complete(*args): | ||||||
|  |                 print("complete", os.getpid(), {text!r}) | ||||||
|  |  | ||||||
|  |             if __name__ == "__main__": | ||||||
|  |                 app.run(**{runargs!r}) | ||||||
|  |             """ | ||||||
|  |         )) | ||||||
|  |     return text | ||||||
|  |  | ||||||
|  | def scanner(proc): | ||||||
|  |     for line in proc.stdout: | ||||||
|  |         line = line.decode().strip() | ||||||
|  |         print(">", line) | ||||||
|  |         if line.startswith("complete"): | ||||||
|  |             yield line | ||||||
|  |  | ||||||
|  |  | ||||||
|  | argv = dict( | ||||||
|  |     script=[sys.executable, "reloader.py"], | ||||||
|  |     module=[sys.executable, "-m", "reloader"], | ||||||
|  |     sanic=[sys.executable, "-m", "sanic", "--port", "42104", "--debug", "reloader.app"], | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | @pytest.mark.parametrize("runargs, mode", [ | ||||||
|  |     (dict(port=42102, auto_reload=True), "script"), | ||||||
|  |     (dict(port=42103, debug=True), "module"), | ||||||
|  |     (dict(), "sanic"), | ||||||
|  | ]) | ||||||
|  | async def test_reloader_live(runargs, mode): | ||||||
|  |     with TemporaryDirectory() as tmpdir: | ||||||
|  |         filename = os.path.join(tmpdir, "reloader.py") | ||||||
|  |         text = write_app(filename, **runargs) | ||||||
|  |         proc = Popen(argv[mode], cwd=tmpdir, stdout=PIPE, creationflags=flags) | ||||||
|  |         try: | ||||||
|  |             timeout = Timer(5, 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) | ||||||
|  |             assert text in next(line) | ||||||
|  |             # Edit source code and try again | ||||||
|  |             text = write_app(filename, **runargs) | ||||||
|  |             assert text in next(line) | ||||||
|  |         finally: | ||||||
|  |             timeout.cancel() | ||||||
|  |             terminate(proc) | ||||||
|  |             with suppress(TimeoutExpired): | ||||||
|  |                 proc.wait(timeout=3) | ||||||
| @@ -614,6 +614,7 @@ def test_request_stream(app): | |||||||
|     assert response.status == 200 |     assert response.status == 200 | ||||||
|     assert response.text == data |     assert response.text == data | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_streaming_new_api(app): | def test_streaming_new_api(app): | ||||||
|     @app.post("/non-stream") |     @app.post("/non-stream") | ||||||
|     async def handler(request): |     async def handler(request): | ||||||
|   | |||||||
| @@ -17,7 +17,7 @@ class DelayableHTTPConnection(httpx.dispatch.connection.HTTPConnection): | |||||||
|     async def send(self, request, timeout=None): |     async def send(self, request, timeout=None): | ||||||
|  |  | ||||||
|         if self.connection is None: |         if self.connection is None: | ||||||
|             self.connection = (await self.connect(timeout=timeout)) |             self.connection = await self.connect(timeout=timeout) | ||||||
|  |  | ||||||
|         if self._request_delay: |         if self._request_delay: | ||||||
|             await asyncio.sleep(self._request_delay) |             await asyncio.sleep(self._request_delay) | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| import asyncio | import asyncio | ||||||
| import inspect | import inspect | ||||||
| import os | import os | ||||||
|  | import warnings | ||||||
|  |  | ||||||
| from collections import namedtuple | from collections import namedtuple | ||||||
| from mimetypes import guess_type | from mimetypes import guess_type | ||||||
| @@ -242,7 +243,7 @@ def test_non_chunked_streaming_adds_correct_headers(non_chunked_streaming_app): | |||||||
|  |  | ||||||
|  |  | ||||||
| def test_non_chunked_streaming_returns_correct_content( | def test_non_chunked_streaming_returns_correct_content( | ||||||
|     non_chunked_streaming_app |     non_chunked_streaming_app, | ||||||
| ): | ): | ||||||
|     request, response = non_chunked_streaming_app.test_client.get("/") |     request, response = non_chunked_streaming_app.test_client.get("/") | ||||||
|     assert response.text == "foo,bar" |     assert response.text == "foo,bar" | ||||||
| @@ -257,7 +258,7 @@ def test_stream_response_status_returns_correct_headers(status): | |||||||
|  |  | ||||||
| @pytest.mark.parametrize("keep_alive_timeout", [10, 20, 30]) | @pytest.mark.parametrize("keep_alive_timeout", [10, 20, 30]) | ||||||
| def test_stream_response_keep_alive_returns_correct_headers( | def test_stream_response_keep_alive_returns_correct_headers( | ||||||
|     keep_alive_timeout |     keep_alive_timeout, | ||||||
| ): | ): | ||||||
|     response = StreamingHTTPResponse(sample_streaming_fn) |     response = StreamingHTTPResponse(sample_streaming_fn) | ||||||
|     headers = response.get_headers( |     headers = response.get_headers( | ||||||
| @@ -286,7 +287,7 @@ def test_stream_response_does_not_include_chunked_header_if_disabled(): | |||||||
|  |  | ||||||
|  |  | ||||||
| def test_stream_response_writes_correct_content_to_transport_when_chunked( | def test_stream_response_writes_correct_content_to_transport_when_chunked( | ||||||
|     streaming_app |     streaming_app, | ||||||
| ): | ): | ||||||
|     response = StreamingHTTPResponse(sample_streaming_fn) |     response = StreamingHTTPResponse(sample_streaming_fn) | ||||||
|     response.protocol = MagicMock(HttpProtocol) |     response.protocol = MagicMock(HttpProtocol) | ||||||
| @@ -434,9 +435,10 @@ def test_file_response_custom_filename( | |||||||
|     request, response = app.test_client.get(f"/files/{source}") |     request, response = app.test_client.get(f"/files/{source}") | ||||||
|     assert response.status == 200 |     assert response.status == 200 | ||||||
|     assert response.body == get_file_content(static_file_directory, source) |     assert response.body == get_file_content(static_file_directory, source) | ||||||
|     assert response.headers[ |     assert ( | ||||||
|         "Content-Disposition" |         response.headers["Content-Disposition"] | ||||||
|     ] == f'attachment; filename="{dest}"' |         == f'attachment; filename="{dest}"' | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"]) | @pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"]) | ||||||
| @@ -510,9 +512,10 @@ def test_file_stream_response_custom_filename( | |||||||
|     request, response = app.test_client.get(f"/files/{source}") |     request, response = app.test_client.get(f"/files/{source}") | ||||||
|     assert response.status == 200 |     assert response.status == 200 | ||||||
|     assert response.body == get_file_content(static_file_directory, source) |     assert response.body == get_file_content(static_file_directory, source) | ||||||
|     assert response.headers[ |     assert ( | ||||||
|         "Content-Disposition" |         response.headers["Content-Disposition"] | ||||||
|     ] == f'attachment; filename="{dest}"' |         == f'attachment; filename="{dest}"' | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"]) | @pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"]) | ||||||
| @@ -581,7 +584,10 @@ def test_file_stream_response_range( | |||||||
|     request, response = app.test_client.get(f"/files/{file_name}") |     request, response = app.test_client.get(f"/files/{file_name}") | ||||||
|     assert response.status == 206 |     assert response.status == 206 | ||||||
|     assert "Content-Range" in response.headers |     assert "Content-Range" in response.headers | ||||||
|     assert response.headers["Content-Range"] == f"bytes {range.start}-{range.end}/{range.total}" |     assert ( | ||||||
|  |         response.headers["Content-Range"] | ||||||
|  |         == f"bytes {range.start}-{range.end}/{range.total}" | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_raw_response(app): | def test_raw_response(app): | ||||||
| @@ -602,3 +608,17 @@ def test_empty_response(app): | |||||||
|     request, response = app.test_client.get("/test") |     request, response = app.test_client.get("/test") | ||||||
|     assert response.content_type is None |     assert response.content_type is None | ||||||
|     assert response.body == b"" |     assert response.body == b"" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_response_body_bytes_deprecated(app): | ||||||
|  |     with warnings.catch_warnings(record=True) as w: | ||||||
|  |         warnings.simplefilter("always") | ||||||
|  |  | ||||||
|  |         HTTPResponse(body_bytes=b'bytes') | ||||||
|  |  | ||||||
|  |         assert len(w) == 1 | ||||||
|  |         assert issubclass(w[0].category, DeprecationWarning) | ||||||
|  |         assert ( | ||||||
|  |             "Parameter `body_bytes` is deprecated, use `body` instead" | ||||||
|  |             in str(w[0].message) | ||||||
|  |         ) | ||||||
|   | |||||||
| @@ -531,6 +531,19 @@ def test_add_webscoket_route(app, strict_slashes): | |||||||
|     assert ev.is_set() |     assert ev.is_set() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_add_webscoket_route_with_version(app): | ||||||
|  |     ev = asyncio.Event() | ||||||
|  |  | ||||||
|  |     async def handler(request, ws): | ||||||
|  |         assert ws.subprotocol is None | ||||||
|  |         ev.set() | ||||||
|  |  | ||||||
|  |     app.add_websocket_route(handler, "/ws", version=1) | ||||||
|  |     request, response = app.test_client.websocket("/v1/ws") | ||||||
|  |     assert response.opened is True | ||||||
|  |     assert ev.is_set() | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_route_duplicate(app): | def test_route_duplicate(app): | ||||||
|  |  | ||||||
|     with pytest.raises(RouteExists): |     with pytest.raises(RouteExists): | ||||||
| @@ -580,7 +593,7 @@ async def test_websocket_route_asgi(app): | |||||||
|     ev.clear() |     ev.clear() | ||||||
|     request, response = await app.asgi_client.websocket("/test/1") |     request, response = await app.asgi_client.websocket("/test/1") | ||||||
|     second_set = ev.is_set() |     second_set = ev.is_set() | ||||||
|     assert(first_set and second_set) |     assert first_set and second_set | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_method_not_allowed(app): | def test_method_not_allowed(app): | ||||||
|   | |||||||
| @@ -33,9 +33,7 @@ def after(app, loop): | |||||||
|     calledq.put(mock.called) |     calledq.put(mock.called) | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.skipif( | @pytest.mark.skipif(os.name == "nt", reason="May hang CI on py38/windows") | ||||||
|     os.name == "nt", reason="May hang CI on py38/windows" |  | ||||||
| ) |  | ||||||
| def test_register_system_signals(app): | def test_register_system_signals(app): | ||||||
|     """Test if sanic register system signals""" |     """Test if sanic register system signals""" | ||||||
|  |  | ||||||
| @@ -51,9 +49,7 @@ def test_register_system_signals(app): | |||||||
|     assert calledq.get() is True |     assert calledq.get() is True | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.skipif( | @pytest.mark.skipif(os.name == "nt", reason="May hang CI on py38/windows") | ||||||
|     os.name == "nt", reason="May hang CI on py38/windows" |  | ||||||
| ) |  | ||||||
| def test_dont_register_system_signals(app): | def test_dont_register_system_signals(app): | ||||||
|     """Test if sanic don't register system signals""" |     """Test if sanic don't register system signals""" | ||||||
|  |  | ||||||
| @@ -69,9 +65,7 @@ def test_dont_register_system_signals(app): | |||||||
|     assert calledq.get() is False |     assert calledq.get() is False | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.skipif( | @pytest.mark.skipif(os.name == "nt", reason="windows cannot SIGINT processes") | ||||||
|     os.name == "nt", reason="windows cannot SIGINT processes" |  | ||||||
| ) |  | ||||||
| def test_windows_workaround(): | def test_windows_workaround(): | ||||||
|     """Test Windows workaround (on any other OS)""" |     """Test Windows workaround (on any other OS)""" | ||||||
|     # At least some code coverage, even though this test doesn't work on |     # At least some code coverage, even though this test doesn't work on | ||||||
|   | |||||||
| @@ -97,9 +97,7 @@ def test_static_file_content_type(app, static_file_directory, file_name): | |||||||
| def test_static_directory(app, file_name, base_uri, static_file_directory): | def test_static_directory(app, file_name, base_uri, static_file_directory): | ||||||
|     app.static(base_uri, static_file_directory) |     app.static(base_uri, static_file_directory) | ||||||
|  |  | ||||||
|     request, response = app.test_client.get( |     request, response = app.test_client.get(uri=f"{base_uri}/{file_name}") | ||||||
|         uri=f"{base_uri}/{file_name}" |  | ||||||
|     ) |  | ||||||
|     assert response.status == 200 |     assert response.status == 200 | ||||||
|     assert response.body == get_file_content(static_file_directory, file_name) |     assert response.body == get_file_content(static_file_directory, file_name) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -9,4 +9,7 @@ def test_routes_with_host(app): | |||||||
|     assert app.url_for("hostindex") == "/" |     assert app.url_for("hostindex") == "/" | ||||||
|     assert app.url_for("hostpath") == "/path" |     assert app.url_for("hostpath") == "/path" | ||||||
|     assert app.url_for("hostindex", _external=True) == "http://example.com/" |     assert app.url_for("hostindex", _external=True) == "http://example.com/" | ||||||
|     assert app.url_for("hostpath", _external=True) == "http://path.example.com/path" |     assert ( | ||||||
|  |         app.url_for("hostpath", _external=True) | ||||||
|  |         == "http://path.example.com/path" | ||||||
|  |     ) | ||||||
|   | |||||||
| @@ -151,8 +151,7 @@ def test_with_custom_class_methods(app): | |||||||
|         def get(self, request): |         def get(self, request): | ||||||
|             self._iternal_method() |             self._iternal_method() | ||||||
|             return text( |             return text( | ||||||
|                 f"I am get method and global var " |                 f"I am get method and global var " f"is {self.global_var}" | ||||||
|                 f"is {self.global_var}" |  | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|     app.add_route(DummyView.as_view(), "/") |     app.add_route(DummyView.as_view(), "/") | ||||||
|   | |||||||
| @@ -128,9 +128,11 @@ def test_handle_quit(worker): | |||||||
|     assert not worker.alive |     assert not worker.alive | ||||||
|     assert worker.exit_code == 0 |     assert worker.exit_code == 0 | ||||||
|  |  | ||||||
|  |  | ||||||
| async def _a_noop(*a, **kw): | async def _a_noop(*a, **kw): | ||||||
|     pass |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_run_max_requests_exceeded(worker): | def test_run_max_requests_exceeded(worker): | ||||||
|     loop = asyncio.new_event_loop() |     loop = asyncio.new_event_loop() | ||||||
|     worker.ppid = 1 |     worker.ppid = 1 | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								tox.ini
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								tox.ini
									
									
									
									
									
								
							| @@ -19,7 +19,7 @@ deps = | |||||||
|     gunicorn |     gunicorn | ||||||
|     pytest-benchmark |     pytest-benchmark | ||||||
|     uvicorn |     uvicorn | ||||||
|     websockets>=7.0,<8.0 |     websockets>=8.1,<9.0 | ||||||
| commands = | commands = | ||||||
|     pytest {posargs:tests --cov sanic} |     pytest {posargs:tests --cov sanic} | ||||||
|     - coverage combine --append |     - coverage combine --append | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user