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" | ||||
|     - env: TOX_ENV=pyNightly-no-ext | ||||
|       python: 'nightly' | ||||
|       name: "Python nightly Extensions" | ||||
|       name: "Python nightly without Extensions" | ||||
|   allow_failures: | ||||
|     - env: TOX_ENV=pyNightly | ||||
|       python: 'nightly' | ||||
|       name: "Python nightly with Extensions" | ||||
|     - env: TOX_ENV=pyNightly-no-ext | ||||
|       python: 'nightly' | ||||
|       name: "Python nightly Extensions" | ||||
|       name: "Python nightly without Extensions" | ||||
| install: | ||||
|   - pip install -U tox | ||||
|   - 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 | ||||
| =============== | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
| 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 | ||||
| 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 | ||||
| string representing its type: `'request'` or `'response'`. | ||||
|  | ||||
| * Request middleware receives only the `request` as argument. | ||||
| * Response middleware receives both the `request` and `response`. | ||||
| * 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` and are executed in *reverse* order. | ||||
|  | ||||
| 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) | ||||
|  | ||||
| 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. | ||||
| 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* | ||||
| 4. The second response middleware **prevent_xss** adds the HTTP header for preventing Cross-Site-Scripting (XSS) attacks. | ||||
| 3. 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 | ||||
| ---------------- | ||||
|   | ||||
| @@ -1,3 +1,6 @@ | ||||
| import os | ||||
| import sys | ||||
|  | ||||
| from argparse import ArgumentParser | ||||
| from importlib import import_module | ||||
| from typing import Any, Dict, Optional | ||||
| @@ -6,7 +9,7 @@ from sanic.app import Sanic | ||||
| from sanic.log import logger | ||||
|  | ||||
|  | ||||
| if __name__ == "__main__": | ||||
| def main(): | ||||
|     parser = ArgumentParser(prog="sanic") | ||||
|     parser.add_argument("--host", dest="host", type=str, default="127.0.0.1") | ||||
|     parser.add_argument("--port", dest="port", type=int, default=8000) | ||||
| @@ -22,6 +25,10 @@ if __name__ == "__main__": | ||||
|     args = parser.parse_args() | ||||
|  | ||||
|     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_name = ".".join(module_parts[:-1]) | ||||
|         app_name = module_parts[-1] | ||||
| @@ -58,3 +65,7 @@ if __name__ == "__main__": | ||||
|         ) | ||||
|     except ValueError: | ||||
|         logger.exception("Failed to run app") | ||||
|  | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     main() | ||||
|   | ||||
| @@ -1 +1 @@ | ||||
| __version__ = "20.3.0" | ||||
| __version__ = "20.6.0" | ||||
|   | ||||
							
								
								
									
										166
									
								
								sanic/app.py
									
									
									
									
									
								
							
							
						
						
									
										166
									
								
								sanic/app.py
									
									
									
									
									
								
							| @@ -117,24 +117,12 @@ class Sanic: | ||||
|         :param task: future, couroutine or awaitable | ||||
|         """ | ||||
|         try: | ||||
|             if callable(task): | ||||
|                 try: | ||||
|                     self.loop.create_task(task(self)) | ||||
|                 except TypeError: | ||||
|                     self.loop.create_task(task()) | ||||
|             else: | ||||
|                 self.loop.create_task(task) | ||||
|             loop = self.loop  # Will raise SanicError if loop is not started | ||||
|             self._loop_add_task(task, self, loop) | ||||
|         except SanicException: | ||||
|  | ||||
|             @self.listener("before_server_start") | ||||
|             def run(app, loop): | ||||
|                 if callable(task): | ||||
|                     try: | ||||
|                         loop.create_task(task(self)) | ||||
|                     except TypeError: | ||||
|                         loop.create_task(task()) | ||||
|                 else: | ||||
|                     loop.create_task(task) | ||||
|             self.listener("before_server_start")( | ||||
|                 partial(self._loop_add_task, task) | ||||
|             ) | ||||
|  | ||||
|     # Decorator | ||||
|     def listener(self, event): | ||||
| @@ -462,7 +450,13 @@ class Sanic: | ||||
|  | ||||
|     # Decorator | ||||
|     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 | ||||
| @@ -493,42 +487,12 @@ class Sanic: | ||||
|                 routes, handler = handler | ||||
|             else: | ||||
|                 routes = [] | ||||
|  | ||||
|             async def websocket_handler(request, *args, **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() | ||||
|  | ||||
|             websocket_handler = partial( | ||||
|                 self._websocket_handler, handler, subprotocols=subprotocols | ||||
|             ) | ||||
|             websocket_handler.__name__ = ( | ||||
|                 "websocket_handler_" + handler.__name__ | ||||
|             ) | ||||
|             routes.extend( | ||||
|                 self.router.add( | ||||
|                     uri=uri, | ||||
| @@ -536,6 +500,7 @@ class Sanic: | ||||
|                     methods=frozenset({"GET"}), | ||||
|                     host=host, | ||||
|                     strict_slashes=strict_slashes, | ||||
|                     version=version, | ||||
|                     name=name, | ||||
|                 ) | ||||
|             ) | ||||
| @@ -550,6 +515,7 @@ class Sanic: | ||||
|         host=None, | ||||
|         strict_slashes=None, | ||||
|         subprotocols=None, | ||||
|         version=None, | ||||
|         name=None, | ||||
|     ): | ||||
|         """ | ||||
| @@ -577,6 +543,7 @@ class Sanic: | ||||
|             host=host, | ||||
|             strict_slashes=strict_slashes, | ||||
|             subprotocols=subprotocols, | ||||
|             version=version, | ||||
|             name=name, | ||||
|         )(handler) | ||||
|  | ||||
| @@ -589,10 +556,7 @@ class Sanic: | ||||
|         if not self.websocket_enabled: | ||||
|             # if the server is stopped, we want to cancel any ongoing | ||||
|             # websocket tasks, to allow the server to exit promptly | ||||
|             @self.listener("before_server_stop") | ||||
|             def cancel_websocket_tasks(app, loop): | ||||
|                 for task in self.websocket_tasks: | ||||
|                     task.cancel() | ||||
|             self.listener("before_server_stop")(self._cancel_websocket_tasks) | ||||
|  | ||||
|         self.websocket_enabled = enable | ||||
|  | ||||
| @@ -1058,16 +1022,18 @@ class Sanic: | ||||
|         self, | ||||
|         host: Optional[str] = None, | ||||
|         port: Optional[int] = None, | ||||
|         *, | ||||
|         debug: bool = False, | ||||
|         auto_reload: Optional[bool] = None, | ||||
|         ssl: Union[dict, SSLContext, None] = None, | ||||
|         sock: Optional[socket] = None, | ||||
|         workers: int = 1, | ||||
|         protocol: Type[Protocol] = None, | ||||
|         protocol: Optional[Type[Protocol]] = None, | ||||
|         backlog: int = 100, | ||||
|         stop_event: Any = None, | ||||
|         register_sys_signals: bool = True, | ||||
|         access_log: Optional[bool] = None, | ||||
|         **kwargs: Any, | ||||
|         loop: None = None, | ||||
|     ) -> None: | ||||
|         """Run the HTTP Server and listen until keyboard interrupt or term | ||||
|         signal. On termination, drain connections before closing. | ||||
| @@ -1078,6 +1044,9 @@ class Sanic: | ||||
|         :type port: int | ||||
|         :param debug: Enables debug output (slows server) | ||||
|         :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 | ||||
|                     for SSL encryption of worker(s) | ||||
|         :type ssl: SSLContext or dict | ||||
| @@ -1099,7 +1068,7 @@ class Sanic: | ||||
|         :type access_log: bool | ||||
|         :return: Nothing | ||||
|         """ | ||||
|         if "loop" in kwargs: | ||||
|         if loop is not None: | ||||
|             raise TypeError( | ||||
|                 "loop is not a valid argument. To use an existing loop, " | ||||
|                 "change to create_server().\nSee more: " | ||||
| @@ -1107,13 +1076,9 @@ class Sanic: | ||||
|                 "#asynchronous-support" | ||||
|             ) | ||||
|  | ||||
|         # Default auto_reload to false | ||||
|         auto_reload = False | ||||
|         # If debug is set, default it to true (unless on windows) | ||||
|         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 auto_reload or auto_reload is None and debug: | ||||
|             if os.environ.get("SANIC_SERVER_RUNNING") != "true": | ||||
|                 return reloader_helpers.watchdog(1.0) | ||||
|  | ||||
|         if sock is None: | ||||
|             host, port = host or "127.0.0.1", port or 8000 | ||||
| @@ -1156,18 +1121,7 @@ class Sanic: | ||||
|                 ) | ||||
|                 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: | ||||
|                 serve_multiple(server_settings, workers) | ||||
|         except BaseException: | ||||
| @@ -1189,6 +1143,7 @@ class Sanic: | ||||
|         self, | ||||
|         host: Optional[str] = None, | ||||
|         port: Optional[int] = None, | ||||
|         *, | ||||
|         debug: bool = False, | ||||
|         ssl: Union[dict, SSLContext, None] = None, | ||||
|         sock: Optional[socket] = None, | ||||
| @@ -1413,7 +1368,7 @@ class Sanic: | ||||
|             server_settings["run_async"] = True | ||||
|  | ||||
|         # Serve | ||||
|         if host and port and os.environ.get("SANIC_SERVER_RUNNING") != "true": | ||||
|         if host and port: | ||||
|             proto = "http" | ||||
|             if ssl is not None: | ||||
|                 proto = "https" | ||||
| @@ -1425,6 +1380,55 @@ class Sanic: | ||||
|         parts = [self.name, *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 | ||||
|     # -------------------------------------------------------------------- # | ||||
|   | ||||
| @@ -143,7 +143,7 @@ class Blueprint: | ||||
|             if _routes: | ||||
|                 routes += _routes | ||||
|  | ||||
|         route_names = [route.name for route in routes] | ||||
|         route_names = [route.name for route in routes if route] | ||||
|         # Middleware | ||||
|         for future in self.middlewares: | ||||
|             if future.args or future.kwargs: | ||||
|   | ||||
| @@ -3,7 +3,6 @@ import signal | ||||
| import subprocess | ||||
| import sys | ||||
|  | ||||
| from multiprocessing import Process | ||||
| from time import sleep | ||||
|  | ||||
|  | ||||
| @@ -35,101 +34,26 @@ def _iter_module_files(): | ||||
|  | ||||
| def _get_args_for_reloading(): | ||||
|     """Returns the executable.""" | ||||
|     rv = [sys.executable] | ||||
|     main_module = sys.modules["__main__"] | ||||
|     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: | ||||
|         # Parent exe was launched as a module rather than a script | ||||
|         rv.extend(["-m", mod_spec.name]) | ||||
|         if len(sys.argv) > 1: | ||||
|             rv.extend(sys.argv[1:]) | ||||
|     else: | ||||
|         rv.extend(sys.argv) | ||||
|     return rv | ||||
|         return [sys.executable, "-m", mod_spec.name] + sys.argv[1:] | ||||
|     return [sys.executable] + sys.argv | ||||
|  | ||||
|  | ||||
| def restart_with_reloader(): | ||||
|     """Create a new process and a subprocess in it with the same arguments as | ||||
|     this one. | ||||
|     """ | ||||
|     cwd = os.getcwd() | ||||
|     args = _get_args_for_reloading() | ||||
|     new_environ = os.environ.copy() | ||||
|     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}, | ||||
|     return subprocess.Popen( | ||||
|         _get_args_for_reloading(), | ||||
|         env={**os.environ, "SANIC_SERVER_RUNNING": "true"}, | ||||
|     ) | ||||
|     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): | ||||
| @@ -138,30 +62,42 @@ def watchdog(sleep_interval): | ||||
|     :param sleep_interval: interval in second. | ||||
|     :return: Nothing | ||||
|     """ | ||||
|  | ||||
|     def interrupt_self(*args): | ||||
|         raise KeyboardInterrupt | ||||
|  | ||||
|     mtimes = {} | ||||
|     signal.signal(signal.SIGTERM, interrupt_self) | ||||
|     if os.name == "nt": | ||||
|         signal.signal(signal.SIGBREAK, interrupt_self) | ||||
|  | ||||
|     worker_process = restart_with_reloader() | ||||
|     signal.signal( | ||||
|         signal.SIGTERM, lambda *args: kill_program_completly(worker_process) | ||||
|     ) | ||||
|     signal.signal( | ||||
|         signal.SIGINT, lambda *args: kill_program_completly(worker_process) | ||||
|     ) | ||||
|     while True: | ||||
|         for filename in _iter_module_files(): | ||||
|             try: | ||||
|                 mtime = os.stat(filename).st_mtime | ||||
|             except OSError: | ||||
|                 continue | ||||
|  | ||||
|             old_time = mtimes.get(filename) | ||||
|             if old_time is None: | ||||
|                 mtimes[filename] = mtime | ||||
|                 continue | ||||
|             elif mtime > old_time: | ||||
|                 kill_process_children(worker_process.pid) | ||||
|     try: | ||||
|         while True: | ||||
|             need_reload = False | ||||
|  | ||||
|             for filename in _iter_module_files(): | ||||
|                 try: | ||||
|                     mtime = os.stat(filename).st_mtime | ||||
|                 except OSError: | ||||
|                     continue | ||||
|  | ||||
|                 old_time = mtimes.get(filename) | ||||
|                 if old_time is None: | ||||
|                     mtimes[filename] = mtime | ||||
|                 elif mtime > old_time: | ||||
|                     mtimes[filename] = mtime | ||||
|                     need_reload = True | ||||
|  | ||||
|             if need_reload: | ||||
|                 worker_process.terminate() | ||||
|                 worker_process.wait() | ||||
|                 worker_process = restart_with_reloader() | ||||
|                 mtimes[filename] = mtime | ||||
|                 break | ||||
|  | ||||
|         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._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): | ||||
|         body = b"" | ||||
|         if has_message_body(self.status): | ||||
| @@ -173,7 +179,7 @@ def empty(status=204, headers=None): | ||||
|     :param status Response code. | ||||
|     :param headers Custom Headers. | ||||
|     """ | ||||
|     return HTTPResponse(body_bytes=b"", status=status, headers=headers) | ||||
|     return HTTPResponse(body=b"", status=status, headers=headers) | ||||
|  | ||||
|  | ||||
| def json( | ||||
| @@ -243,10 +249,7 @@ def raw( | ||||
|     :param content_type: the content type (string) of the response. | ||||
|     """ | ||||
|     return HTTPResponse( | ||||
|         body_bytes=body, | ||||
|         status=status, | ||||
|         headers=headers, | ||||
|         content_type=content_type, | ||||
|         body=body, 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" | ||||
|     return HTTPResponse( | ||||
|         body=out_stream, | ||||
|         status=status, | ||||
|         headers=headers, | ||||
|         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(SIGTERM, lambda s, f: sig_handler(s, f)) | ||||
|     mp = multiprocessing.get_context("fork") | ||||
|     mp = multiprocessing.get_context("spawn") | ||||
|  | ||||
|     for _ in range(workers): | ||||
|         process = mp.Process(target=serve, kwargs=server_settings) | ||||
|   | ||||
							
								
								
									
										171
									
								
								sanic/static.py
									
									
									
									
									
								
							
							
						
						
									
										171
									
								
								sanic/static.py
									
									
									
									
									
								
							| @@ -1,3 +1,4 @@ | ||||
| from functools import partial, wraps | ||||
| from mimetypes import guess_type | ||||
| from os import path | ||||
| from re import sub | ||||
| @@ -15,6 +16,89 @@ from sanic.handlers import ContentRangeHandler | ||||
| from sanic.response import HTTPResponse, file, file_stream | ||||
|  | ||||
|  | ||||
| async def _static_request_handler( | ||||
|     file_or_directory, | ||||
|     use_modified_since, | ||||
|     use_content_range, | ||||
|     stream_large_files, | ||||
|     request, | ||||
|     content_type=None, | ||||
|     file_uri=None, | ||||
| ): | ||||
|     # Using this to determine if the URL is trying to break out of the path | ||||
|     # served.  os.path.realpath seems to be very slow | ||||
|     if file_uri and "../" in file_uri: | ||||
|         raise InvalidUsage("Invalid URL") | ||||
|     # Merge served directory and requested file if provided | ||||
|     # Strip all / that in the beginning of the URL to help prevent python | ||||
|     # from herping a derp and treating the uri as an absolute path | ||||
|     root_path = file_path = file_or_directory | ||||
|     if file_uri: | ||||
|         file_path = path.join(file_or_directory, sub("^[/]*", "", file_uri)) | ||||
|  | ||||
|     # URL decode the path sent by the browser otherwise we won't be able to | ||||
|     # match filenames which got encoded (filenames with spaces etc) | ||||
|     file_path = path.abspath(unquote(file_path)) | ||||
|     if not file_path.startswith(path.abspath(unquote(root_path))): | ||||
|         raise FileNotFound( | ||||
|             "File not found", path=file_or_directory, relative_url=file_uri | ||||
|         ) | ||||
|     try: | ||||
|         headers = {} | ||||
|         # Check if the client has been sent this file before | ||||
|         # and it has not been modified since | ||||
|         stats = None | ||||
|         if use_modified_since: | ||||
|             stats = await stat_async(file_path) | ||||
|             modified_since = strftime( | ||||
|                 "%a, %d %b %Y %H:%M:%S GMT", gmtime(stats.st_mtime) | ||||
|             ) | ||||
|             if request.headers.get("If-Modified-Since") == modified_since: | ||||
|                 return HTTPResponse(status=304) | ||||
|             headers["Last-Modified"] = modified_since | ||||
|         _range = None | ||||
|         if use_content_range: | ||||
|             _range = None | ||||
|             if not stats: | ||||
|                 stats = await stat_async(file_path) | ||||
|             headers["Accept-Ranges"] = "bytes" | ||||
|             headers["Content-Length"] = str(stats.st_size) | ||||
|             if request.method != "HEAD": | ||||
|                 try: | ||||
|                     _range = ContentRangeHandler(request, stats) | ||||
|                 except HeaderNotFound: | ||||
|                     pass | ||||
|                 else: | ||||
|                     del headers["Content-Length"] | ||||
|                     for key, value in _range.headers.items(): | ||||
|                         headers[key] = value | ||||
|         headers["Content-Type"] = ( | ||||
|             content_type or guess_type(file_path)[0] or "text/plain" | ||||
|         ) | ||||
|         if request.method == "HEAD": | ||||
|             return HTTPResponse(headers=headers) | ||||
|         else: | ||||
|             if stream_large_files: | ||||
|                 if type(stream_large_files) == int: | ||||
|                     threshold = stream_large_files | ||||
|                 else: | ||||
|                     threshold = 1024 * 1024 | ||||
|  | ||||
|                 if not stats: | ||||
|                     stats = await stat_async(file_path) | ||||
|                 if stats.st_size >= threshold: | ||||
|                     return await file_stream( | ||||
|                         file_path, headers=headers, _range=_range | ||||
|                     ) | ||||
|             return await file(file_path, headers=headers, _range=_range) | ||||
|     except ContentRangeError: | ||||
|         raise | ||||
|     except Exception: | ||||
|         raise FileNotFound( | ||||
|             "File not found", path=file_or_directory, relative_url=file_uri | ||||
|         ) | ||||
|  | ||||
|  | ||||
| def register( | ||||
|     app, | ||||
|     uri, | ||||
| @@ -56,86 +140,21 @@ def register( | ||||
|     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 | ||||
|         # served.  os.path.realpath seems to be very slow | ||||
|         if file_uri and "../" in file_uri: | ||||
|             raise InvalidUsage("Invalid URL") | ||||
|         # Merge served directory and requested file if provided | ||||
|         # Strip all / that in the beginning of the URL to help prevent python | ||||
|         # from herping a derp and treating the uri as an absolute path | ||||
|         root_path = file_path = file_or_directory | ||||
|         if file_uri: | ||||
|             file_path = path.join( | ||||
|                 file_or_directory, sub("^[/]*", "", file_uri) | ||||
|             ) | ||||
|  | ||||
|         # URL decode the path sent by the browser otherwise we won't be able to | ||||
|         # match filenames which got encoded (filenames with spaces etc) | ||||
|         file_path = path.abspath(unquote(file_path)) | ||||
|         if not file_path.startswith(path.abspath(unquote(root_path))): | ||||
|             raise FileNotFound( | ||||
|                 "File not found", path=file_or_directory, relative_url=file_uri | ||||
|             ) | ||||
|         try: | ||||
|             headers = {} | ||||
|             # Check if the client has been sent this file before | ||||
|             # and it has not been modified since | ||||
|             stats = None | ||||
|             if use_modified_since: | ||||
|                 stats = await stat_async(file_path) | ||||
|                 modified_since = strftime( | ||||
|                     "%a, %d %b %Y %H:%M:%S GMT", gmtime(stats.st_mtime) | ||||
|                 ) | ||||
|                 if request.headers.get("If-Modified-Since") == modified_since: | ||||
|                     return HTTPResponse(status=304) | ||||
|                 headers["Last-Modified"] = modified_since | ||||
|             _range = None | ||||
|             if use_content_range: | ||||
|                 _range = None | ||||
|                 if not stats: | ||||
|                     stats = await stat_async(file_path) | ||||
|                 headers["Accept-Ranges"] = "bytes" | ||||
|                 headers["Content-Length"] = str(stats.st_size) | ||||
|                 if request.method != "HEAD": | ||||
|                     try: | ||||
|                         _range = ContentRangeHandler(request, stats) | ||||
|                     except HeaderNotFound: | ||||
|                         pass | ||||
|                     else: | ||||
|                         del headers["Content-Length"] | ||||
|                         for key, value in _range.headers.items(): | ||||
|                             headers[key] = value | ||||
|             headers["Content-Type"] = ( | ||||
|                 content_type or guess_type(file_path)[0] or "text/plain" | ||||
|             ) | ||||
|             if request.method == "HEAD": | ||||
|                 return HTTPResponse(headers=headers) | ||||
|             else: | ||||
|                 if stream_large_files: | ||||
|                     if type(stream_large_files) == int: | ||||
|                         threshold = stream_large_files | ||||
|                     else: | ||||
|                         threshold = 1024 * 1024 | ||||
|  | ||||
|                     if not stats: | ||||
|                         stats = await stat_async(file_path) | ||||
|                     if stats.st_size >= threshold: | ||||
|                         return await file_stream( | ||||
|                             file_path, headers=headers, _range=_range | ||||
|                         ) | ||||
|                 return await file(file_path, headers=headers, _range=_range) | ||||
|         except ContentRangeError: | ||||
|             raise | ||||
|         except Exception: | ||||
|             raise FileNotFound( | ||||
|                 "File not found", path=file_or_directory, relative_url=file_uri | ||||
|             ) | ||||
|  | ||||
|     # special prefix for static files | ||||
|     if not name.startswith("_static_"): | ||||
|         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( | ||||
|         uri, | ||||
|         methods=["GET", "HEAD"], | ||||
|   | ||||
| @@ -113,7 +113,7 @@ class WebSocketProtocol(HttpProtocol): | ||||
|  | ||||
|         # hook up the websocket protocol | ||||
|         self.websocket = WebSocketCommonProtocol( | ||||
|             timeout=self.websocket_timeout, | ||||
|             close_timeout=self.websocket_timeout, | ||||
|             max_size=self.websocket_max_size, | ||||
|             max_queue=self.websocket_max_queue, | ||||
|             read_limit=self.websocket_read_limit, | ||||
|   | ||||
							
								
								
									
										12
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								setup.py
									
									
									
									
									
								
							| @@ -5,7 +5,6 @@ import codecs | ||||
| import os | ||||
| import re | ||||
| import sys | ||||
|  | ||||
| from distutils.util import strtobool | ||||
|  | ||||
| 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: | ||||
|     try: | ||||
|         version = re.findall( | ||||
|             r"^__version__ = \"([^']+)\"\r?$", fp.read(), re.M | ||||
|         )[0] | ||||
|         version = re.findall(r"^__version__ = \"([^']+)\"\r?$", fp.read(), re.M)[0] | ||||
|     except IndexError: | ||||
|         raise RuntimeError("Unable to determine version.") | ||||
|  | ||||
| @@ -70,11 +67,10 @@ setup_kwargs = { | ||||
|         "Programming Language :: Python :: 3.7", | ||||
|         "Programming Language :: Python :: 3.8", | ||||
|     ], | ||||
|     "entry_points": {"console_scripts": ["sanic = sanic.__main__:main"]}, | ||||
| } | ||||
|  | ||||
| env_dependency = ( | ||||
|     '; sys_platform != "win32" ' 'and implementation_name == "cpython"' | ||||
| ) | ||||
| env_dependency = '; sys_platform != "win32" ' 'and implementation_name == "cpython"' | ||||
| ujson = "ujson>=1.35" + env_dependency | ||||
| uvloop = "uvloop>=0.5.3" + env_dependency | ||||
|  | ||||
| @@ -83,7 +79,7 @@ requirements = [ | ||||
|     uvloop, | ||||
|     ujson, | ||||
|     "aiofiles>=0.3.0", | ||||
|     "websockets>=7.0,<9.0", | ||||
|     "websockets>=8.1,<9.0", | ||||
|     "multidict>=4.0,<5.0", | ||||
|     "httpx==0.11.1", | ||||
| ] | ||||
|   | ||||
| @@ -56,6 +56,7 @@ def test_asyncio_server_no_start_serving(app): | ||||
|         srv = loop.run_until_complete(asyncio_srv_coro) | ||||
|         assert srv.is_serving() is False | ||||
|  | ||||
|  | ||||
| @pytest.mark.skipif( | ||||
|     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) | ||||
|         # Looks like we can't easily test `serve_forever()` | ||||
|  | ||||
|  | ||||
| def test_app_loop_not_running(app): | ||||
|     with pytest.raises(SanicException) as excinfo: | ||||
|         app.loop | ||||
| @@ -125,7 +127,10 @@ def test_app_handle_request_handler_is_none(app, monkeypatch): | ||||
|  | ||||
|     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]) | ||||
|   | ||||
| @@ -84,8 +84,8 @@ def test_listeners_triggered(app): | ||||
|  | ||||
|     all_tasks = ( | ||||
|         asyncio.Task.all_tasks() | ||||
|         if sys.version_info < (3, 7) else | ||||
|         asyncio.all_tasks(asyncio.get_event_loop()) | ||||
|         if sys.version_info < (3, 7) | ||||
|         else asyncio.all_tasks(asyncio.get_event_loop()) | ||||
|     ) | ||||
|     for task in all_tasks: | ||||
|         task.cancel() | ||||
| @@ -134,8 +134,8 @@ def test_listeners_triggered_async(app): | ||||
|  | ||||
|     all_tasks = ( | ||||
|         asyncio.Task.all_tasks() | ||||
|         if sys.version_info < (3, 7) else | ||||
|         asyncio.all_tasks(asyncio.get_event_loop()) | ||||
|         if sys.version_info < (3, 7) | ||||
|         else asyncio.all_tasks(asyncio.get_event_loop()) | ||||
|     ) | ||||
|     for task in all_tasks: | ||||
|         task.cancel() | ||||
|   | ||||
| @@ -252,6 +252,76 @@ def test_several_bp_with_host(app): | ||||
|     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): | ||||
|     blueprint = Blueprint("test_bp_middleware") | ||||
|  | ||||
| @@ -270,24 +340,31 @@ def test_bp_middleware(app): | ||||
|     assert response.status == 200 | ||||
|     assert response.text == "FAIL" | ||||
|  | ||||
|  | ||||
| def test_bp_middleware_order(app): | ||||
|     blueprint = Blueprint("test_bp_middleware_order") | ||||
|     order = list() | ||||
|  | ||||
|     @blueprint.middleware("request") | ||||
|     def mw_1(request): | ||||
|         order.append(1) | ||||
|  | ||||
|     @blueprint.middleware("request") | ||||
|     def mw_2(request): | ||||
|         order.append(2) | ||||
|  | ||||
|     @blueprint.middleware("request") | ||||
|     def mw_3(request): | ||||
|         order.append(3) | ||||
|  | ||||
|     @blueprint.middleware("response") | ||||
|     def mw_4(request, response): | ||||
|         order.append(6) | ||||
|  | ||||
|     @blueprint.middleware("response") | ||||
|     def mw_5(request, response): | ||||
|         order.append(5) | ||||
|  | ||||
|     @blueprint.middleware("response") | ||||
|     def mw_6(request, response): | ||||
|         order.append(4) | ||||
| @@ -303,6 +380,7 @@ def test_bp_middleware_order(app): | ||||
|     assert response.status == 200 | ||||
|     assert order == [1, 2, 3, 4, 5, 6] | ||||
|  | ||||
|  | ||||
| def test_bp_exception_handler(app): | ||||
|     blueprint = Blueprint("test_middleware") | ||||
|  | ||||
| @@ -585,9 +663,7 @@ def test_bp_group_with_default_url_prefix(app): | ||||
|     from uuid import uuid4 | ||||
|  | ||||
|     resource_id = str(uuid4()) | ||||
|     request, response = app.test_client.get( | ||||
|         f"/api/v1/resources/{resource_id}" | ||||
|     ) | ||||
|     request, response = app.test_client.get(f"/api/v1/resources/{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.testing import HOST, SanicTestClient | ||||
|  | ||||
|  | ||||
| CONFIG_FOR_TESTS = {"KEEP_ALIVE_TIMEOUT": 2, "KEEP_ALIVE": True} | ||||
|  | ||||
| old_conn = None | ||||
| @@ -46,7 +47,7 @@ class ReusableSanicConnectionPool( | ||||
|                 cert=self.cert, | ||||
|                 verify=self.verify, | ||||
|                 trust_env=self.trust_env, | ||||
|                 http2=self.http2 | ||||
|                 http2=self.http2, | ||||
|             ) | ||||
|             connection = httpx.dispatch.connection.HTTPConnection( | ||||
|                 origin, | ||||
| @@ -166,9 +167,7 @@ class ReuseableSanicTestClient(SanicTestClient): | ||||
|             try: | ||||
|                 return results[-1] | ||||
|             except Exception: | ||||
|                 raise ValueError( | ||||
|                     f"Request object expected, got ({results})" | ||||
|                 ) | ||||
|                 raise ValueError(f"Request object expected, got ({results})") | ||||
|  | ||||
|     def kill_server(self): | ||||
|         try: | ||||
|   | ||||
| @@ -87,3 +87,14 @@ def test_pickle_app_with_bp(app, protocol): | ||||
|     request, response = up_p_app.test_client.get("/") | ||||
|     assert up_p_app.is_request_stream is False | ||||
|     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.text == data | ||||
|  | ||||
|  | ||||
| def test_streaming_new_api(app): | ||||
|     @app.post("/non-stream") | ||||
|     async def handler(request): | ||||
|   | ||||
| @@ -17,7 +17,7 @@ class DelayableHTTPConnection(httpx.dispatch.connection.HTTPConnection): | ||||
|     async def send(self, request, timeout=None): | ||||
|  | ||||
|         if self.connection is None: | ||||
|             self.connection = (await self.connect(timeout=timeout)) | ||||
|             self.connection = await self.connect(timeout=timeout) | ||||
|  | ||||
|         if self._request_delay: | ||||
|             await asyncio.sleep(self._request_delay) | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import asyncio | ||||
| import inspect | ||||
| import os | ||||
| import warnings | ||||
|  | ||||
| from collections import namedtuple | ||||
| 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( | ||||
|     non_chunked_streaming_app | ||||
|     non_chunked_streaming_app, | ||||
| ): | ||||
|     request, response = non_chunked_streaming_app.test_client.get("/") | ||||
|     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]) | ||||
| def test_stream_response_keep_alive_returns_correct_headers( | ||||
|     keep_alive_timeout | ||||
|     keep_alive_timeout, | ||||
| ): | ||||
|     response = StreamingHTTPResponse(sample_streaming_fn) | ||||
|     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( | ||||
|     streaming_app | ||||
|     streaming_app, | ||||
| ): | ||||
|     response = StreamingHTTPResponse(sample_streaming_fn) | ||||
|     response.protocol = MagicMock(HttpProtocol) | ||||
| @@ -434,9 +435,10 @@ def test_file_response_custom_filename( | ||||
|     request, response = app.test_client.get(f"/files/{source}") | ||||
|     assert response.status == 200 | ||||
|     assert response.body == get_file_content(static_file_directory, source) | ||||
|     assert response.headers[ | ||||
|         "Content-Disposition" | ||||
|     ] == f'attachment; filename="{dest}"' | ||||
|     assert ( | ||||
|         response.headers["Content-Disposition"] | ||||
|         == f'attachment; filename="{dest}"' | ||||
|     ) | ||||
|  | ||||
|  | ||||
| @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}") | ||||
|     assert response.status == 200 | ||||
|     assert response.body == get_file_content(static_file_directory, source) | ||||
|     assert response.headers[ | ||||
|         "Content-Disposition" | ||||
|     ] == f'attachment; filename="{dest}"' | ||||
|     assert ( | ||||
|         response.headers["Content-Disposition"] | ||||
|         == f'attachment; filename="{dest}"' | ||||
|     ) | ||||
|  | ||||
|  | ||||
| @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}") | ||||
|     assert response.status == 206 | ||||
|     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): | ||||
| @@ -602,3 +608,17 @@ def test_empty_response(app): | ||||
|     request, response = app.test_client.get("/test") | ||||
|     assert response.content_type is None | ||||
|     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() | ||||
|  | ||||
|  | ||||
| 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): | ||||
|  | ||||
|     with pytest.raises(RouteExists): | ||||
| @@ -580,7 +593,7 @@ async def test_websocket_route_asgi(app): | ||||
|     ev.clear() | ||||
|     request, response = await app.asgi_client.websocket("/test/1") | ||||
|     second_set = ev.is_set() | ||||
|     assert(first_set and second_set) | ||||
|     assert first_set and second_set | ||||
|  | ||||
|  | ||||
| def test_method_not_allowed(app): | ||||
|   | ||||
| @@ -33,9 +33,7 @@ def after(app, loop): | ||||
|     calledq.put(mock.called) | ||||
|  | ||||
|  | ||||
| @pytest.mark.skipif( | ||||
|     os.name == "nt", reason="May hang CI on py38/windows" | ||||
| ) | ||||
| @pytest.mark.skipif(os.name == "nt", reason="May hang CI on py38/windows") | ||||
| def test_register_system_signals(app): | ||||
|     """Test if sanic register system signals""" | ||||
|  | ||||
| @@ -51,9 +49,7 @@ def test_register_system_signals(app): | ||||
|     assert calledq.get() is True | ||||
|  | ||||
|  | ||||
| @pytest.mark.skipif( | ||||
|     os.name == "nt", reason="May hang CI on py38/windows" | ||||
| ) | ||||
| @pytest.mark.skipif(os.name == "nt", reason="May hang CI on py38/windows") | ||||
| def test_dont_register_system_signals(app): | ||||
|     """Test if sanic don't register system signals""" | ||||
|  | ||||
| @@ -69,9 +65,7 @@ def test_dont_register_system_signals(app): | ||||
|     assert calledq.get() is False | ||||
|  | ||||
|  | ||||
| @pytest.mark.skipif( | ||||
|     os.name == "nt", reason="windows cannot SIGINT processes" | ||||
| ) | ||||
| @pytest.mark.skipif(os.name == "nt", reason="windows cannot SIGINT processes") | ||||
| def test_windows_workaround(): | ||||
|     """Test Windows workaround (on any other OS)""" | ||||
|     # 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): | ||||
|     app.static(base_uri, static_file_directory) | ||||
|  | ||||
|     request, response = app.test_client.get( | ||||
|         uri=f"{base_uri}/{file_name}" | ||||
|     ) | ||||
|     request, response = app.test_client.get(uri=f"{base_uri}/{file_name}") | ||||
|     assert response.status == 200 | ||||
|     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("hostpath") == "/path" | ||||
|     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): | ||||
|             self._iternal_method() | ||||
|             return text( | ||||
|                 f"I am get method and global var " | ||||
|                 f"is {self.global_var}" | ||||
|                 f"I am get method and global var " f"is {self.global_var}" | ||||
|             ) | ||||
|  | ||||
|     app.add_route(DummyView.as_view(), "/") | ||||
|   | ||||
| @@ -128,9 +128,11 @@ def test_handle_quit(worker): | ||||
|     assert not worker.alive | ||||
|     assert worker.exit_code == 0 | ||||
|  | ||||
|  | ||||
| async def _a_noop(*a, **kw): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| def test_run_max_requests_exceeded(worker): | ||||
|     loop = asyncio.new_event_loop() | ||||
|     worker.ppid = 1 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user