Merge in latest from sanic-routing branch
This commit is contained in:
		
							
								
								
									
										67
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| # For most projects, this workflow file will not need changing; you simply need | ||||
| # to commit it to your repository. | ||||
| # | ||||
| # You may wish to alter this file to override the set of languages analyzed, | ||||
| # or to provide custom queries or build logic. | ||||
| # | ||||
| # ******** NOTE ******** | ||||
| # We have attempted to detect the languages in your repository. Please check | ||||
| # the `language` matrix defined below to confirm you have the correct set of | ||||
| # supported CodeQL languages. | ||||
| # | ||||
| name: "CodeQL" | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: [ master ] | ||||
|   pull_request: | ||||
|     # The branches below must be a subset of the branches above | ||||
|     branches: [ master ] | ||||
|   schedule: | ||||
|     - cron: '25 16 * * 0' | ||||
|  | ||||
| jobs: | ||||
|   analyze: | ||||
|     name: Analyze | ||||
|     runs-on: ubuntu-latest | ||||
|  | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|         language: [ 'python' ] | ||||
|         # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] | ||||
|         # Learn more: | ||||
|         # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed | ||||
|  | ||||
|     steps: | ||||
|     - name: Checkout repository | ||||
|       uses: actions/checkout@v2 | ||||
|  | ||||
|     # Initializes the CodeQL tools for scanning. | ||||
|     - name: Initialize CodeQL | ||||
|       uses: github/codeql-action/init@v1 | ||||
|       with: | ||||
|         languages: ${{ matrix.language }} | ||||
|         # If you wish to specify custom queries, you can do so here or in a config file. | ||||
|         # By default, queries listed here will override any specified in a config file. | ||||
|         # Prefix the list here with "+" to use these queries and those in the config file. | ||||
|         # queries: ./path/to/local/query, your-org/your-repo/queries@main | ||||
|  | ||||
|     # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java). | ||||
|     # If this step fails, then you should remove it and run the build manually (see below) | ||||
|     - name: Autobuild | ||||
|       uses: github/codeql-action/autobuild@v1 | ||||
|  | ||||
|     # ℹ️ Command-line programs to run using the OS shell. | ||||
|     # 📚 https://git.io/JvXDl | ||||
|  | ||||
|     # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines | ||||
|     #    and modify them (or add more) to build your code if your project | ||||
|     #    uses a compiled language | ||||
|  | ||||
|     #- run: | | ||||
|     #   make bootstrap | ||||
|     #   make release | ||||
|  | ||||
|     - name: Perform CodeQL Analysis | ||||
|       uses: github/codeql-action/analyze@v1 | ||||
							
								
								
									
										7
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										7
									
								
								Makefile
									
									
									
									
									
								
							| @@ -63,17 +63,14 @@ ifdef include_tests | ||||
| 	isort -rc sanic tests | ||||
| else | ||||
| 	$(info Sorting Imports) | ||||
| 	isort -rc sanic tests | ||||
| 	isort -rc sanic tests  --profile=black | ||||
| endif | ||||
| endif | ||||
|  | ||||
| black: | ||||
| 	black --config ./.black.toml sanic tests | ||||
|  | ||||
| isort: | ||||
| 	isort sanic tests | ||||
|  | ||||
| pretty: black isort | ||||
| pretty: beautify | ||||
|  | ||||
| docs-clean: | ||||
| 	cd docs && make clean | ||||
|   | ||||
| @@ -6,14 +6,15 @@ Sanic releases long term support release once a year in December. LTS releases r | ||||
|  | ||||
| | Version | LTS           | Supported          | | ||||
| | ------- | ------------- | ------------------ | | ||||
| | 20.9    |               | :heavy_check_mark: | | ||||
| | 20.12   | until 2022-12 | :heavy_check_mark: | | ||||
| | 20.9    |               | :x:                | | ||||
| | 20.6    |               | :x:                | | ||||
| | 20.3    |               | :x:                | | ||||
| | 19.12   | until 2021-12 | :white_check_mark: | | ||||
| | 19.9    |               | :x:                | | ||||
| | 19.6    |               | :x:                | | ||||
| | 19.3    |               | :x:                | | ||||
| | 18.12   | until 2020-12 | :white_check_mark: | | ||||
| | 18.12   |               | :x:                | | ||||
| | 0.8.3   |               | :x:                | | ||||
| | 0.7.0   |               | :x:                | | ||||
| | 0.6.0   |               | :x:                | | ||||
|   | ||||
							
								
								
									
										339
									
								
								sanic/app.py
									
									
									
									
									
								
							
							
						
						
									
										339
									
								
								sanic/app.py
									
									
									
									
									
								
							| @@ -25,27 +25,24 @@ from typing import ( | ||||
| ) | ||||
| from urllib.parse import urlencode, urlunparse | ||||
|  | ||||
| from sanic_routing.route import Route | ||||
| from sanic_routing.exceptions import FinalizationError  # type: ignore | ||||
| from sanic_routing.route import Route  # type: ignore | ||||
|  | ||||
| from sanic import reloader_helpers | ||||
| from sanic.asgi import ASGIApp | ||||
| from sanic.base import BaseSanic | ||||
| from sanic.blueprint_group import BlueprintGroup | ||||
| from sanic.blueprints import Blueprint | ||||
| from sanic.config import BASE_LOGO, Config | ||||
| from sanic.exceptions import ( | ||||
|     InvalidUsage, | ||||
|     NotFound, | ||||
|     SanicException, | ||||
|     ServerError, | ||||
|     URLBuildError, | ||||
| ) | ||||
| from sanic.handlers import ErrorHandler, ListenerType, MiddlewareType | ||||
| from sanic.log import LOGGING_CONFIG_DEFAULTS, error_logger, logger | ||||
| from sanic.mixins.base import BaseMixin | ||||
| from sanic.mixins.exceptions import ExceptionMixin | ||||
| from sanic.mixins.listeners import ListenerEvent, ListenerMixin | ||||
| from sanic.mixins.middleware import MiddlewareMixin | ||||
| from sanic.mixins.routes import RouteMixin | ||||
| from sanic.mixins.listeners import ListenerEvent | ||||
| from sanic.models.futures import ( | ||||
|     FutureException, | ||||
|     FutureListener, | ||||
| @@ -68,9 +65,7 @@ from sanic.static import register as static_register | ||||
| from sanic.websocket import ConnectionClosed, WebSocketProtocol | ||||
|  | ||||
|  | ||||
| class Sanic( | ||||
|     BaseMixin, RouteMixin, MiddlewareMixin, ListenerMixin, ExceptionMixin | ||||
| ): | ||||
| class Sanic(BaseSanic): | ||||
|     """ | ||||
|     The main application instance | ||||
|     """ | ||||
| @@ -103,9 +98,7 @@ class Sanic( | ||||
|  | ||||
|         self.name = name | ||||
|         self.asgi = False | ||||
|         self.router = router or Router( | ||||
|             exception=NotFound, method_handler_exception=NotFound | ||||
|         ) | ||||
|         self.router = router or Router() | ||||
|         self.request_class = request_class | ||||
|         self.error_handler = error_handler or ErrorHandler() | ||||
|         self.config = Config(load_env=load_env) | ||||
| @@ -124,6 +117,9 @@ class Sanic( | ||||
|         self.websocket_tasks: Set[Future] = set() | ||||
|         self.named_request_middleware: Dict[str, Deque[MiddlewareType]] = {} | ||||
|         self.named_response_middleware: Dict[str, Deque[MiddlewareType]] = {} | ||||
|         # self.named_request_middleware: Dict[str, MiddlewareType] = {} | ||||
|         # self.named_response_middleware: Dict[str, MiddlewareType] = {} | ||||
|         self._test_manager = None | ||||
|         self._test_client = None | ||||
|         self._asgi_client = None | ||||
|         # Register alternative method names | ||||
| @@ -135,6 +131,8 @@ class Sanic( | ||||
|         if self.config.REGISTER: | ||||
|             self.__class__.register_app(self) | ||||
|  | ||||
|         self.router.ctx.app = self | ||||
|  | ||||
|     @property | ||||
|     def loop(self): | ||||
|         """ | ||||
| @@ -175,10 +173,6 @@ class Sanic( | ||||
|                 partial(self._loop_add_task, task) | ||||
|             ) | ||||
|  | ||||
|     # Decorator | ||||
|     def _apply_listener(self, listener: FutureListener): | ||||
|         return self.register_listener(listener.listener, listener.event) | ||||
|  | ||||
|     def register_listener(self, listener: Callable, event: str) -> Any: | ||||
|         """ | ||||
|         Register the listener for a given event. | ||||
| @@ -197,42 +191,6 @@ class Sanic( | ||||
|         self.listeners[_event].append(listener) | ||||
|         return listener | ||||
|  | ||||
|     def _apply_route(self, route: FutureRoute) -> Route: | ||||
|         return self.router.add(**route._asdict()) | ||||
|  | ||||
|     def _apply_static(self, static: FutureStatic) -> Route: | ||||
|         return static_register(self, static) | ||||
|  | ||||
|     def enable_websocket(self, enable: bool = True): | ||||
|         """ | ||||
|         Enable or disable the support for websocket. | ||||
|  | ||||
|         Websocket is enabled automatically if websocket routes are | ||||
|         added to the application. | ||||
|         """ | ||||
|         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")(self._cancel_websocket_tasks) | ||||
|  | ||||
|         self.websocket_enabled = enable | ||||
|  | ||||
|     # Decorator | ||||
|     def _apply_exception_handler(self, handler: FutureException): | ||||
|         """Decorate a function to be registered as a handler for exceptions | ||||
|  | ||||
|         :param exceptions: exceptions | ||||
|         :return: decorated function | ||||
|         """ | ||||
|  | ||||
|         for exception in handler.exceptions: | ||||
|             if isinstance(exception, (tuple, list)): | ||||
|                 for e in exception: | ||||
|                     self.error_handler.add(e, handler.handler) | ||||
|             else: | ||||
|                 self.error_handler.add(exception, handler.handler) | ||||
|         return handler | ||||
|  | ||||
|     def register_middleware(self, middleware, attach_to: str = "request"): | ||||
|         """ | ||||
|         Register an application level middleware that will be attached | ||||
| @@ -288,13 +246,49 @@ class Sanic( | ||||
|                 if middleware not in self.named_response_middleware[_rn]: | ||||
|                     self.named_response_middleware[_rn].appendleft(middleware) | ||||
|  | ||||
|     # Decorator | ||||
|     def _apply_exception_handler(self, handler: FutureException): | ||||
|         """Decorate a function to be registered as a handler for exceptions | ||||
|  | ||||
|         :param exceptions: exceptions | ||||
|         :return: decorated function | ||||
|         """ | ||||
|  | ||||
|         for exception in handler.exceptions: | ||||
|             if isinstance(exception, (tuple, list)): | ||||
|                 for e in exception: | ||||
|                     self.error_handler.add(e, handler.handler) | ||||
|             else: | ||||
|                 self.error_handler.add(exception, handler.handler) | ||||
|         return handler | ||||
|  | ||||
|     def _apply_listener(self, listener: FutureListener): | ||||
|         return self.register_listener(listener.listener, listener.event) | ||||
|  | ||||
|     def _apply_route(self, route: FutureRoute) -> Route: | ||||
|         params = route._asdict() | ||||
|         websocket = params.pop("websocket", False) | ||||
|         subprotocols = params.pop("subprotocols", None) | ||||
|  | ||||
|         if websocket: | ||||
|             self.enable_websocket() | ||||
|             websocket_handler = partial( | ||||
|                 self._websocket_handler, | ||||
|                 route.handler, | ||||
|                 subprotocols=subprotocols, | ||||
|             ) | ||||
|             websocket_handler.__name__ = route.handler.__name__  # type: ignore | ||||
|             websocket_handler.is_websocket = True  # type: ignore | ||||
|             params["handler"] = websocket_handler | ||||
|         return self.router.add(**params) | ||||
|  | ||||
|     def _apply_static(self, static: FutureStatic) -> Route: | ||||
|         return static_register(self, static) | ||||
|  | ||||
|     def _apply_middleware( | ||||
|         self, | ||||
|         middleware: FutureMiddleware, | ||||
|         route_names: Optional[List[str]] = None, | ||||
|     ): | ||||
|         print(f"{middleware=}") | ||||
|         if route_names: | ||||
|             return self.register_named_middleware( | ||||
|                 middleware.middleware, route_names, middleware.attach_to | ||||
| @@ -304,6 +298,19 @@ class Sanic( | ||||
|                 middleware.middleware, middleware.attach_to | ||||
|             ) | ||||
|  | ||||
|     def enable_websocket(self, enable=True): | ||||
|         """Enable or disable the support for websocket. | ||||
|  | ||||
|         Websocket is enabled automatically if websocket routes are | ||||
|         added to the application. | ||||
|         """ | ||||
|         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")(self._cancel_websocket_tasks) | ||||
|  | ||||
|         self.websocket_enabled = enable | ||||
|  | ||||
|     def blueprint(self, blueprint, **options): | ||||
|         """Register a blueprint on the application. | ||||
|  | ||||
| @@ -323,6 +330,12 @@ class Sanic( | ||||
|         else: | ||||
|             self.blueprints[blueprint.name] = blueprint | ||||
|             self._blueprint_order.append(blueprint) | ||||
|  | ||||
|         if ( | ||||
|             self.strict_slashes is not None | ||||
|             and blueprint.strict_slashes is None | ||||
|         ): | ||||
|             blueprint.strict_slashes = self.strict_slashes | ||||
|         blueprint.register(self, options) | ||||
|  | ||||
|     def url_for(self, view_name: str, **kwargs): | ||||
| @@ -351,30 +364,28 @@ class Sanic( | ||||
|         # find the route by the supplied view name | ||||
|         kw: Dict[str, str] = {} | ||||
|         # special static files url_for | ||||
|         if view_name == "static": | ||||
|             kw.update(name=kwargs.pop("name", "static")) | ||||
|         elif view_name.endswith(".static"):  # blueprint.static | ||||
|             kwargs.pop("name", None) | ||||
|  | ||||
|         if "." not in view_name: | ||||
|             view_name = f"{self.name}.{view_name}" | ||||
|  | ||||
|         if view_name.endswith(".static"): | ||||
|             name = kwargs.pop("name", None) | ||||
|             if name: | ||||
|                 view_name = view_name.replace("static", name) | ||||
|             kw.update(name=view_name) | ||||
|  | ||||
|         uri, route = self.router.find_route_by_view_name(view_name, **kw) | ||||
|         if not (uri and route): | ||||
|         route = self.router.find_route_by_view_name(view_name, **kw) | ||||
|         if not route: | ||||
|             raise URLBuildError( | ||||
|                 f"Endpoint with name `{view_name}` was not found" | ||||
|             ) | ||||
|  | ||||
|         # If the route has host defined, split that off | ||||
|         # TODO: Retain netloc and path separately in Route objects | ||||
|         host = uri.find("/") | ||||
|         if host > 0: | ||||
|             host, uri = uri[:host], uri[host:] | ||||
|         else: | ||||
|             host = None | ||||
|         uri = route.path | ||||
|  | ||||
|         if view_name == "static" or view_name.endswith(".static"): | ||||
|             filename = kwargs.pop("filename", None) | ||||
|         if getattr(route.ctx, "static", None): | ||||
|             filename = kwargs.pop("filename", "") | ||||
|             # it's static folder | ||||
|             if "<file_uri:" in uri: | ||||
|             if "file_uri" in uri: | ||||
|                 folder_ = uri.split("<file_uri:", 1)[0] | ||||
|                 if folder_.endswith("/"): | ||||
|                     folder_ = folder_[:-1] | ||||
| @@ -382,22 +393,36 @@ class Sanic( | ||||
|                 if filename.startswith("/"): | ||||
|                     filename = filename[1:] | ||||
|  | ||||
|                 uri = f"{folder_}/{filename}" | ||||
|                 kwargs["file_uri"] = filename | ||||
|  | ||||
|         if uri != "/" and uri.endswith("/"): | ||||
|             uri = uri[:-1] | ||||
|  | ||||
|         out = uri | ||||
|         if not uri.startswith("/"): | ||||
|             uri = f"/{uri}" | ||||
|  | ||||
|         # find all the parameters we will need to build in the URL | ||||
|         matched_params = re.findall(self.router.parameter_pattern, uri) | ||||
|         out = uri | ||||
|  | ||||
|         # _method is only a placeholder now, don't know how to support it | ||||
|         kwargs.pop("_method", None) | ||||
|         anchor = kwargs.pop("_anchor", "") | ||||
|         # _external need SERVER_NAME in config or pass _server arg | ||||
|         external = kwargs.pop("_external", False) | ||||
|         host = kwargs.pop("_host", None) | ||||
|         external = kwargs.pop("_external", False) or bool(host) | ||||
|         scheme = kwargs.pop("_scheme", "") | ||||
|         if route.ctx.hosts and external: | ||||
|             if not host and len(route.ctx.hosts) > 1: | ||||
|                 raise ValueError( | ||||
|                     f"Host is ambiguous: {', '.join(route.ctx.hosts)}" | ||||
|                 ) | ||||
|             elif host and host not in route.ctx.hosts: | ||||
|                 raise ValueError( | ||||
|                     f"Requested host ({host}) is not available for this " | ||||
|                     f"route: {route.ctx.hosts}" | ||||
|                 ) | ||||
|             elif not host: | ||||
|                 host = list(route.ctx.hosts)[0] | ||||
|  | ||||
|         if scheme and not external: | ||||
|             raise ValueError("When specifying _scheme, _external must be True") | ||||
|  | ||||
| @@ -415,44 +440,44 @@ class Sanic( | ||||
|             if "://" in netloc[:8]: | ||||
|                 netloc = netloc.split("://", 1)[-1] | ||||
|  | ||||
|         for match in matched_params: | ||||
|             name, _type, pattern = self.router.parse_parameter_string(match) | ||||
|         # find all the parameters we will need to build in the URL | ||||
|         # matched_params = re.findall(self.router.parameter_pattern, uri) | ||||
|         route.finalize() | ||||
|         for param_info in route.params.values(): | ||||
|             # name, _type, pattern = self.router.parse_parameter_string(match) | ||||
|             # we only want to match against each individual parameter | ||||
|             specific_pattern = f"^{pattern}$" | ||||
|             supplied_param = None | ||||
|  | ||||
|             if name in kwargs: | ||||
|                 supplied_param = kwargs.get(name) | ||||
|                 del kwargs[name] | ||||
|             else: | ||||
|             try: | ||||
|                 supplied_param = str(kwargs.pop(param_info.name)) | ||||
|             except KeyError: | ||||
|                 raise URLBuildError( | ||||
|                     f"Required parameter `{name}` was not passed to url_for" | ||||
|                     f"Required parameter `{param_info.name}` was not " | ||||
|                     "passed to url_for" | ||||
|                 ) | ||||
|  | ||||
|             supplied_param = str(supplied_param) | ||||
|             # determine if the parameter supplied by the caller passes the test | ||||
|             # in the URL | ||||
|             passes_pattern = re.match(specific_pattern, supplied_param) | ||||
|  | ||||
|             if not passes_pattern: | ||||
|                 if _type != str: | ||||
|                     type_name = _type.__name__ | ||||
|  | ||||
|                     msg = ( | ||||
|                         f'Value "{supplied_param}" ' | ||||
|                         f"for parameter `{name}` does not " | ||||
|                         f"match pattern for type `{type_name}`: {pattern}" | ||||
|                     ) | ||||
|                 else: | ||||
|                     msg = ( | ||||
|                         f'Value "{supplied_param}" for parameter `{name}` ' | ||||
|                         f"does not satisfy pattern {pattern}" | ||||
|                     ) | ||||
|                 raise URLBuildError(msg) | ||||
|             # determine if the parameter supplied by the caller | ||||
|             # passes the test in the URL | ||||
|             if param_info.pattern: | ||||
|                 passes_pattern = param_info.pattern.match(supplied_param) | ||||
|                 if not passes_pattern: | ||||
|                     if param_info.cast != str: | ||||
|                         msg = ( | ||||
|                             f'Value "{supplied_param}" ' | ||||
|                             f"for parameter `{param_info.name}` does " | ||||
|                             "not match pattern for type " | ||||
|                             f"`{param_info.cast.__name__}`: " | ||||
|                             f"{param_info.pattern.pattern}" | ||||
|                         ) | ||||
|                     else: | ||||
|                         msg = ( | ||||
|                             f'Value "{supplied_param}" for parameter ' | ||||
|                             f"`{param_info.name}` does not satisfy " | ||||
|                             f"pattern {param_info.pattern.pattern}" | ||||
|                         ) | ||||
|                     raise URLBuildError(msg) | ||||
|  | ||||
|             # replace the parameter in the URL with the supplied value | ||||
|             replacement_regex = f"(<{name}.*?>)" | ||||
|  | ||||
|             replacement_regex = f"(<{param_info.name}.*?>)" | ||||
|             out = re.sub(replacement_regex, supplied_param, out) | ||||
|  | ||||
|         # parse the remainder of the keyword arguments into a querystring | ||||
| @@ -545,14 +570,13 @@ class Sanic( | ||||
|             # Fetch handler from router | ||||
|             ( | ||||
|                 handler, | ||||
|                 args, | ||||
|                 kwargs, | ||||
|                 uri, | ||||
|                 name, | ||||
|                 endpoint, | ||||
|                 ignore_body, | ||||
|             ) = self.router.get(request) | ||||
|             request.name = name | ||||
|             request._match_info = kwargs | ||||
|  | ||||
|             if ( | ||||
|                 request.stream | ||||
| @@ -578,7 +602,7 @@ class Sanic( | ||||
|                 # Execute Handler | ||||
|                 # -------------------------------------------- # | ||||
|  | ||||
|                 request.uri_template = uri | ||||
|                 request.uri_template = f"/{uri}" | ||||
|                 if handler is None: | ||||
|                     raise ServerError( | ||||
|                         ( | ||||
| @@ -587,10 +611,10 @@ class Sanic( | ||||
|                         ) | ||||
|                     ) | ||||
|  | ||||
|                 request.endpoint = endpoint | ||||
|                 request.endpoint = request.name | ||||
|  | ||||
|                 # Run response handler | ||||
|                 response = handler(request, *args, **kwargs) | ||||
|                 response = handler(request, **kwargs) | ||||
|                 if isawaitable(response): | ||||
|                     response = await response | ||||
|             if response: | ||||
| @@ -615,26 +639,60 @@ class Sanic( | ||||
|         except CancelledError: | ||||
|             raise | ||||
|         except Exception as e: | ||||
|             # -------------------------------------------- # | ||||
|             # Response Generation Failed | ||||
|             # -------------------------------------------- # | ||||
|             await self.handle_exception(request, e) | ||||
|  | ||||
|     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() | ||||
|  | ||||
|     # -------------------------------------------------------------------- # | ||||
|     # Testing | ||||
|     # -------------------------------------------------------------------- # | ||||
|  | ||||
|     @property | ||||
|     def test_client(self): | ||||
|     def test_client(self):  # noqa | ||||
|         if self._test_client: | ||||
|             return self._test_client | ||||
|         elif self._test_manager: | ||||
|             return self._test_manager.test_client | ||||
|         from sanic_testing.testing import SanicTestClient  # type: ignore | ||||
|  | ||||
|         self._test_client = SanicTestClient(self) | ||||
|         return self._test_client | ||||
|  | ||||
|     @property | ||||
|     def asgi_client(self): | ||||
|     def asgi_client(self):  # noqa | ||||
|         """ | ||||
|         A testing client that uses ASGI to reach into the application to | ||||
|         execute hanlers. | ||||
| @@ -644,6 +702,8 @@ class Sanic( | ||||
|         """ | ||||
|         if self._asgi_client: | ||||
|             return self._asgi_client | ||||
|         elif self._test_manager: | ||||
|             return self._test_manager.asgi_client | ||||
|         from sanic_testing.testing import SanicASGITestClient  # type: ignore | ||||
|  | ||||
|         self._asgi_client = SanicASGITestClient(self) | ||||
| @@ -915,7 +975,11 @@ class Sanic( | ||||
|     ): | ||||
|         """Helper function used by `run` and `create_server`.""" | ||||
|  | ||||
|         self.router.finalize() | ||||
|         try: | ||||
|             self.router.finalize() | ||||
|         except FinalizationError as e: | ||||
|             if not Sanic.test_mode: | ||||
|                 raise e | ||||
|  | ||||
|         if isinstance(ssl, dict): | ||||
|             # try common aliaseses | ||||
| @@ -950,9 +1014,7 @@ class Sanic( | ||||
|             "backlog": backlog, | ||||
|         } | ||||
|  | ||||
|         # -------------------------------------------- # | ||||
|         # Register start/stop events | ||||
|         # -------------------------------------------- # | ||||
|  | ||||
|         for event_name, settings_name, reverse in ( | ||||
|             ("before_server_start", "before_start", False), | ||||
| @@ -1014,40 +1076,6 @@ class Sanic( | ||||
|         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 | ||||
|     # -------------------------------------------------------------------- # | ||||
| @@ -1057,9 +1085,10 @@ class Sanic( | ||||
|         To be ASGI compliant, our instance must be a callable that accepts | ||||
|         three arguments: scope, receive, send. See the ASGI reference for more | ||||
|         details: https://asgi.readthedocs.io/en/latest | ||||
|         /""" | ||||
|         """ | ||||
|         self.asgi = True | ||||
|         asgi_app = await ASGIApp.create(self, scope, receive, send) | ||||
|         self._asgi_app = await ASGIApp.create(self, scope, receive, send) | ||||
|         asgi_app = self._asgi_app | ||||
|         await asgi_app() | ||||
|  | ||||
|     _asgi_single_callable = True  # We conform to ASGI 3.0 single-callable | ||||
|   | ||||
| @@ -131,6 +131,7 @@ class Lifespan: | ||||
|         in sequence since the ASGI lifespan protocol only supports a single | ||||
|         startup event. | ||||
|         """ | ||||
|         self.asgi_app.sanic_app.router.finalize() | ||||
|         listeners = self.asgi_app.sanic_app.listeners.get( | ||||
|             "before_server_start", [] | ||||
|         ) + self.asgi_app.sanic_app.listeners.get("after_server_start", []) | ||||
|   | ||||
							
								
								
									
										36
									
								
								sanic/base.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								sanic/base.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| from sanic.mixins.exceptions import ExceptionMixin | ||||
| from sanic.mixins.listeners import ListenerMixin | ||||
| from sanic.mixins.middleware import MiddlewareMixin | ||||
| from sanic.mixins.routes import RouteMixin | ||||
|  | ||||
|  | ||||
| class Base(type): | ||||
|     def __new__(cls, name, bases, attrs): | ||||
|         init = attrs.get("__init__") | ||||
|  | ||||
|         def __init__(self, *args, **kwargs): | ||||
|             nonlocal init | ||||
|             nonlocal name | ||||
|  | ||||
|             bases = [ | ||||
|                 b for base in type(self).__bases__ for b in base.__bases__ | ||||
|             ] | ||||
|  | ||||
|             for base in bases: | ||||
|                 base.__init__(self, *args, **kwargs) | ||||
|  | ||||
|             if init: | ||||
|                 init(self, *args, **kwargs) | ||||
|  | ||||
|         attrs["__init__"] = __init__ | ||||
|         return type.__new__(cls, name, bases, attrs) | ||||
|  | ||||
|  | ||||
| class BaseSanic( | ||||
|     RouteMixin, | ||||
|     MiddlewareMixin, | ||||
|     ListenerMixin, | ||||
|     ExceptionMixin, | ||||
|     metaclass=Base, | ||||
| ): | ||||
|     ... | ||||
| @@ -1,18 +1,12 @@ | ||||
| from collections import defaultdict, namedtuple | ||||
| from typing import Iterable, Optional | ||||
| from collections import defaultdict | ||||
| from typing import Optional | ||||
|  | ||||
| from sanic.base import BaseSanic | ||||
| from sanic.blueprint_group import BlueprintGroup | ||||
| from sanic.mixins.base import BaseMixin | ||||
| from sanic.mixins.exceptions import ExceptionMixin | ||||
| from sanic.mixins.listeners import ListenerMixin | ||||
| from sanic.mixins.middleware import MiddlewareMixin | ||||
| from sanic.mixins.routes import RouteMixin | ||||
| from sanic.models.futures import FutureRoute, FutureStatic | ||||
|  | ||||
|  | ||||
| class Blueprint( | ||||
|     BaseMixin, RouteMixin, MiddlewareMixin, ListenerMixin, ExceptionMixin | ||||
| ): | ||||
| class Blueprint(BaseSanic): | ||||
|     """ | ||||
|     In *Sanic* terminology, a **Blueprint** is a logical collection of | ||||
|     URLs that perform a specific set of tasks which can be identified by | ||||
| @@ -122,20 +116,35 @@ class Blueprint( | ||||
|             # Prepend the blueprint URI prefix if available | ||||
|             uri = url_prefix + future.uri if url_prefix else future.uri | ||||
|  | ||||
|             strict_slashes = ( | ||||
|                 self.strict_slashes | ||||
|                 if future.strict_slashes is None | ||||
|                 and self.strict_slashes is not None | ||||
|                 else future.strict_slashes | ||||
|             ) | ||||
|             name = app._generate_name(future.name) | ||||
|  | ||||
|             apply_route = FutureRoute( | ||||
|                 future.handler, | ||||
|                 uri[1:] if uri.startswith("//") else uri, | ||||
|                 future.methods, | ||||
|                 future.host or self.host, | ||||
|                 future.strict_slashes, | ||||
|                 strict_slashes, | ||||
|                 future.stream, | ||||
|                 future.version or self.version, | ||||
|                 future.name, | ||||
|                 name, | ||||
|                 future.ignore_body, | ||||
|                 future.websocket, | ||||
|                 future.subprotocols, | ||||
|                 future.unquote, | ||||
|                 future.static, | ||||
|             ) | ||||
|  | ||||
|             route = app._apply_route(apply_route) | ||||
|             routes.append(route) | ||||
|             operation = ( | ||||
|                 routes.extend if isinstance(route, list) else routes.append | ||||
|             ) | ||||
|             operation(route) | ||||
|  | ||||
|         # Static Files | ||||
|         for future in self._future_statics: | ||||
| @@ -148,8 +157,9 @@ class Blueprint( | ||||
|         route_names = [route.name for route in routes if route] | ||||
|  | ||||
|         # Middleware | ||||
|         for future in self._future_middleware: | ||||
|             app._apply_middleware(future, route_names) | ||||
|         if route_names: | ||||
|             for future in self._future_middleware: | ||||
|                 app._apply_middleware(future, route_names) | ||||
|  | ||||
|         # Exceptions | ||||
|         for future in self._future_exceptions: | ||||
| @@ -158,6 +168,3 @@ class Blueprint( | ||||
|         # Event listeners | ||||
|         for listener in self._future_listeners: | ||||
|             app._apply_listener(listener) | ||||
|  | ||||
|     def _generate_name(self, handler, name: str) -> str: | ||||
|         return f"{self.name}.{name or handler.__name__}" | ||||
|   | ||||
| @@ -1,19 +0,0 @@ | ||||
| class Base(type): | ||||
|     def __new__(cls, name, bases, attrs): | ||||
|         init = attrs.get("__init__") | ||||
|  | ||||
|         def __init__(self, *args, **kwargs): | ||||
|             nonlocal init | ||||
|             for base in type(self).__bases__: | ||||
|                 if base.__name__ != "BaseMixin": | ||||
|                     base.__init__(self, *args, **kwargs) | ||||
|  | ||||
|             if init: | ||||
|                 init(self, *args, **kwargs) | ||||
|  | ||||
|         attrs["__init__"] = __init__ | ||||
|         return type.__new__(cls, name, bases, attrs) | ||||
|  | ||||
|  | ||||
| class BaseMixin(metaclass=Base): | ||||
|     ... | ||||
| @@ -1,5 +1,3 @@ | ||||
| from enum import Enum, auto | ||||
| from functools import partial | ||||
| from typing import Set | ||||
|  | ||||
| from sanic.models.futures import FutureException | ||||
| @@ -29,6 +27,9 @@ class ExceptionMixin: | ||||
|             nonlocal apply | ||||
|             nonlocal exceptions | ||||
|  | ||||
|             if isinstance(exceptions[0], list): | ||||
|                 exceptions = tuple(*exceptions) | ||||
|  | ||||
|             future_exception = FutureException(handler, exceptions) | ||||
|             self._future_exceptions.add(future_exception) | ||||
|             if apply: | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| from enum import Enum, auto | ||||
| from functools import partial | ||||
| from typing import Any, Callable, Coroutine, Optional, Set, Union | ||||
| from typing import Any, Callable, Coroutine, List, Optional, Set, Union | ||||
|  | ||||
| from sanic.models.futures import FutureListener | ||||
|  | ||||
| @@ -17,7 +17,7 @@ class ListenerEvent(str, Enum): | ||||
|  | ||||
| class ListenerMixin: | ||||
|     def __init__(self, *args, **kwargs) -> None: | ||||
|         self._future_listeners: Set[FutureListener] = set() | ||||
|         self._future_listeners: List[FutureListener] = list() | ||||
|  | ||||
|     def _apply_listener(self, listener: FutureListener): | ||||
|         raise NotImplementedError | ||||
| @@ -51,7 +51,7 @@ class ListenerMixin: | ||||
|             nonlocal apply | ||||
|  | ||||
|             future_listener = FutureListener(listener, event) | ||||
|             self._future_listeners.add(future_listener) | ||||
|             self._future_listeners.append(future_listener) | ||||
|             if apply: | ||||
|                 self._apply_listener(future_listener) | ||||
|             return listener | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
| from functools import partial | ||||
| from typing import Set | ||||
| from typing import List | ||||
|  | ||||
| from sanic.models.futures import FutureMiddleware | ||||
|  | ||||
|  | ||||
| class MiddlewareMixin: | ||||
|     def __init__(self, *args, **kwargs) -> None: | ||||
|         self._future_middleware: Set[FutureMiddleware] = set() | ||||
|         self._future_middleware: List[FutureMiddleware] = list() | ||||
|  | ||||
|     def _apply_middleware(self, middleware: FutureMiddleware): | ||||
|         raise NotImplementedError | ||||
| @@ -30,7 +30,7 @@ class MiddlewareMixin: | ||||
|             nonlocal apply | ||||
|  | ||||
|             future_middleware = FutureMiddleware(middleware, attach_to) | ||||
|             self._future_middleware.add(future_middleware) | ||||
|             self._future_middleware.append(future_middleware) | ||||
|             if apply: | ||||
|                 self._apply_middleware(future_middleware) | ||||
|             return middleware | ||||
|   | ||||
| @@ -1,9 +1,8 @@ | ||||
| from functools import partial | ||||
| from inspect import signature | ||||
| from pathlib import PurePath | ||||
| from typing import Iterable, List, Optional, Set, Union | ||||
|  | ||||
| from sanic_routing.route import Route | ||||
| from sanic_routing.route import Route  # type: ignore | ||||
|  | ||||
| from sanic.constants import HTTP_METHODS | ||||
| from sanic.models.futures import FutureRoute, FutureStatic | ||||
| @@ -36,6 +35,8 @@ class RouteMixin: | ||||
|         apply: bool = True, | ||||
|         subprotocols: Optional[List[str]] = None, | ||||
|         websocket: bool = False, | ||||
|         unquote: bool = False, | ||||
|         static: bool = False, | ||||
|     ): | ||||
|         """ | ||||
|         Decorate a function to be registered as a route | ||||
| @@ -52,9 +53,6 @@ class RouteMixin: | ||||
|         :return: tuple of routes, decorated function | ||||
|         """ | ||||
|  | ||||
|         if websocket: | ||||
|             self.enable_websocket() | ||||
|  | ||||
|         # Fix case where the user did not prefix the URL with a / | ||||
|         # and will probably get confused as to why it's not working | ||||
|         if not uri.startswith("/"): | ||||
| @@ -63,6 +61,9 @@ class RouteMixin: | ||||
|         if strict_slashes is None: | ||||
|             strict_slashes = self.strict_slashes | ||||
|  | ||||
|         if not methods and not websocket: | ||||
|             methods = frozenset({"GET"}) | ||||
|  | ||||
|         def decorator(handler): | ||||
|             nonlocal uri | ||||
|             nonlocal methods | ||||
| @@ -74,39 +75,43 @@ class RouteMixin: | ||||
|             nonlocal ignore_body | ||||
|             nonlocal subprotocols | ||||
|             nonlocal websocket | ||||
|             nonlocal static | ||||
|  | ||||
|             if isinstance(handler, tuple): | ||||
|                 # if a handler fn is already wrapped in a route, the handler | ||||
|                 # variable will be a tuple of (existing routes, handler fn) | ||||
|                 _, handler = handler | ||||
|  | ||||
|             if websocket: | ||||
|                 websocket_handler = partial( | ||||
|                     self._websocket_handler, | ||||
|                     handler, | ||||
|                     subprotocols=subprotocols, | ||||
|                 ) | ||||
|                 websocket_handler.__name__ = ( | ||||
|                     "websocket_handler_" + handler.__name__ | ||||
|                 ) | ||||
|                 websocket_handler.is_websocket = True | ||||
|                 handler = websocket_handler | ||||
|             name = self._generate_name(name, handler) | ||||
|  | ||||
|             # TODO: | ||||
|             # - THink this thru.... do we want all routes namespaced? | ||||
|             # - | ||||
|             name = self._generate_name(handler, name) | ||||
|             if isinstance(host, str): | ||||
|                 host = frozenset([host]) | ||||
|             elif host and not isinstance(host, frozenset): | ||||
|                 try: | ||||
|                     host = frozenset(host) | ||||
|                 except TypeError: | ||||
|                     raise ValueError( | ||||
|                         "Expected either string or Iterable of host strings, " | ||||
|                         "not %s" % host | ||||
|                     ) | ||||
|  | ||||
|             if isinstance(subprotocols, (list, tuple, set)): | ||||
|                 subprotocols = frozenset(subprotocols) | ||||
|  | ||||
|             route = FutureRoute( | ||||
|                 handler, | ||||
|                 uri, | ||||
|                 frozenset(methods), | ||||
|                 None if websocket else frozenset([x.upper() for x in methods]), | ||||
|                 host, | ||||
|                 strict_slashes, | ||||
|                 stream, | ||||
|                 version, | ||||
|                 name, | ||||
|                 ignore_body, | ||||
|                 websocket, | ||||
|                 subprotocols, | ||||
|                 unquote, | ||||
|                 static, | ||||
|             ) | ||||
|  | ||||
|             self._future_routes.add(route) | ||||
| @@ -441,6 +446,7 @@ class RouteMixin: | ||||
|         subprotocols: Optional[List[str]] = None, | ||||
|         version: Optional[int] = None, | ||||
|         name: Optional[str] = None, | ||||
|         apply: bool = True, | ||||
|     ): | ||||
|         """ | ||||
|         Decorate a function to be registered as a websocket route | ||||
| @@ -543,12 +549,16 @@ class RouteMixin: | ||||
|         :rtype: List[sanic.router.Route] | ||||
|         """ | ||||
|  | ||||
|         if not name.startswith(self.name + "."): | ||||
|             name = f"{self.name}.{name}" | ||||
|         name = self._generate_name(name) | ||||
|  | ||||
|         if strict_slashes is None and self.strict_slashes is not None: | ||||
|             strict_slashes = self.strict_slashes | ||||
|  | ||||
|         if not isinstance(file_or_directory, (str, bytes, PurePath)): | ||||
|             raise ValueError( | ||||
|                 f"Static route must be a valid path, not {file_or_directory}" | ||||
|             ) | ||||
|  | ||||
|         static = FutureStatic( | ||||
|             uri, | ||||
|             file_or_directory, | ||||
| @@ -566,5 +576,29 @@ class RouteMixin: | ||||
|         if apply: | ||||
|             self._apply_static(static) | ||||
|  | ||||
|     def _generate_name(self, handler, name: str) -> str: | ||||
|         return name or handler.__name__ | ||||
|     def _generate_name(self, *objects) -> str: | ||||
|         name = None | ||||
|  | ||||
|         for obj in objects: | ||||
|             if obj: | ||||
|                 if isinstance(obj, str): | ||||
|                     name = obj | ||||
|                     break | ||||
|  | ||||
|                 try: | ||||
|                     name = obj.name | ||||
|                 except AttributeError: | ||||
|                     try: | ||||
|                         name = obj.__name__ | ||||
|                     except AttributeError: | ||||
|                         continue | ||||
|                 else: | ||||
|                     break | ||||
|  | ||||
|         if not name: | ||||
|             raise Exception("...") | ||||
|  | ||||
|         if not name.startswith(f"{self.name}."): | ||||
|             name = f"{self.name}.{name}" | ||||
|  | ||||
|         return name | ||||
|   | ||||
| @@ -13,6 +13,10 @@ FutureRoute = namedtuple( | ||||
|         "version", | ||||
|         "name", | ||||
|         "ignore_body", | ||||
|         "websocket", | ||||
|         "subprotocols", | ||||
|         "unquote", | ||||
|         "static", | ||||
|     ], | ||||
| ) | ||||
| FutureListener = namedtuple("FutureListener", ["listener", "event"]) | ||||
|   | ||||
| @@ -87,6 +87,7 @@ class Request: | ||||
|         "_port", | ||||
|         "_remote_addr", | ||||
|         "_socket", | ||||
|         "_match_info", | ||||
|         "app", | ||||
|         "body", | ||||
|         "conn_info", | ||||
| @@ -147,6 +148,7 @@ class Request: | ||||
|         self.uri_template: Optional[str] = None | ||||
|         self.request_middleware_started = False | ||||
|         self._cookies: Dict[str, str] = {} | ||||
|         self._match_info = {} | ||||
|         self.stream: Optional[Http] = None | ||||
|         self.endpoint: Optional[str] = None | ||||
|  | ||||
| @@ -455,7 +457,7 @@ class Request: | ||||
|         """ | ||||
|         :return: matched info after resolving route | ||||
|         """ | ||||
|         return self.app.router.get(self)[2] | ||||
|         return self._match_info | ||||
|  | ||||
|     # Transport properties (obtained from local interface only) | ||||
|  | ||||
|   | ||||
							
								
								
									
										175
									
								
								sanic/router.py
									
									
									
									
									
								
							
							
						
						
									
										175
									
								
								sanic/router.py
									
									
									
									
									
								
							| @@ -1,14 +1,22 @@ | ||||
| from functools import lru_cache | ||||
| from typing import Any, Dict, Iterable, Optional, Tuple, Union | ||||
| from typing import Any, Dict, Iterable, List, Optional, Tuple, Union | ||||
|  | ||||
| from sanic_routing import BaseRouter | ||||
| from sanic_routing.route import Route | ||||
| from sanic_routing import BaseRouter  # type: ignore | ||||
| from sanic_routing.exceptions import NoMethod  # type: ignore | ||||
| from sanic_routing.exceptions import ( | ||||
|     NotFound as RoutingNotFound,  # type: ignore | ||||
| ) | ||||
| from sanic_routing.route import Route  # type: ignore | ||||
|  | ||||
| from sanic.constants import HTTP_METHODS | ||||
| from sanic.exceptions import MethodNotSupported, NotFound | ||||
| from sanic.handlers import RouteHandler | ||||
| from sanic.request import Request | ||||
|  | ||||
|  | ||||
| ROUTER_CACHE_SIZE = 1024 | ||||
|  | ||||
|  | ||||
| class Router(BaseRouter): | ||||
|     """ | ||||
|     The router implementation responsible for routing a :class:`Request` object | ||||
| @@ -18,18 +26,38 @@ class Router(BaseRouter): | ||||
|     DEFAULT_METHOD = "GET" | ||||
|     ALLOWED_METHODS = HTTP_METHODS | ||||
|  | ||||
|     @lru_cache | ||||
|     def get( | ||||
|         self, request: Request | ||||
|     ) -> Tuple[ | ||||
|         RouteHandler, | ||||
|         Tuple[Any, ...], | ||||
|         Dict[str, Any], | ||||
|         str, | ||||
|         str, | ||||
|         Optional[str], | ||||
|         bool, | ||||
|     ]: | ||||
|     # Putting the lru_cache on Router.get() performs better for the benchmarsk | ||||
|     # at tests/benchmark/test_route_resolution_benchmark.py | ||||
|     # However, overall application performance is significantly improved | ||||
|     # with the lru_cache on this method. | ||||
|     @lru_cache(maxsize=ROUTER_CACHE_SIZE) | ||||
|     def _get( | ||||
|         self, path, method, host | ||||
|     ) -> Tuple[RouteHandler, Dict[str, Any], str, str, bool,]: | ||||
|         try: | ||||
|             route, handler, params = self.resolve( | ||||
|                 path=path, | ||||
|                 method=method, | ||||
|                 extra={"host": host}, | ||||
|             ) | ||||
|         except RoutingNotFound as e: | ||||
|             raise NotFound("Requested URL {} not found".format(e.path)) | ||||
|         except NoMethod as e: | ||||
|             raise MethodNotSupported( | ||||
|                 "Method {} not allowed for URL {}".format(method, path), | ||||
|                 method=method, | ||||
|                 allowed_methods=e.allowed_methods, | ||||
|             ) | ||||
|  | ||||
|         return ( | ||||
|             handler, | ||||
|             params, | ||||
|             route.path, | ||||
|             route.name, | ||||
|             route.ctx.ignore_body, | ||||
|         ) | ||||
|  | ||||
|     def get(self, request: Request): | ||||
|         """ | ||||
|         Retrieve a `Route` object containg the details about how to handle | ||||
|         a response for a given request | ||||
| @@ -41,23 +69,8 @@ class Router(BaseRouter): | ||||
|         :rtype: Tuple[ RouteHandler, Tuple[Any, ...], Dict[str, Any], str, str, | ||||
|             Optional[str], bool, ] | ||||
|         """ | ||||
|         route, handler, params = self.resolve( | ||||
|             path=request.path, | ||||
|             method=request.method, | ||||
|         ) | ||||
|  | ||||
|         # TODO: Implement response | ||||
|         # - args, | ||||
|         # - endpoint, | ||||
|  | ||||
|         return ( | ||||
|             handler, | ||||
|             (), | ||||
|             params, | ||||
|             route.path, | ||||
|             route.name, | ||||
|             None, | ||||
|             route.ctx.ignore_body, | ||||
|         return self._get( | ||||
|             request.path, request.method, request.headers.get("host") | ||||
|         ) | ||||
|  | ||||
|     def add( | ||||
| @@ -65,13 +78,15 @@ class Router(BaseRouter): | ||||
|         uri: str, | ||||
|         methods: Iterable[str], | ||||
|         handler: RouteHandler, | ||||
|         host: Optional[str] = None, | ||||
|         host: Optional[Union[str, Iterable[str]]] = None, | ||||
|         strict_slashes: bool = False, | ||||
|         stream: bool = False, | ||||
|         ignore_body: bool = False, | ||||
|         version: Union[str, float, int] = None, | ||||
|         name: Optional[str] = None, | ||||
|     ) -> Route: | ||||
|         unquote: bool = False, | ||||
|         static: bool = False, | ||||
|     ) -> Union[Route, List[Route]]: | ||||
|         """ | ||||
|         Add a handler to the router | ||||
|  | ||||
| @@ -99,19 +114,93 @@ class Router(BaseRouter): | ||||
|         :return: the route object | ||||
|         :rtype: Route | ||||
|         """ | ||||
|         # TODO: Implement | ||||
|         # - host | ||||
|         # - strict_slashes | ||||
|         # - ignore_body | ||||
|         # - stream | ||||
|         if version is not None: | ||||
|             version = str(version).strip("/").lstrip("v") | ||||
|             uri = "/".join([f"/v{version}", uri.lstrip("/")]) | ||||
|  | ||||
|         route = super().add( | ||||
|             path=uri, handler=handler, methods=methods, name=name | ||||
|         params = dict( | ||||
|             path=uri, | ||||
|             handler=handler, | ||||
|             methods=methods, | ||||
|             name=name, | ||||
|             strict=strict_slashes, | ||||
|             unquote=unquote, | ||||
|         ) | ||||
|         route.ctx.ignore_body = ignore_body | ||||
|         route.ctx.stream = stream | ||||
|  | ||||
|         if isinstance(host, str): | ||||
|             hosts = [host] | ||||
|         else: | ||||
|             hosts = host or [None]  # type: ignore | ||||
|  | ||||
|         routes = [] | ||||
|  | ||||
|         for host in hosts: | ||||
|             if host: | ||||
|                 params.update({"requirements": {"host": host}}) | ||||
|  | ||||
|             route = super().add(**params) | ||||
|             route.ctx.ignore_body = ignore_body | ||||
|             route.ctx.stream = stream | ||||
|             route.ctx.hosts = hosts | ||||
|             route.ctx.static = static | ||||
|  | ||||
|             routes.append(route) | ||||
|  | ||||
|         if len(routes) == 1: | ||||
|             return routes[0] | ||||
|         return routes | ||||
|  | ||||
|     def is_stream_handler(self, request) -> bool: | ||||
|         """ | ||||
|         Handler for request is stream or not. | ||||
|  | ||||
|         :param request: Request object | ||||
|         :return: bool | ||||
|         """ | ||||
|         try: | ||||
|             handler = self.get(request)[0] | ||||
|         except (NotFound, MethodNotSupported): | ||||
|             return False | ||||
|         if hasattr(handler, "view_class") and hasattr( | ||||
|             handler.view_class, request.method.lower() | ||||
|         ): | ||||
|             handler = getattr(handler.view_class, request.method.lower()) | ||||
|         return hasattr(handler, "is_stream") | ||||
|  | ||||
|     @lru_cache(maxsize=ROUTER_CACHE_SIZE) | ||||
|     def find_route_by_view_name(self, view_name, name=None): | ||||
|         """ | ||||
|         Find a route in the router based on the specified view name. | ||||
|  | ||||
|         :param view_name: string of view name to search by | ||||
|         :param kwargs: additional params, usually for static files | ||||
|         :return: tuple containing (uri, Route) | ||||
|         """ | ||||
|         if not view_name: | ||||
|             return None | ||||
|  | ||||
|         route = self.name_index.get(view_name) | ||||
|         if not route: | ||||
|             full_name = self.ctx.app._generate_name(view_name) | ||||
|             route = self.name_index.get(full_name) | ||||
|  | ||||
|         if not route: | ||||
|             return None | ||||
|  | ||||
|         return route | ||||
|  | ||||
|     @property | ||||
|     def routes_all(self): | ||||
|         return self.routes | ||||
|  | ||||
|     @property | ||||
|     def routes_static(self): | ||||
|         return self.static_routes | ||||
|  | ||||
|     @property | ||||
|     def routes_dynamic(self): | ||||
|         return self.dynamic_routes | ||||
|  | ||||
|     @property | ||||
|     def routes_regex(self): | ||||
|         return self.regex_routes | ||||
|   | ||||
| @@ -157,11 +157,11 @@ def register( | ||||
|     # If we're not trying to match a file directly, | ||||
|     # serve from the folder | ||||
|     if not path.isfile(file_or_directory): | ||||
|         uri += "<file_uri:" + static.pattern + ">" | ||||
|         uri += "/<file_uri>" | ||||
|  | ||||
|     # special prefix for static files | ||||
|     if not static.name.startswith("_static_"): | ||||
|         name = f"_static_{static.name}" | ||||
|     # if not static.name.startswith("_static_"): | ||||
|     #     name = f"_static_{static.name}" | ||||
|  | ||||
|     _handler = wraps(_static_request_handler)( | ||||
|         partial( | ||||
| @@ -174,11 +174,13 @@ def register( | ||||
|         ) | ||||
|     ) | ||||
|  | ||||
|     _routes, _ = app.route( | ||||
|     route, _ = app.route( | ||||
|         uri=uri, | ||||
|         methods=["GET", "HEAD"], | ||||
|         name=name, | ||||
|         host=static.host, | ||||
|         strict_slashes=static.strict_slashes, | ||||
|         static=True, | ||||
|     )(_handler) | ||||
|     return _routes | ||||
|  | ||||
|     return route | ||||
|   | ||||
							
								
								
									
										1
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								setup.py
									
									
									
									
									
								
							| @@ -83,6 +83,7 @@ ujson = "ujson>=1.35" + env_dependency | ||||
| uvloop = "uvloop>=0.5.3" + env_dependency | ||||
|  | ||||
| requirements = [ | ||||
|     "sanic-routing", | ||||
|     "httptools>=0.0.10", | ||||
|     uvloop, | ||||
|     ujson, | ||||
|   | ||||
| @@ -4,6 +4,8 @@ from pytest import mark | ||||
|  | ||||
| import sanic.router | ||||
|  | ||||
| from sanic.request import Request | ||||
|  | ||||
|  | ||||
| seed("Pack my box with five dozen liquor jugs.") | ||||
|  | ||||
| @@ -23,8 +25,17 @@ class TestSanicRouteResolution: | ||||
|         route_to_call = choice(simple_routes) | ||||
|  | ||||
|         result = benchmark.pedantic( | ||||
|             router._get, | ||||
|             ("/{}".format(route_to_call[-1]), route_to_call[0], "localhost"), | ||||
|             router.get, | ||||
|             ( | ||||
|                 Request( | ||||
|                     "/{}".format(route_to_call[-1]).encode(), | ||||
|                     {"host": "localhost"}, | ||||
|                     "v1", | ||||
|                     route_to_call[0], | ||||
|                     None, | ||||
|                     None, | ||||
|                 ), | ||||
|             ), | ||||
|             iterations=1000, | ||||
|             rounds=1000, | ||||
|         ) | ||||
| @@ -47,8 +58,17 @@ class TestSanicRouteResolution: | ||||
|         print("{} -> {}".format(route_to_call[-1], url)) | ||||
|  | ||||
|         result = benchmark.pedantic( | ||||
|             router._get, | ||||
|             ("/{}".format(url), route_to_call[0], "localhost"), | ||||
|             router.get, | ||||
|             ( | ||||
|                 Request( | ||||
|                     "/{}".format(url).encode(), | ||||
|                     {"host": "localhost"}, | ||||
|                     "v1", | ||||
|                     route_to_call[0], | ||||
|                     None, | ||||
|                     None, | ||||
|                 ), | ||||
|             ), | ||||
|             iterations=1000, | ||||
|             rounds=1000, | ||||
|         ) | ||||
|   | ||||
| @@ -4,12 +4,16 @@ import string | ||||
| import sys | ||||
| import uuid | ||||
|  | ||||
| from typing import Tuple | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| from sanic_routing.exceptions import RouteExists | ||||
| from sanic_testing import TestManager | ||||
|  | ||||
| from sanic import Sanic | ||||
| from sanic.router import RouteExists, Router | ||||
| from sanic.constants import HTTP_METHODS | ||||
| from sanic.router import Router | ||||
|  | ||||
|  | ||||
| random.seed("Pack my box with five dozen liquor jugs.") | ||||
| @@ -38,12 +42,12 @@ async def _handler(request): | ||||
|  | ||||
| TYPE_TO_GENERATOR_MAP = { | ||||
|     "string": lambda: "".join( | ||||
|         [random.choice(string.ascii_letters + string.digits) for _ in range(4)] | ||||
|         [random.choice(string.ascii_lowercase) for _ in range(4)] | ||||
|     ), | ||||
|     "int": lambda: random.choice(range(1000000)), | ||||
|     "number": lambda: random.random(), | ||||
|     "alpha": lambda: "".join( | ||||
|         [random.choice(string.ascii_letters) for _ in range(4)] | ||||
|         [random.choice(string.ascii_lowercase) for _ in range(4)] | ||||
|     ), | ||||
|     "uuid": lambda: str(uuid.uuid1()), | ||||
| } | ||||
| @@ -52,7 +56,7 @@ TYPE_TO_GENERATOR_MAP = { | ||||
| class RouteStringGenerator: | ||||
|  | ||||
|     ROUTE_COUNT_PER_DEPTH = 100 | ||||
|     HTTP_METHODS = ["GET", "PUT", "POST", "PATCH", "DELETE", "OPTION"] | ||||
|     HTTP_METHODS = HTTP_METHODS | ||||
|     ROUTE_PARAM_TYPES = ["string", "int", "number", "alpha", "uuid"] | ||||
|  | ||||
|     def generate_random_direct_route(self, max_route_depth=4): | ||||
| @@ -105,12 +109,12 @@ class RouteStringGenerator: | ||||
| @pytest.fixture(scope="function") | ||||
| def sanic_router(app): | ||||
|     # noinspection PyProtectedMember | ||||
|     def _setup(route_details: tuple) -> (Router, tuple): | ||||
|         router = Router(app) | ||||
|     def _setup(route_details: tuple) -> Tuple[Router, tuple]: | ||||
|         router = Router() | ||||
|         added_router = [] | ||||
|         for method, route in route_details: | ||||
|             try: | ||||
|                 router._add( | ||||
|                 router.add( | ||||
|                     uri=f"/{route}", | ||||
|                     methods=frozenset({method}), | ||||
|                     host="localhost", | ||||
| @@ -119,6 +123,7 @@ def sanic_router(app): | ||||
|                 added_router.append((method, route)) | ||||
|             except RouteExists: | ||||
|                 pass | ||||
|         router.finalize() | ||||
|         return router, added_router | ||||
|  | ||||
|     return _setup | ||||
| @@ -137,5 +142,4 @@ def url_param_generator(): | ||||
| @pytest.fixture(scope="function") | ||||
| def app(request): | ||||
|     app = Sanic(request.node.name) | ||||
|     # TestManager(app) | ||||
|     return app | ||||
|   | ||||
| @@ -118,7 +118,7 @@ def test_app_route_raise_value_error(app): | ||||
|  | ||||
| def test_app_handle_request_handler_is_none(app, monkeypatch): | ||||
|     def mockreturn(*args, **kwargs): | ||||
|         return None, [], {}, "", "", None, False | ||||
|         return None, {}, "", "", False | ||||
|  | ||||
|     # Not sure how to make app.router.get() return None, so use mock here. | ||||
|     monkeypatch.setattr(app.router, "get", mockreturn) | ||||
|   | ||||
| @@ -45,7 +45,8 @@ def protocol(transport): | ||||
|     return transport.get_protocol() | ||||
|  | ||||
|  | ||||
| def test_listeners_triggered(app): | ||||
| def test_listeners_triggered(): | ||||
|     app = Sanic("app") | ||||
|     before_server_start = False | ||||
|     after_server_start = False | ||||
|     before_server_stop = False | ||||
| @@ -71,6 +72,10 @@ def test_listeners_triggered(app): | ||||
|         nonlocal after_server_stop | ||||
|         after_server_stop = True | ||||
|  | ||||
|     @app.route("/") | ||||
|     def handler(request): | ||||
|         return text("...") | ||||
|  | ||||
|     class CustomServer(uvicorn.Server): | ||||
|         def install_signal_handlers(self): | ||||
|             pass | ||||
| @@ -121,6 +126,10 @@ def test_listeners_triggered_async(app): | ||||
|         nonlocal after_server_stop | ||||
|         after_server_stop = True | ||||
|  | ||||
|     @app.route("/") | ||||
|     def handler(request): | ||||
|         return text("...") | ||||
|  | ||||
|     class CustomServer(uvicorn.Server): | ||||
|         def install_signal_handlers(self): | ||||
|             pass | ||||
| @@ -325,7 +334,7 @@ async def test_cookie_customization(app): | ||||
|  | ||||
|  | ||||
| @pytest.mark.asyncio | ||||
| async def test_json_content_type(app): | ||||
| async def test_content_type(app): | ||||
|     @app.get("/json") | ||||
|     def send_json(request): | ||||
|         return json({"foo": "bar"}) | ||||
|   | ||||
| @@ -4,6 +4,8 @@ import asyncio | ||||
| def test_bad_request_response(app): | ||||
|     lines = [] | ||||
|  | ||||
|     app.get("/")(lambda x: ...) | ||||
|  | ||||
|     @app.listener("after_server_start") | ||||
|     async def _request(sanic, loop): | ||||
|         connect = asyncio.open_connection("127.0.0.1", 42101) | ||||
|   | ||||
| @@ -209,18 +209,28 @@ def test_bp_with_host(app): | ||||
|  | ||||
|     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 subdomain!" | ||||
|     assert response.body == b"Hello subdomain!" | ||||
|  | ||||
|  | ||||
| def test_several_bp_with_host(app): | ||||
|     bp = Blueprint("test_text", url_prefix="/test", host="example.com") | ||||
|     bp2 = Blueprint("test_text2", url_prefix="/test", host="sub.example.com") | ||||
|     bp = Blueprint( | ||||
|         "test_text", | ||||
|         url_prefix="/test", | ||||
|         host="example.com", | ||||
|         strict_slashes=True, | ||||
|     ) | ||||
|     bp2 = Blueprint( | ||||
|         "test_text2", | ||||
|         url_prefix="/test", | ||||
|         host="sub.example.com", | ||||
|         strict_slashes=True, | ||||
|     ) | ||||
|  | ||||
|     @bp.route("/") | ||||
|     def handler(request): | ||||
| @@ -240,6 +250,7 @@ def test_several_bp_with_host(app): | ||||
|     assert bp.host == "example.com" | ||||
|     headers = {"Host": "example.com"} | ||||
|     request, response = app.test_client.get("/test/", headers=headers) | ||||
|  | ||||
|     assert response.text == "Hello" | ||||
|  | ||||
|     assert bp2.host == "sub.example.com" | ||||
| @@ -352,6 +363,29 @@ def test_bp_middleware(app): | ||||
|     assert response.text == "FAIL" | ||||
|  | ||||
|  | ||||
| def test_bp_middleware_with_route(app): | ||||
|     blueprint = Blueprint("test_bp_middleware") | ||||
|  | ||||
|     @blueprint.middleware("response") | ||||
|     async def process_response(request, response): | ||||
|         return text("OK") | ||||
|  | ||||
|     @app.route("/") | ||||
|     async def handler(request): | ||||
|         return text("FAIL") | ||||
|  | ||||
|     @blueprint.route("/bp") | ||||
|     async def bp_handler(request): | ||||
|         return text("FAIL") | ||||
|  | ||||
|     app.blueprint(blueprint) | ||||
|  | ||||
|     request, response = app.test_client.get("/bp") | ||||
|  | ||||
|     assert response.status == 200 | ||||
|     assert response.text == "OK" | ||||
|  | ||||
|  | ||||
| def test_bp_middleware_order(app): | ||||
|     blueprint = Blueprint("test_bp_middleware_order") | ||||
|     order = list() | ||||
| @@ -425,6 +459,7 @@ def test_bp_exception_handler(app): | ||||
|  | ||||
|  | ||||
| def test_bp_listeners(app): | ||||
|     app.route("/")(lambda x: x) | ||||
|     blueprint = Blueprint("test_middleware") | ||||
|  | ||||
|     order = [] | ||||
| @@ -537,19 +572,19 @@ def test_bp_shorthand(app): | ||||
|     app.blueprint(blueprint) | ||||
|  | ||||
|     request, response = app.test_client.get("/get") | ||||
|     assert response.text == "OK" | ||||
|     assert response.body == b"OK" | ||||
|  | ||||
|     request, response = app.test_client.post("/get") | ||||
|     assert response.status == 405 | ||||
|  | ||||
|     request, response = app.test_client.put("/put") | ||||
|     assert response.text == "OK" | ||||
|     assert response.body == b"OK" | ||||
|  | ||||
|     request, response = app.test_client.get("/post") | ||||
|     assert response.status == 405 | ||||
|  | ||||
|     request, response = app.test_client.post("/post") | ||||
|     assert response.text == "OK" | ||||
|     assert response.body == b"OK" | ||||
|  | ||||
|     request, response = app.test_client.get("/post") | ||||
|     assert response.status == 405 | ||||
| @@ -561,19 +596,19 @@ def test_bp_shorthand(app): | ||||
|     assert response.status == 405 | ||||
|  | ||||
|     request, response = app.test_client.options("/options") | ||||
|     assert response.text == "OK" | ||||
|     assert response.body == b"OK" | ||||
|  | ||||
|     request, response = app.test_client.get("/options") | ||||
|     assert response.status == 405 | ||||
|  | ||||
|     request, response = app.test_client.patch("/patch") | ||||
|     assert response.text == "OK" | ||||
|     assert response.body == b"OK" | ||||
|  | ||||
|     request, response = app.test_client.get("/patch") | ||||
|     assert response.status == 405 | ||||
|  | ||||
|     request, response = app.test_client.delete("/delete") | ||||
|     assert response.text == "OK" | ||||
|     assert response.body == b"OK" | ||||
|  | ||||
|     request, response = app.test_client.get("/delete") | ||||
|     assert response.status == 405 | ||||
| @@ -699,7 +734,8 @@ def test_blueprint_middleware_with_args(app: Sanic): | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("file_name", ["test.file"]) | ||||
| def test_static_blueprint_name(app: Sanic, static_file_directory, file_name): | ||||
| def test_static_blueprint_name(static_file_directory, file_name): | ||||
|     app = Sanic("app") | ||||
|     current_file = inspect.getfile(inspect.currentframe()) | ||||
|     with open(current_file, "rb") as file: | ||||
|         file.read() | ||||
| @@ -814,17 +850,19 @@ def test_duplicate_blueprint(app): | ||||
|     ) | ||||
|  | ||||
|  | ||||
| def test_strict_slashes_behavior_adoption(app): | ||||
| def test_strict_slashes_behavior_adoption(): | ||||
|     app = Sanic("app") | ||||
|     app.strict_slashes = True | ||||
|     bp = Blueprint("bp") | ||||
|     bp2 = Blueprint("bp2", strict_slashes=False) | ||||
|  | ||||
|     @app.get("/test") | ||||
|     def handler_test(request): | ||||
|         return text("Test") | ||||
|  | ||||
|     assert app.test_client.get("/test")[1].status == 200 | ||||
|     assert app.test_client.get("/test/")[1].status == 404 | ||||
|  | ||||
|     bp = Blueprint("bp") | ||||
|     @app.get("/f1", strict_slashes=False) | ||||
|     def f1(request): | ||||
|         return text("f1") | ||||
|  | ||||
|     @bp.get("/one", strict_slashes=False) | ||||
|     def one(request): | ||||
| @@ -834,7 +872,15 @@ def test_strict_slashes_behavior_adoption(app): | ||||
|     def second(request): | ||||
|         return text("second") | ||||
|  | ||||
|     @bp2.get("/third") | ||||
|     def third(request): | ||||
|         return text("third") | ||||
|  | ||||
|     app.blueprint(bp) | ||||
|     app.blueprint(bp2) | ||||
|  | ||||
|     assert app.test_client.get("/test")[1].status == 200 | ||||
|     assert app.test_client.get("/test/")[1].status == 404 | ||||
|  | ||||
|     assert app.test_client.get("/one")[1].status == 200 | ||||
|     assert app.test_client.get("/one/")[1].status == 200 | ||||
| @@ -842,19 +888,8 @@ def test_strict_slashes_behavior_adoption(app): | ||||
|     assert app.test_client.get("/second")[1].status == 200 | ||||
|     assert app.test_client.get("/second/")[1].status == 404 | ||||
|  | ||||
|     bp2 = Blueprint("bp2", strict_slashes=False) | ||||
|  | ||||
|     @bp2.get("/third") | ||||
|     def third(request): | ||||
|         return text("third") | ||||
|  | ||||
|     app.blueprint(bp2) | ||||
|     assert app.test_client.get("/third")[1].status == 200 | ||||
|     assert app.test_client.get("/third/")[1].status == 200 | ||||
|  | ||||
|     @app.get("/f1", strict_slashes=False) | ||||
|     def f1(request): | ||||
|         return text("f1") | ||||
|  | ||||
|     assert app.test_client.get("/f1")[1].status == 200 | ||||
|     assert app.test_client.get("/f1/")[1].status == 200 | ||||
|   | ||||
| @@ -43,7 +43,7 @@ async def test_cookies_asgi(app): | ||||
|     response_cookies = SimpleCookie() | ||||
|     response_cookies.load(response.headers.get("set-cookie", {})) | ||||
|  | ||||
|     assert response.text == "Cookies are: working!" | ||||
|     assert response.body == b"Cookies are: working!" | ||||
|     assert response_cookies["right_back"].value == "at you" | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -1,44 +1,44 @@ | ||||
| import pytest | ||||
| # import pytest | ||||
|  | ||||
| from sanic.response import text | ||||
| from sanic.router import RouteExists | ||||
| # from sanic.response import text | ||||
| # from sanic.router import RouteExists | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize( | ||||
|     "method,attr, expected", | ||||
|     [ | ||||
|         ("get", "text", "OK1 test"), | ||||
|         ("post", "text", "OK2 test"), | ||||
|         ("put", "text", "OK2 test"), | ||||
|         ("delete", "status", 405), | ||||
|     ], | ||||
| ) | ||||
| def test_overload_dynamic_routes(app, method, attr, expected): | ||||
|     @app.route("/overload/<param>", methods=["GET"]) | ||||
|     async def handler1(request, param): | ||||
|         return text("OK1 " + param) | ||||
| # @pytest.mark.parametrize( | ||||
| #     "method,attr, expected", | ||||
| #     [ | ||||
| #         ("get", "text", "OK1 test"), | ||||
| #         ("post", "text", "OK2 test"), | ||||
| #         ("put", "text", "OK2 test"), | ||||
| #         ("delete", "status", 405), | ||||
| #     ], | ||||
| # ) | ||||
| # def test_overload_dynamic_routes(app, method, attr, expected): | ||||
| #     @app.route("/overload/<param>", methods=["GET"]) | ||||
| #     async def handler1(request, param): | ||||
| #         return text("OK1 " + param) | ||||
|  | ||||
|     @app.route("/overload/<param>", methods=["POST", "PUT"]) | ||||
|     async def handler2(request, param): | ||||
|         return text("OK2 " + param) | ||||
| #     @app.route("/overload/<param>", methods=["POST", "PUT"]) | ||||
| #     async def handler2(request, param): | ||||
| #         return text("OK2 " + param) | ||||
|  | ||||
|     request, response = getattr(app.test_client, method)("/overload/test") | ||||
|     assert getattr(response, attr) == expected | ||||
| #     request, response = getattr(app.test_client, method)("/overload/test") | ||||
| #     assert getattr(response, attr) == expected | ||||
|  | ||||
|  | ||||
| def test_overload_dynamic_routes_exist(app): | ||||
|     @app.route("/overload/<param>", methods=["GET"]) | ||||
|     async def handler1(request, param): | ||||
|         return text("OK1 " + param) | ||||
| # def test_overload_dynamic_routes_exist(app): | ||||
| #     @app.route("/overload/<param>", methods=["GET"]) | ||||
| #     async def handler1(request, param): | ||||
| #         return text("OK1 " + param) | ||||
|  | ||||
|     @app.route("/overload/<param>", methods=["POST", "PUT"]) | ||||
|     async def handler2(request, param): | ||||
|         return text("OK2 " + param) | ||||
| #     @app.route("/overload/<param>", methods=["POST", "PUT"]) | ||||
| #     async def handler2(request, param): | ||||
| #         return text("OK2 " + param) | ||||
|  | ||||
|     # if this doesn't raise an error, than at least the below should happen: | ||||
|     # assert response.text == 'Duplicated' | ||||
|     with pytest.raises(RouteExists): | ||||
| #     # if this doesn't raise an error, than at least the below should happen: | ||||
| #     # assert response.text == 'Duplicated' | ||||
| #     with pytest.raises(RouteExists): | ||||
|  | ||||
|         @app.route("/overload/<param>", methods=["PUT", "DELETE"]) | ||||
|         async def handler3(request, param): | ||||
|             return text("Duplicated") | ||||
| #         @app.route("/overload/<param>", methods=["PUT", "DELETE"]) | ||||
| #         async def handler3(request, param): | ||||
| #             return text("Duplicated") | ||||
|   | ||||
| @@ -126,8 +126,9 @@ def test_html_traceback_output_in_debug_mode(): | ||||
|     assert response.status == 500 | ||||
|     soup = BeautifulSoup(response.body, "html.parser") | ||||
|     html = str(soup) | ||||
|     print(html) | ||||
|  | ||||
|     assert "response = handler(request, *args, **kwargs)" in html | ||||
|     assert "response = handler(request, **kwargs)" in html | ||||
|     assert "handler_4" in html | ||||
|     assert "foo = bar" in html | ||||
|  | ||||
| @@ -151,7 +152,7 @@ def test_chained_exception_handler(): | ||||
|     soup = BeautifulSoup(response.body, "html.parser") | ||||
|     html = str(soup) | ||||
|  | ||||
|     assert "response = handler(request, *args, **kwargs)" in html | ||||
|     assert "response = handler(request, **kwargs)" in html | ||||
|     assert "handler_6" in html | ||||
|     assert "foo = 1 / arg" in html | ||||
|     assert "ValueError" in html | ||||
|   | ||||
| @@ -103,7 +103,13 @@ def test_logging_pass_customer_logconfig(): | ||||
|         assert fmt._fmt == modified_config["formatters"]["access"]["format"] | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("debug", (True, False)) | ||||
| @pytest.mark.parametrize( | ||||
|     "debug", | ||||
|     ( | ||||
|         True, | ||||
|         False, | ||||
|     ), | ||||
| ) | ||||
| def test_log_connection_lost(app, debug, monkeypatch): | ||||
|     """ Should not log Connection lost exception on non debug """ | ||||
|     stream = StringIO() | ||||
| @@ -117,7 +123,7 @@ def test_log_connection_lost(app, debug, monkeypatch): | ||||
|         request.transport.close() | ||||
|         return response | ||||
|  | ||||
|     req, res = app.test_client.get("/conn_lost", debug=debug) | ||||
|     req, res = app.test_client.get("/conn_lost", debug=debug, allow_none=True) | ||||
|     assert res is None | ||||
|  | ||||
|     log = stream.getvalue() | ||||
|   | ||||
| @@ -102,6 +102,7 @@ def test_middleware_response_raise_exception(app, caplog): | ||||
|     async def process_response(request, response): | ||||
|         raise Exception("Exception at response middleware") | ||||
|  | ||||
|     app.route("/")(lambda x: x) | ||||
|     with caplog.at_level(logging.ERROR): | ||||
|         reqrequest, response = app.test_client.get("/fail") | ||||
|  | ||||
| @@ -129,7 +130,7 @@ def test_middleware_override_request(app): | ||||
|     async def handler(request): | ||||
|         return text("FAIL") | ||||
|  | ||||
|     response = app.test_client.get("/", gather_request=False) | ||||
|     _, response = app.test_client.get("/", gather_request=False) | ||||
|  | ||||
|     assert response.status == 200 | ||||
|     assert response.text == "OK" | ||||
|   | ||||
| @@ -68,9 +68,12 @@ def handler(request): | ||||
| @pytest.mark.parametrize("protocol", [3, 4]) | ||||
| def test_pickle_app(app, protocol): | ||||
|     app.route("/")(handler) | ||||
|     app.router.finalize() | ||||
|     app.router.reset() | ||||
|     p_app = pickle.dumps(app, protocol=protocol) | ||||
|     del app | ||||
|     up_p_app = pickle.loads(p_app) | ||||
|     up_p_app.router.finalize() | ||||
|     assert up_p_app | ||||
|     request, response = up_p_app.test_client.get("/") | ||||
|     assert response.text == "Hello" | ||||
| @@ -81,9 +84,12 @@ def test_pickle_app_with_bp(app, protocol): | ||||
|     bp = Blueprint("test_text") | ||||
|     bp.route("/")(handler) | ||||
|     app.blueprint(bp) | ||||
|     app.router.finalize() | ||||
|     app.router.reset() | ||||
|     p_app = pickle.dumps(app, protocol=protocol) | ||||
|     del app | ||||
|     up_p_app = pickle.loads(p_app) | ||||
|     up_p_app.router.finalize() | ||||
|     assert up_p_app | ||||
|     request, response = up_p_app.test_client.get("/") | ||||
|     assert response.text == "Hello" | ||||
| @@ -93,9 +99,12 @@ def test_pickle_app_with_bp(app, protocol): | ||||
| def test_pickle_app_with_static(app, protocol): | ||||
|     app.route("/")(handler) | ||||
|     app.static("/static", "/tmp/static") | ||||
|     app.router.finalize() | ||||
|     app.router.reset() | ||||
|     p_app = pickle.dumps(app, protocol=protocol) | ||||
|     del app | ||||
|     up_p_app = pickle.loads(p_app) | ||||
|     up_p_app.router.finalize() | ||||
|     assert up_p_app | ||||
|     request, response = up_p_app.test_client.get("/static/missing.txt") | ||||
|     assert response.status == 404 | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import asyncio | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| from sanic import Sanic | ||||
| from sanic.blueprints import Blueprint | ||||
| from sanic.constants import HTTP_METHODS | ||||
| from sanic.exceptions import URLBuildError | ||||
| @@ -17,7 +18,9 @@ from sanic.response import text | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("method", HTTP_METHODS) | ||||
| def test_versioned_named_routes_get(app, method): | ||||
| def test_versioned_named_routes_get(method): | ||||
|     app = Sanic("app") | ||||
|  | ||||
|     bp = Blueprint("test_bp", url_prefix="/bp") | ||||
|  | ||||
|     method = method.lower() | ||||
| @@ -32,7 +35,6 @@ def test_versioned_named_routes_get(app, method): | ||||
|             return text("OK") | ||||
|  | ||||
|     else: | ||||
|         print(func) | ||||
|         raise | ||||
|  | ||||
|     func = getattr(bp, method) | ||||
| @@ -43,15 +45,28 @@ def test_versioned_named_routes_get(app, method): | ||||
|             return text("OK") | ||||
|  | ||||
|     else: | ||||
|         print(func) | ||||
|         raise | ||||
|  | ||||
|     app.blueprint(bp) | ||||
|  | ||||
|     assert app.router.routes_all[f"/v1/{method}"].name == route_name | ||||
|     assert ( | ||||
|         app.router.routes_all[ | ||||
|             ( | ||||
|                 "v1", | ||||
|                 method, | ||||
|             ) | ||||
|         ].name | ||||
|         == f"app.{route_name}" | ||||
|     ) | ||||
|  | ||||
|     route = app.router.routes_all[f"/v1/bp/{method}"] | ||||
|     assert route.name == f"test_bp.{route_name2}" | ||||
|     route = app.router.routes_all[ | ||||
|         ( | ||||
|             "v1", | ||||
|             "bp", | ||||
|             method, | ||||
|         ) | ||||
|     ] | ||||
|     assert route.name == f"app.test_bp.{route_name2}" | ||||
|  | ||||
|     assert app.url_for(route_name) == f"/v1/{method}" | ||||
|     url = app.url_for(f"test_bp.{route_name2}") | ||||
| @@ -60,16 +75,19 @@ def test_versioned_named_routes_get(app, method): | ||||
|         app.url_for("handler") | ||||
|  | ||||
|  | ||||
| def test_shorthand_default_routes_get(app): | ||||
| def test_shorthand_default_routes_get(): | ||||
|     app = Sanic("app") | ||||
|  | ||||
|     @app.get("/get") | ||||
|     def handler(request): | ||||
|         return text("OK") | ||||
|  | ||||
|     assert app.router.routes_all["/get"].name == "handler" | ||||
|     assert app.router.routes_all[("get",)].name == "app.handler" | ||||
|     assert app.url_for("handler") == "/get" | ||||
|  | ||||
|  | ||||
| def test_shorthand_named_routes_get(app): | ||||
| def test_shorthand_named_routes_get(): | ||||
|     app = Sanic("app") | ||||
|     bp = Blueprint("test_bp", url_prefix="/bp") | ||||
|  | ||||
|     @app.get("/get", name="route_get") | ||||
| @@ -82,84 +100,106 @@ def test_shorthand_named_routes_get(app): | ||||
|  | ||||
|     app.blueprint(bp) | ||||
|  | ||||
|     assert app.router.routes_all["/get"].name == "route_get" | ||||
|     assert app.router.routes_all[("get",)].name == "app.route_get" | ||||
|     assert app.url_for("route_get") == "/get" | ||||
|     with pytest.raises(URLBuildError): | ||||
|         app.url_for("handler") | ||||
|  | ||||
|     assert app.router.routes_all["/bp/get"].name == "test_bp.route_bp" | ||||
|     assert ( | ||||
|         app.router.routes_all[ | ||||
|             ( | ||||
|                 "bp", | ||||
|                 "get", | ||||
|             ) | ||||
|         ].name | ||||
|         == "app.test_bp.route_bp" | ||||
|     ) | ||||
|     assert app.url_for("test_bp.route_bp") == "/bp/get" | ||||
|     with pytest.raises(URLBuildError): | ||||
|         app.url_for("test_bp.handler2") | ||||
|  | ||||
|  | ||||
| def test_shorthand_named_routes_post(app): | ||||
| def test_shorthand_named_routes_post(): | ||||
|     app = Sanic("app") | ||||
|  | ||||
|     @app.post("/post", name="route_name") | ||||
|     def handler(request): | ||||
|         return text("OK") | ||||
|  | ||||
|     assert app.router.routes_all["/post"].name == "route_name" | ||||
|     assert app.router.routes_all[("post",)].name == "app.route_name" | ||||
|     assert app.url_for("route_name") == "/post" | ||||
|     with pytest.raises(URLBuildError): | ||||
|         app.url_for("handler") | ||||
|  | ||||
|  | ||||
| def test_shorthand_named_routes_put(app): | ||||
| def test_shorthand_named_routes_put(): | ||||
|     app = Sanic("app") | ||||
|  | ||||
|     @app.put("/put", name="route_put") | ||||
|     def handler(request): | ||||
|         return text("OK") | ||||
|  | ||||
|     assert app.router.routes_all["/put"].name == "route_put" | ||||
|     assert app.router.routes_all[("put",)].name == "app.route_put" | ||||
|     assert app.url_for("route_put") == "/put" | ||||
|     with pytest.raises(URLBuildError): | ||||
|         app.url_for("handler") | ||||
|  | ||||
|  | ||||
| def test_shorthand_named_routes_delete(app): | ||||
| def test_shorthand_named_routes_delete(): | ||||
|     app = Sanic("app") | ||||
|  | ||||
|     @app.delete("/delete", name="route_delete") | ||||
|     def handler(request): | ||||
|         return text("OK") | ||||
|  | ||||
|     assert app.router.routes_all["/delete"].name == "route_delete" | ||||
|     assert app.router.routes_all[("delete",)].name == "app.route_delete" | ||||
|     assert app.url_for("route_delete") == "/delete" | ||||
|     with pytest.raises(URLBuildError): | ||||
|         app.url_for("handler") | ||||
|  | ||||
|  | ||||
| def test_shorthand_named_routes_patch(app): | ||||
| def test_shorthand_named_routes_patch(): | ||||
|     app = Sanic("app") | ||||
|  | ||||
|     @app.patch("/patch", name="route_patch") | ||||
|     def handler(request): | ||||
|         return text("OK") | ||||
|  | ||||
|     assert app.router.routes_all["/patch"].name == "route_patch" | ||||
|     assert app.router.routes_all[("patch",)].name == "app.route_patch" | ||||
|     assert app.url_for("route_patch") == "/patch" | ||||
|     with pytest.raises(URLBuildError): | ||||
|         app.url_for("handler") | ||||
|  | ||||
|  | ||||
| def test_shorthand_named_routes_head(app): | ||||
| def test_shorthand_named_routes_head(): | ||||
|     app = Sanic("app") | ||||
|  | ||||
|     @app.head("/head", name="route_head") | ||||
|     def handler(request): | ||||
|         return text("OK") | ||||
|  | ||||
|     assert app.router.routes_all["/head"].name == "route_head" | ||||
|     assert app.router.routes_all[("head",)].name == "app.route_head" | ||||
|     assert app.url_for("route_head") == "/head" | ||||
|     with pytest.raises(URLBuildError): | ||||
|         app.url_for("handler") | ||||
|  | ||||
|  | ||||
| def test_shorthand_named_routes_options(app): | ||||
| def test_shorthand_named_routes_options(): | ||||
|     app = Sanic("app") | ||||
|  | ||||
|     @app.options("/options", name="route_options") | ||||
|     def handler(request): | ||||
|         return text("OK") | ||||
|  | ||||
|     assert app.router.routes_all["/options"].name == "route_options" | ||||
|     assert app.router.routes_all[("options",)].name == "app.route_options" | ||||
|     assert app.url_for("route_options") == "/options" | ||||
|     with pytest.raises(URLBuildError): | ||||
|         app.url_for("handler") | ||||
|  | ||||
|  | ||||
| def test_named_static_routes(app): | ||||
| def test_named_static_routes(): | ||||
|     app = Sanic("app") | ||||
|  | ||||
|     @app.route("/test", name="route_test") | ||||
|     async def handler1(request): | ||||
|         return text("OK1") | ||||
| @@ -168,20 +208,21 @@ def test_named_static_routes(app): | ||||
|     async def handler2(request): | ||||
|         return text("OK2") | ||||
|  | ||||
|     assert app.router.routes_all["/test"].name == "route_test" | ||||
|     assert app.router.routes_static["/test"].name == "route_test" | ||||
|     assert app.router.routes_all[("test",)].name == "app.route_test" | ||||
|     assert app.router.routes_static[("test",)].name == "app.route_test" | ||||
|     assert app.url_for("route_test") == "/test" | ||||
|     with pytest.raises(URLBuildError): | ||||
|         app.url_for("handler1") | ||||
|  | ||||
|     assert app.router.routes_all["/pizazz"].name == "route_pizazz" | ||||
|     assert app.router.routes_static["/pizazz"].name == "route_pizazz" | ||||
|     assert app.router.routes_all[("pizazz",)].name == "app.route_pizazz" | ||||
|     assert app.router.routes_static[("pizazz",)].name == "app.route_pizazz" | ||||
|     assert app.url_for("route_pizazz") == "/pizazz" | ||||
|     with pytest.raises(URLBuildError): | ||||
|         app.url_for("handler2") | ||||
|  | ||||
|  | ||||
| def test_named_dynamic_route(app): | ||||
| def test_named_dynamic_route(): | ||||
|     app = Sanic("app") | ||||
|     results = [] | ||||
|  | ||||
|     @app.route("/folder/<name>", name="route_dynamic") | ||||
| @@ -189,52 +230,83 @@ def test_named_dynamic_route(app): | ||||
|         results.append(name) | ||||
|         return text("OK") | ||||
|  | ||||
|     assert app.router.routes_all["/folder/<name>"].name == "route_dynamic" | ||||
|     assert ( | ||||
|         app.router.routes_all[ | ||||
|             ( | ||||
|                 "folder", | ||||
|                 "<name>", | ||||
|             ) | ||||
|         ].name | ||||
|         == "app.route_dynamic" | ||||
|     ) | ||||
|     assert app.url_for("route_dynamic", name="test") == "/folder/test" | ||||
|     with pytest.raises(URLBuildError): | ||||
|         app.url_for("handler") | ||||
|  | ||||
|  | ||||
| def test_dynamic_named_route_regex(app): | ||||
| def test_dynamic_named_route_regex(): | ||||
|     app = Sanic("app") | ||||
|  | ||||
|     @app.route("/folder/<folder_id:[A-Za-z0-9]{0,4}>", name="route_re") | ||||
|     async def handler(request, folder_id): | ||||
|         return text("OK") | ||||
|  | ||||
|     route = app.router.routes_all["/folder/<folder_id:[A-Za-z0-9]{0,4}>"] | ||||
|     assert route.name == "route_re" | ||||
|     route = app.router.routes_all[ | ||||
|         ( | ||||
|             "folder", | ||||
|             "<folder_id:[A-Za-z0-9]{0,4}>", | ||||
|         ) | ||||
|     ] | ||||
|     assert route.name == "app.route_re" | ||||
|     assert app.url_for("route_re", folder_id="test") == "/folder/test" | ||||
|     with pytest.raises(URLBuildError): | ||||
|         app.url_for("handler") | ||||
|  | ||||
|  | ||||
| def test_dynamic_named_route_path(app): | ||||
| def test_dynamic_named_route_path(): | ||||
|     app = Sanic("app") | ||||
|  | ||||
|     @app.route("/<path:path>/info", name="route_dynamic_path") | ||||
|     async def handler(request, path): | ||||
|         return text("OK") | ||||
|  | ||||
|     route = app.router.routes_all["/<path:path>/info"] | ||||
|     assert route.name == "route_dynamic_path" | ||||
|     route = app.router.routes_all[ | ||||
|         ( | ||||
|             "<path:path>", | ||||
|             "info", | ||||
|         ) | ||||
|     ] | ||||
|     assert route.name == "app.route_dynamic_path" | ||||
|     assert app.url_for("route_dynamic_path", path="path/1") == "/path/1/info" | ||||
|     with pytest.raises(URLBuildError): | ||||
|         app.url_for("handler") | ||||
|  | ||||
|  | ||||
| def test_dynamic_named_route_unhashable(app): | ||||
| def test_dynamic_named_route_unhashable(): | ||||
|     app = Sanic("app") | ||||
|  | ||||
|     @app.route( | ||||
|         "/folder/<unhashable:[A-Za-z0-9/]+>/end/", name="route_unhashable" | ||||
|     ) | ||||
|     async def handler(request, unhashable): | ||||
|         return text("OK") | ||||
|  | ||||
|     route = app.router.routes_all["/folder/<unhashable:[A-Za-z0-9/]+>/end/"] | ||||
|     assert route.name == "route_unhashable" | ||||
|     route = app.router.routes_all[ | ||||
|         ( | ||||
|             "folder", | ||||
|             "<unhashable:[A-Za-z0-9/]+>", | ||||
|             "end", | ||||
|         ) | ||||
|     ] | ||||
|     assert route.name == "app.route_unhashable" | ||||
|     url = app.url_for("route_unhashable", unhashable="test/asdf") | ||||
|     assert url == "/folder/test/asdf/end" | ||||
|     with pytest.raises(URLBuildError): | ||||
|         app.url_for("handler") | ||||
|  | ||||
|  | ||||
| def test_websocket_named_route(app): | ||||
| def test_websocket_named_route(): | ||||
|     app = Sanic("app") | ||||
|     ev = asyncio.Event() | ||||
|  | ||||
|     @app.websocket("/ws", name="route_ws") | ||||
| @@ -242,26 +314,29 @@ def test_websocket_named_route(app): | ||||
|         assert ws.subprotocol is None | ||||
|         ev.set() | ||||
|  | ||||
|     assert app.router.routes_all["/ws"].name == "route_ws" | ||||
|     assert app.router.routes_all[("ws",)].name == "app.route_ws" | ||||
|     assert app.url_for("route_ws") == "/ws" | ||||
|     with pytest.raises(URLBuildError): | ||||
|         app.url_for("handler") | ||||
|  | ||||
|  | ||||
| def test_websocket_named_route_with_subprotocols(app): | ||||
| def test_websocket_named_route_with_subprotocols(): | ||||
|     app = Sanic("app") | ||||
|     results = [] | ||||
|  | ||||
|     @app.websocket("/ws", subprotocols=["foo", "bar"], name="route_ws") | ||||
|     async def handler(request, ws): | ||||
|         results.append(ws.subprotocol) | ||||
|  | ||||
|     assert app.router.routes_all["/ws"].name == "route_ws" | ||||
|     assert app.router.routes_all[("ws",)].name == "app.route_ws" | ||||
|     assert app.url_for("route_ws") == "/ws" | ||||
|     with pytest.raises(URLBuildError): | ||||
|         app.url_for("handler") | ||||
|  | ||||
|  | ||||
| def test_static_add_named_route(app): | ||||
| def test_static_add_named_route(): | ||||
|     app = Sanic("app") | ||||
|  | ||||
|     async def handler1(request): | ||||
|         return text("OK1") | ||||
|  | ||||
| @@ -271,20 +346,21 @@ def test_static_add_named_route(app): | ||||
|     app.add_route(handler1, "/test", name="route_test") | ||||
|     app.add_route(handler2, "/test2", name="route_test2") | ||||
|  | ||||
|     assert app.router.routes_all["/test"].name == "route_test" | ||||
|     assert app.router.routes_static["/test"].name == "route_test" | ||||
|     assert app.router.routes_all[("test",)].name == "app.route_test" | ||||
|     assert app.router.routes_static[("test",)].name == "app.route_test" | ||||
|     assert app.url_for("route_test") == "/test" | ||||
|     with pytest.raises(URLBuildError): | ||||
|         app.url_for("handler1") | ||||
|  | ||||
|     assert app.router.routes_all["/test2"].name == "route_test2" | ||||
|     assert app.router.routes_static["/test2"].name == "route_test2" | ||||
|     assert app.router.routes_all[("test2",)].name == "app.route_test2" | ||||
|     assert app.router.routes_static[("test2",)].name == "app.route_test2" | ||||
|     assert app.url_for("route_test2") == "/test2" | ||||
|     with pytest.raises(URLBuildError): | ||||
|         app.url_for("handler2") | ||||
|  | ||||
|  | ||||
| def test_dynamic_add_named_route(app): | ||||
| def test_dynamic_add_named_route(): | ||||
|     app = Sanic("app") | ||||
|     results = [] | ||||
|  | ||||
|     async def handler(request, name): | ||||
| @@ -292,13 +368,17 @@ def test_dynamic_add_named_route(app): | ||||
|         return text("OK") | ||||
|  | ||||
|     app.add_route(handler, "/folder/<name>", name="route_dynamic") | ||||
|     assert app.router.routes_all["/folder/<name>"].name == "route_dynamic" | ||||
|     assert ( | ||||
|         app.router.routes_all[("folder", "<name>")].name == "app.route_dynamic" | ||||
|     ) | ||||
|     assert app.url_for("route_dynamic", name="test") == "/folder/test" | ||||
|     with pytest.raises(URLBuildError): | ||||
|         app.url_for("handler") | ||||
|  | ||||
|  | ||||
| def test_dynamic_add_named_route_unhashable(app): | ||||
| def test_dynamic_add_named_route_unhashable(): | ||||
|     app = Sanic("app") | ||||
|  | ||||
|     async def handler(request, unhashable): | ||||
|         return text("OK") | ||||
|  | ||||
| @@ -307,15 +387,23 @@ def test_dynamic_add_named_route_unhashable(app): | ||||
|         "/folder/<unhashable:[A-Za-z0-9/]+>/end/", | ||||
|         name="route_unhashable", | ||||
|     ) | ||||
|     route = app.router.routes_all["/folder/<unhashable:[A-Za-z0-9/]+>/end/"] | ||||
|     assert route.name == "route_unhashable" | ||||
|     route = app.router.routes_all[ | ||||
|         ( | ||||
|             "folder", | ||||
|             "<unhashable:[A-Za-z0-9/]+>", | ||||
|             "end", | ||||
|         ) | ||||
|     ] | ||||
|     assert route.name == "app.route_unhashable" | ||||
|     url = app.url_for("route_unhashable", unhashable="folder1") | ||||
|     assert url == "/folder/folder1/end" | ||||
|     with pytest.raises(URLBuildError): | ||||
|         app.url_for("handler") | ||||
|  | ||||
|  | ||||
| def test_overload_routes(app): | ||||
| def test_overload_routes(): | ||||
|     app = Sanic("app") | ||||
|  | ||||
|     @app.route("/overload", methods=["GET"], name="route_first") | ||||
|     async def handler1(request): | ||||
|         return text("OK1") | ||||
| @@ -342,7 +430,7 @@ def test_overload_routes(app): | ||||
|     request, response = app.test_client.put(app.url_for("route_second")) | ||||
|     assert response.text == "OK2" | ||||
|  | ||||
|     assert app.router.routes_all["/overload"].name == "route_first" | ||||
|     assert app.router.routes_all[("overload",)].name == "app.route_first" | ||||
|     with pytest.raises(URLBuildError): | ||||
|         app.url_for("handler1") | ||||
|  | ||||
|   | ||||
| @@ -13,7 +13,7 @@ def test_payload_too_large_from_error_handler(app): | ||||
|     def handler_exception(request, exception): | ||||
|         return text("Payload Too Large from error_handler.", 413) | ||||
|  | ||||
|     response = app.test_client.get("/1", gather_request=False) | ||||
|     _, response = app.test_client.get("/1", gather_request=False) | ||||
|     assert response.status == 413 | ||||
|     assert response.text == "Payload Too Large from error_handler." | ||||
|  | ||||
| @@ -25,7 +25,7 @@ def test_payload_too_large_at_data_received_default(app): | ||||
|     async def handler2(request): | ||||
|         return text("OK") | ||||
|  | ||||
|     response = app.test_client.get("/1", gather_request=False) | ||||
|     _, response = app.test_client.get("/1", gather_request=False) | ||||
|     assert response.status == 413 | ||||
|     assert "Request header" in response.text | ||||
|  | ||||
| @@ -38,6 +38,6 @@ def test_payload_too_large_at_on_header_default(app): | ||||
|         return text("OK") | ||||
|  | ||||
|     data = "a" * 1000 | ||||
|     response = app.test_client.post("/1", gather_request=False, data=data) | ||||
|     _, response = app.test_client.post("/1", gather_request=False, data=data) | ||||
|     assert response.status == 413 | ||||
|     assert "Request body" in response.text | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| from urllib.parse import quote | ||||
| from urllib.parse import quote, unquote | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| @@ -109,7 +109,14 @@ def test_redirect_with_header_injection(redirect_app): | ||||
|     assert not response.text.startswith("test-body") | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("test_str", ["sanic-test", "sanictest", "sanic test"]) | ||||
| @pytest.mark.parametrize( | ||||
|     "test_str", | ||||
|     [ | ||||
|         "sanic-test", | ||||
|         "sanictest", | ||||
|         "sanic test", | ||||
|     ], | ||||
| ) | ||||
| def test_redirect_with_params(app, test_str): | ||||
|     use_in_uri = quote(test_str) | ||||
|  | ||||
| @@ -117,7 +124,7 @@ def test_redirect_with_params(app, test_str): | ||||
|     async def init_handler(request, test): | ||||
|         return redirect(f"/api/v2/test/{use_in_uri}/") | ||||
|  | ||||
|     @app.route("/api/v2/test/<test>/") | ||||
|     @app.route("/api/v2/test/<test>/", unquote=True) | ||||
|     async def target_handler(request, test): | ||||
|         assert test == test_str | ||||
|         return text("OK") | ||||
| @@ -125,4 +132,4 @@ def test_redirect_with_params(app, test_str): | ||||
|     _, response = app.test_client.get(f"/api/v1/test/{use_in_uri}/") | ||||
|     assert response.status == 200 | ||||
|  | ||||
|     assert response.content == b"OK" | ||||
|     assert response.body == b"OK" | ||||
|   | ||||
| @@ -42,6 +42,8 @@ def write_app(filename, **runargs): | ||||
|  | ||||
|             app = Sanic(__name__) | ||||
|  | ||||
|             app.route("/")(lambda x: x) | ||||
|  | ||||
|             @app.listener("after_server_start") | ||||
|             def complete(*args): | ||||
|                 print("complete", os.getpid(), {text!r}) | ||||
|   | ||||
| @@ -10,7 +10,6 @@ import pytest | ||||
|  | ||||
| from sanic_testing.testing import ( | ||||
|     ASGI_BASE_URL, | ||||
|     ASGI_HOST, | ||||
|     ASGI_PORT, | ||||
|     HOST, | ||||
|     PORT, | ||||
| @@ -19,7 +18,7 @@ from sanic_testing.testing import ( | ||||
|  | ||||
| from sanic import Blueprint, Sanic | ||||
| from sanic.exceptions import ServerError | ||||
| from sanic.request import DEFAULT_HTTP_CONTENT_TYPE, Request, RequestParameters | ||||
| from sanic.request import DEFAULT_HTTP_CONTENT_TYPE, RequestParameters | ||||
| from sanic.response import html, json, text | ||||
|  | ||||
|  | ||||
| @@ -35,7 +34,7 @@ def test_sync(app): | ||||
|  | ||||
|     request, response = app.test_client.get("/") | ||||
|  | ||||
|     assert response.text == "Hello" | ||||
|     assert response.body == b"Hello" | ||||
|  | ||||
|  | ||||
| @pytest.mark.asyncio | ||||
| @@ -46,7 +45,7 @@ async def test_sync_asgi(app): | ||||
|  | ||||
|     request, response = await app.asgi_client.get("/") | ||||
|  | ||||
|     assert response.text == "Hello" | ||||
|     assert response.body == b"Hello" | ||||
|  | ||||
|  | ||||
| def test_ip(app): | ||||
| @@ -56,7 +55,7 @@ def test_ip(app): | ||||
|  | ||||
|     request, response = app.test_client.get("/") | ||||
|  | ||||
|     assert response.text == "127.0.0.1" | ||||
|     assert response.body == b"127.0.0.1" | ||||
|  | ||||
|  | ||||
| @pytest.mark.asyncio | ||||
| @@ -67,10 +66,12 @@ async def test_url_asgi(app): | ||||
|  | ||||
|     request, response = await app.asgi_client.get("/") | ||||
|  | ||||
|     if response.text.endswith("/") and not ASGI_BASE_URL.endswith("/"): | ||||
|         response.text[:-1] == ASGI_BASE_URL | ||||
|     if response.body.decode().endswith("/") and not ASGI_BASE_URL.endswith( | ||||
|         "/" | ||||
|     ): | ||||
|         response.body[:-1] == ASGI_BASE_URL.encode() | ||||
|     else: | ||||
|         assert response.text == ASGI_BASE_URL | ||||
|         assert response.body == ASGI_BASE_URL.encode() | ||||
|  | ||||
|  | ||||
| def test_text(app): | ||||
| @@ -80,7 +81,7 @@ def test_text(app): | ||||
|  | ||||
|     request, response = app.test_client.get("/") | ||||
|  | ||||
|     assert response.text == "Hello" | ||||
|     assert response.body == b"Hello" | ||||
|  | ||||
|  | ||||
| def test_html(app): | ||||
| @@ -109,13 +110,13 @@ def test_html(app): | ||||
|  | ||||
|     request, response = app.test_client.get("/") | ||||
|     assert response.content_type == "text/html; charset=utf-8" | ||||
|     assert response.text == "<h1>Hello</h1>" | ||||
|     assert response.body == b"<h1>Hello</h1>" | ||||
|  | ||||
|     request, response = app.test_client.get("/foo") | ||||
|     assert response.text == "<h1>Foo</h1>" | ||||
|     assert response.body == b"<h1>Foo</h1>" | ||||
|  | ||||
|     request, response = app.test_client.get("/bar") | ||||
|     assert response.text == "<h1>Bar object repr</h1>" | ||||
|     assert response.body == b"<h1>Bar object repr</h1>" | ||||
|  | ||||
|  | ||||
| @pytest.mark.asyncio | ||||
| @@ -126,7 +127,7 @@ async def test_text_asgi(app): | ||||
|  | ||||
|     request, response = await app.asgi_client.get("/") | ||||
|  | ||||
|     assert response.text == "Hello" | ||||
|     assert response.body == b"Hello" | ||||
|  | ||||
|  | ||||
| def test_headers(app): | ||||
| @@ -186,7 +187,7 @@ def test_invalid_response(app): | ||||
|  | ||||
|     request, response = app.test_client.get("/") | ||||
|     assert response.status == 500 | ||||
|     assert response.text == "Internal Server Error." | ||||
|     assert response.body == b"Internal Server Error." | ||||
|  | ||||
|  | ||||
| @pytest.mark.asyncio | ||||
| @@ -201,7 +202,7 @@ async def test_invalid_response_asgi(app): | ||||
|  | ||||
|     request, response = await app.asgi_client.get("/") | ||||
|     assert response.status == 500 | ||||
|     assert response.text == "Internal Server Error." | ||||
|     assert response.body == b"Internal Server Error." | ||||
|  | ||||
|  | ||||
| def test_json(app): | ||||
| @@ -224,7 +225,7 @@ async def test_json_asgi(app): | ||||
|  | ||||
|     request, response = await app.asgi_client.get("/") | ||||
|  | ||||
|     results = json_loads(response.text) | ||||
|     results = json_loads(response.body) | ||||
|  | ||||
|     assert results.get("test") is True | ||||
|  | ||||
| @@ -237,7 +238,7 @@ def test_empty_json(app): | ||||
|  | ||||
|     request, response = app.test_client.get("/") | ||||
|     assert response.status == 200 | ||||
|     assert response.text == "null" | ||||
|     assert response.body == b"null" | ||||
|  | ||||
|  | ||||
| @pytest.mark.asyncio | ||||
| @@ -249,7 +250,7 @@ async def test_empty_json_asgi(app): | ||||
|  | ||||
|     request, response = await app.asgi_client.get("/") | ||||
|     assert response.status == 200 | ||||
|     assert response.text == "null" | ||||
|     assert response.body == b"null" | ||||
|  | ||||
|  | ||||
| def test_invalid_json(app): | ||||
| @@ -423,12 +424,12 @@ def test_content_type(app): | ||||
|  | ||||
|     request, response = app.test_client.get("/") | ||||
|     assert request.content_type == DEFAULT_HTTP_CONTENT_TYPE | ||||
|     assert response.text == DEFAULT_HTTP_CONTENT_TYPE | ||||
|     assert response.body.decode() == DEFAULT_HTTP_CONTENT_TYPE | ||||
|  | ||||
|     headers = {"content-type": "application/json"} | ||||
|     request, response = app.test_client.get("/", headers=headers) | ||||
|     assert request.content_type == "application/json" | ||||
|     assert response.text == "application/json" | ||||
|     assert response.body == b"application/json" | ||||
|  | ||||
|  | ||||
| @pytest.mark.asyncio | ||||
| @@ -439,12 +440,12 @@ async def test_content_type_asgi(app): | ||||
|  | ||||
|     request, response = await app.asgi_client.get("/") | ||||
|     assert request.content_type == DEFAULT_HTTP_CONTENT_TYPE | ||||
|     assert response.text == DEFAULT_HTTP_CONTENT_TYPE | ||||
|     assert response.body.decode() == DEFAULT_HTTP_CONTENT_TYPE | ||||
|  | ||||
|     headers = {"content-type": "application/json"} | ||||
|     request, response = await app.asgi_client.get("/", headers=headers) | ||||
|     assert request.content_type == "application/json" | ||||
|     assert response.text == "application/json" | ||||
|     assert response.body == b"application/json" | ||||
|  | ||||
|  | ||||
| def test_standard_forwarded(app): | ||||
| @@ -581,14 +582,15 @@ async def test_standard_forwarded_asgi(app): | ||||
|         "X-Scheme": "ws", | ||||
|     } | ||||
|     request, response = await app.asgi_client.get("/", headers=headers) | ||||
|     assert response.json() == {"for": "127.0.0.2", "proto": "ws"} | ||||
|  | ||||
|     assert response.json == {"for": "127.0.0.2", "proto": "ws"} | ||||
|     assert request.remote_addr == "127.0.0.2" | ||||
|     assert request.scheme == "ws" | ||||
|     assert request.server_port == ASGI_PORT | ||||
|  | ||||
|     app.config.FORWARDED_SECRET = "mySecret" | ||||
|     request, response = await app.asgi_client.get("/", headers=headers) | ||||
|     assert response.json() == { | ||||
|     assert response.json == { | ||||
|         "for": "[::2]", | ||||
|         "proto": "https", | ||||
|         "host": "me.tld", | ||||
| @@ -603,13 +605,13 @@ async def test_standard_forwarded_asgi(app): | ||||
|     # Empty Forwarded header -> use X-headers | ||||
|     headers["Forwarded"] = "" | ||||
|     request, response = await app.asgi_client.get("/", headers=headers) | ||||
|     assert response.json() == {"for": "127.0.0.2", "proto": "ws"} | ||||
|     assert response.json == {"for": "127.0.0.2", "proto": "ws"} | ||||
|  | ||||
|     # Header present but not matching anything | ||||
|     request, response = await app.asgi_client.get( | ||||
|         "/", headers={"Forwarded": "."} | ||||
|     ) | ||||
|     assert response.json() == {} | ||||
|     assert response.json == {} | ||||
|  | ||||
|     # Forwarded header present but no matching secret -> use X-headers | ||||
|     headers = { | ||||
| @@ -617,13 +619,13 @@ async def test_standard_forwarded_asgi(app): | ||||
|         "X-Real-IP": "127.0.0.2", | ||||
|     } | ||||
|     request, response = await app.asgi_client.get("/", headers=headers) | ||||
|     assert response.json() == {"for": "127.0.0.2"} | ||||
|     assert response.json == {"for": "127.0.0.2"} | ||||
|     assert request.remote_addr == "127.0.0.2" | ||||
|  | ||||
|     # Different formatting and hitting both ends of the header | ||||
|     headers = {"Forwarded": 'Secret="mySecret";For=127.0.0.4;Port=1234'} | ||||
|     request, response = await app.asgi_client.get("/", headers=headers) | ||||
|     assert response.json() == { | ||||
|     assert response.json == { | ||||
|         "for": "127.0.0.4", | ||||
|         "port": 1234, | ||||
|         "secret": "mySecret", | ||||
| @@ -632,7 +634,7 @@ async def test_standard_forwarded_asgi(app): | ||||
|     # Test escapes (modify this if you see anyone implementing quoted-pairs) | ||||
|     headers = {"Forwarded": 'for=test;quoted="\\,x=x;y=\\";secret=mySecret'} | ||||
|     request, response = await app.asgi_client.get("/", headers=headers) | ||||
|     assert response.json() == { | ||||
|     assert response.json == { | ||||
|         "for": "test", | ||||
|         "quoted": "\\,x=x;y=\\", | ||||
|         "secret": "mySecret", | ||||
| @@ -641,17 +643,17 @@ async def test_standard_forwarded_asgi(app): | ||||
|     # Secret insulated by malformed field #1 | ||||
|     headers = {"Forwarded": "for=test;secret=mySecret;b0rked;proto=wss;"} | ||||
|     request, response = await app.asgi_client.get("/", headers=headers) | ||||
|     assert response.json() == {"for": "test", "secret": "mySecret"} | ||||
|     assert response.json == {"for": "test", "secret": "mySecret"} | ||||
|  | ||||
|     # Secret insulated by malformed field #2 | ||||
|     headers = {"Forwarded": "for=test;b0rked;secret=mySecret;proto=wss"} | ||||
|     request, response = await app.asgi_client.get("/", headers=headers) | ||||
|     assert response.json() == {"proto": "wss", "secret": "mySecret"} | ||||
|     assert response.json == {"proto": "wss", "secret": "mySecret"} | ||||
|  | ||||
|     # Unexpected termination should not lose existing acceptable values | ||||
|     headers = {"Forwarded": "b0rked;secret=mySecret;proto=wss"} | ||||
|     request, response = await app.asgi_client.get("/", headers=headers) | ||||
|     assert response.json() == {"proto": "wss", "secret": "mySecret"} | ||||
|     assert response.json == {"proto": "wss", "secret": "mySecret"} | ||||
|  | ||||
|     # Field normalization | ||||
|     headers = { | ||||
| @@ -659,7 +661,7 @@ async def test_standard_forwarded_asgi(app): | ||||
|         'PATH="/With%20Spaces%22Quoted%22/sanicApp?key=val";SECRET=mySecret' | ||||
|     } | ||||
|     request, response = await app.asgi_client.get("/", headers=headers) | ||||
|     assert response.json() == { | ||||
|     assert response.json == { | ||||
|         "proto": "wss", | ||||
|         "by": "[cafe::8000]", | ||||
|         "host": "a:2", | ||||
| @@ -671,7 +673,10 @@ async def test_standard_forwarded_asgi(app): | ||||
|     app.config.FORWARDED_SECRET = "_proxySecret" | ||||
|     headers = {"Forwarded": "for=1.2.3.4; by=_proxySecret"} | ||||
|     request, response = await app.asgi_client.get("/", headers=headers) | ||||
|     assert response.json() == {"for": "1.2.3.4", "by": "_proxySecret"} | ||||
|     assert response.json == { | ||||
|         "for": "1.2.3.4", | ||||
|         "by": "_proxySecret", | ||||
|     } | ||||
|  | ||||
|  | ||||
| def test_remote_addr_with_two_proxies(app): | ||||
| @@ -685,33 +690,33 @@ def test_remote_addr_with_two_proxies(app): | ||||
|     headers = {"X-Real-IP": "127.0.0.2", "X-Forwarded-For": "127.0.1.1"} | ||||
|     request, response = app.test_client.get("/", headers=headers) | ||||
|     assert request.remote_addr == "127.0.0.2" | ||||
|     assert response.text == "127.0.0.2" | ||||
|     assert response.body == b"127.0.0.2" | ||||
|  | ||||
|     headers = {"X-Forwarded-For": "127.0.1.1"} | ||||
|     request, response = app.test_client.get("/", headers=headers) | ||||
|     assert request.remote_addr == "" | ||||
|     assert response.text == "" | ||||
|     assert response.body == b"" | ||||
|  | ||||
|     headers = {"X-Forwarded-For": "127.0.0.1, 127.0.1.2"} | ||||
|     request, response = app.test_client.get("/", headers=headers) | ||||
|     assert request.remote_addr == "127.0.0.1" | ||||
|     assert response.text == "127.0.0.1" | ||||
|     assert response.body == b"127.0.0.1" | ||||
|  | ||||
|     request, response = app.test_client.get("/") | ||||
|     assert request.remote_addr == "" | ||||
|     assert response.text == "" | ||||
|     assert response.body == b"" | ||||
|  | ||||
|     headers = {"X-Forwarded-For": "127.0.0.1, ,   ,,127.0.1.2"} | ||||
|     request, response = app.test_client.get("/", headers=headers) | ||||
|     assert request.remote_addr == "127.0.0.1" | ||||
|     assert response.text == "127.0.0.1" | ||||
|     assert response.body == b"127.0.0.1" | ||||
|  | ||||
|     headers = { | ||||
|         "X-Forwarded-For": ", 127.0.2.2, ,  ,127.0.0.1, ,   ,,127.0.1.2" | ||||
|     } | ||||
|     request, response = app.test_client.get("/", headers=headers) | ||||
|     assert request.remote_addr == "127.0.0.1" | ||||
|     assert response.text == "127.0.0.1" | ||||
|     assert response.body == b"127.0.0.1" | ||||
|  | ||||
|  | ||||
| @pytest.mark.asyncio | ||||
| @@ -726,33 +731,33 @@ async def test_remote_addr_with_two_proxies_asgi(app): | ||||
|     headers = {"X-Real-IP": "127.0.0.2", "X-Forwarded-For": "127.0.1.1"} | ||||
|     request, response = await app.asgi_client.get("/", headers=headers) | ||||
|     assert request.remote_addr == "127.0.0.2" | ||||
|     assert response.text == "127.0.0.2" | ||||
|     assert response.body == b"127.0.0.2" | ||||
|  | ||||
|     headers = {"X-Forwarded-For": "127.0.1.1"} | ||||
|     request, response = await app.asgi_client.get("/", headers=headers) | ||||
|     assert request.remote_addr == "" | ||||
|     assert response.text == "" | ||||
|     assert response.body == b"" | ||||
|  | ||||
|     headers = {"X-Forwarded-For": "127.0.0.1, 127.0.1.2"} | ||||
|     request, response = await app.asgi_client.get("/", headers=headers) | ||||
|     assert request.remote_addr == "127.0.0.1" | ||||
|     assert response.text == "127.0.0.1" | ||||
|     assert response.body == b"127.0.0.1" | ||||
|  | ||||
|     request, response = await app.asgi_client.get("/") | ||||
|     assert request.remote_addr == "" | ||||
|     assert response.text == "" | ||||
|     assert response.body == b"" | ||||
|  | ||||
|     headers = {"X-Forwarded-For": "127.0.0.1, ,   ,,127.0.1.2"} | ||||
|     request, response = await app.asgi_client.get("/", headers=headers) | ||||
|     assert request.remote_addr == "127.0.0.1" | ||||
|     assert response.text == "127.0.0.1" | ||||
|     assert response.body == b"127.0.0.1" | ||||
|  | ||||
|     headers = { | ||||
|         "X-Forwarded-For": ", 127.0.2.2, ,  ,127.0.0.1, ,   ,,127.0.1.2" | ||||
|     } | ||||
|     request, response = await app.asgi_client.get("/", headers=headers) | ||||
|     assert request.remote_addr == "127.0.0.1" | ||||
|     assert response.text == "127.0.0.1" | ||||
|     assert response.body == b"127.0.0.1" | ||||
|  | ||||
|  | ||||
| def test_remote_addr_without_proxy(app): | ||||
| @@ -765,17 +770,17 @@ def test_remote_addr_without_proxy(app): | ||||
|     headers = {"X-Real-IP": "127.0.0.2", "X-Forwarded-For": "127.0.1.1"} | ||||
|     request, response = app.test_client.get("/", headers=headers) | ||||
|     assert request.remote_addr == "" | ||||
|     assert response.text == "" | ||||
|     assert response.body == b"" | ||||
|  | ||||
|     headers = {"X-Forwarded-For": "127.0.1.1"} | ||||
|     request, response = app.test_client.get("/", headers=headers) | ||||
|     assert request.remote_addr == "" | ||||
|     assert response.text == "" | ||||
|     assert response.body == b"" | ||||
|  | ||||
|     headers = {"X-Forwarded-For": "127.0.0.1, 127.0.1.2"} | ||||
|     request, response = app.test_client.get("/", headers=headers) | ||||
|     assert request.remote_addr == "" | ||||
|     assert response.text == "" | ||||
|     assert response.body == b"" | ||||
|  | ||||
|  | ||||
| @pytest.mark.asyncio | ||||
| @@ -789,17 +794,17 @@ async def test_remote_addr_without_proxy_asgi(app): | ||||
|     headers = {"X-Real-IP": "127.0.0.2", "X-Forwarded-For": "127.0.1.1"} | ||||
|     request, response = await app.asgi_client.get("/", headers=headers) | ||||
|     assert request.remote_addr == "" | ||||
|     assert response.text == "" | ||||
|     assert response.body == b"" | ||||
|  | ||||
|     headers = {"X-Forwarded-For": "127.0.1.1"} | ||||
|     request, response = await app.asgi_client.get("/", headers=headers) | ||||
|     assert request.remote_addr == "" | ||||
|     assert response.text == "" | ||||
|     assert response.body == b"" | ||||
|  | ||||
|     headers = {"X-Forwarded-For": "127.0.0.1, 127.0.1.2"} | ||||
|     request, response = await app.asgi_client.get("/", headers=headers) | ||||
|     assert request.remote_addr == "" | ||||
|     assert response.text == "" | ||||
|     assert response.body == b"" | ||||
|  | ||||
|  | ||||
| def test_remote_addr_custom_headers(app): | ||||
| @@ -814,17 +819,17 @@ def test_remote_addr_custom_headers(app): | ||||
|     headers = {"X-Real-IP": "127.0.0.2", "Forwarded": "127.0.1.1"} | ||||
|     request, response = app.test_client.get("/", headers=headers) | ||||
|     assert request.remote_addr == "127.0.1.1" | ||||
|     assert response.text == "127.0.1.1" | ||||
|     assert response.body == b"127.0.1.1" | ||||
|  | ||||
|     headers = {"X-Forwarded-For": "127.0.1.1"} | ||||
|     request, response = app.test_client.get("/", headers=headers) | ||||
|     assert request.remote_addr == "" | ||||
|     assert response.text == "" | ||||
|     assert response.body == b"" | ||||
|  | ||||
|     headers = {"Client-IP": "127.0.0.2", "Forwarded": "127.0.1.1"} | ||||
|     request, response = app.test_client.get("/", headers=headers) | ||||
|     assert request.remote_addr == "127.0.0.2" | ||||
|     assert response.text == "127.0.0.2" | ||||
|     assert response.body == b"127.0.0.2" | ||||
|  | ||||
|  | ||||
| @pytest.mark.asyncio | ||||
| @@ -840,17 +845,17 @@ async def test_remote_addr_custom_headers_asgi(app): | ||||
|     headers = {"X-Real-IP": "127.0.0.2", "Forwarded": "127.0.1.1"} | ||||
|     request, response = await app.asgi_client.get("/", headers=headers) | ||||
|     assert request.remote_addr == "127.0.1.1" | ||||
|     assert response.text == "127.0.1.1" | ||||
|     assert response.body == b"127.0.1.1" | ||||
|  | ||||
|     headers = {"X-Forwarded-For": "127.0.1.1"} | ||||
|     request, response = await app.asgi_client.get("/", headers=headers) | ||||
|     assert request.remote_addr == "" | ||||
|     assert response.text == "" | ||||
|     assert response.body == b"" | ||||
|  | ||||
|     headers = {"Client-IP": "127.0.0.2", "Forwarded": "127.0.1.1"} | ||||
|     request, response = await app.asgi_client.get("/", headers=headers) | ||||
|     assert request.remote_addr == "127.0.0.2" | ||||
|     assert response.text == "127.0.0.2" | ||||
|     assert response.body == b"127.0.0.2" | ||||
|  | ||||
|  | ||||
| def test_forwarded_scheme(app): | ||||
| @@ -894,7 +899,7 @@ async def test_match_info_asgi(app): | ||||
|     request, response = await app.asgi_client.get("/api/v1/user/sanic_user/") | ||||
|  | ||||
|     assert request.match_info == {"user_id": "sanic_user"} | ||||
|     assert json_loads(response.text) == {"user_id": "sanic_user"} | ||||
|     assert json_loads(response.body) == {"user_id": "sanic_user"} | ||||
|  | ||||
|  | ||||
| # ------------------------------------------------------------ # | ||||
| @@ -916,7 +921,7 @@ def test_post_json(app): | ||||
|  | ||||
|     assert request.json.get("test") == "OK" | ||||
|     assert request.json.get("test") == "OK"  # for request.parsed_json | ||||
|     assert response.text == "OK" | ||||
|     assert response.body == b"OK" | ||||
|  | ||||
|  | ||||
| @pytest.mark.asyncio | ||||
| @@ -934,7 +939,7 @@ async def test_post_json_asgi(app): | ||||
|  | ||||
|     assert request.json.get("test") == "OK" | ||||
|     assert request.json.get("test") == "OK"  # for request.parsed_json | ||||
|     assert response.text == "OK" | ||||
|     assert response.body == b"OK" | ||||
|  | ||||
|  | ||||
| def test_post_form_urlencoded(app): | ||||
| @@ -2136,7 +2141,7 @@ def test_safe_method_with_body_ignored(app): | ||||
|  | ||||
|     assert request.body == b"" | ||||
|     assert request.json == None | ||||
|     assert response.text == "OK" | ||||
|     assert response.body == b"OK" | ||||
|  | ||||
|  | ||||
| def test_safe_method_with_body(app): | ||||
| @@ -2153,4 +2158,4 @@ def test_safe_method_with_body(app): | ||||
|  | ||||
|     assert request.body == data.encode("utf-8") | ||||
|     assert request.json.get("test") == "OK" | ||||
|     assert response.text == "OK" | ||||
|     assert response.body == b"OK" | ||||
|   | ||||
| @@ -14,6 +14,7 @@ import pytest | ||||
| from aiofiles import os as async_os | ||||
| from sanic_testing.testing import HOST, PORT | ||||
|  | ||||
| from sanic import Sanic | ||||
| from sanic.response import ( | ||||
|     HTTPResponse, | ||||
|     StreamingHTTPResponse, | ||||
| @@ -51,16 +52,22 @@ async def sample_streaming_fn(response): | ||||
|     await response.write("bar") | ||||
|  | ||||
|  | ||||
| def test_method_not_allowed(app): | ||||
| def test_method_not_allowed(): | ||||
|     app = Sanic("app") | ||||
|  | ||||
|     @app.get("/") | ||||
|     async def test_get(request): | ||||
|         return response.json({"hello": "world"}) | ||||
|  | ||||
|     request, response = app.test_client.head("/") | ||||
|     assert response.headers["Allow"] == "GET" | ||||
|     assert set(response.headers["Allow"].split(", ")) == { | ||||
|         "GET", | ||||
|     } | ||||
|  | ||||
|     request, response = app.test_client.post("/") | ||||
|     assert response.headers["Allow"] == "GET" | ||||
|     assert set(response.headers["Allow"].split(", ")) == {"GET", "HEAD"} | ||||
|  | ||||
|     app.router.reset() | ||||
|  | ||||
|     @app.post("/") | ||||
|     async def test_post(request): | ||||
| @@ -68,12 +75,20 @@ def test_method_not_allowed(app): | ||||
|  | ||||
|     request, response = app.test_client.head("/") | ||||
|     assert response.status == 405 | ||||
|     assert set(response.headers["Allow"].split(", ")) == {"GET", "POST"} | ||||
|     assert set(response.headers["Allow"].split(", ")) == { | ||||
|         "GET", | ||||
|         "POST", | ||||
|         "HEAD", | ||||
|     } | ||||
|     assert response.headers["Content-Length"] == "0" | ||||
|  | ||||
|     request, response = app.test_client.patch("/") | ||||
|     assert response.status == 405 | ||||
|     assert set(response.headers["Allow"].split(", ")) == {"GET", "POST"} | ||||
|     assert set(response.headers["Allow"].split(", ")) == { | ||||
|         "GET", | ||||
|         "POST", | ||||
|         "HEAD", | ||||
|     } | ||||
|     assert response.headers["Content-Length"] == "0" | ||||
|  | ||||
|  | ||||
| @@ -237,7 +252,7 @@ def test_chunked_streaming_returns_correct_content(streaming_app): | ||||
| @pytest.mark.asyncio | ||||
| async def test_chunked_streaming_returns_correct_content_asgi(streaming_app): | ||||
|     request, response = await streaming_app.asgi_client.get("/") | ||||
|     assert response.text == "foo,bar" | ||||
|     assert response.body == b"foo,bar" | ||||
|  | ||||
|  | ||||
| def test_non_chunked_streaming_adds_correct_headers(non_chunked_streaming_app): | ||||
|   | ||||
| @@ -1,18 +1,180 @@ | ||||
| import asyncio | ||||
|  | ||||
| from unittest.mock import Mock | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| from sanic_routing.exceptions import ParameterNameConflicts, RouteExists | ||||
| from sanic_testing.testing import SanicTestClient | ||||
|  | ||||
| from sanic import Sanic | ||||
| from sanic import Blueprint, Sanic | ||||
| from sanic.constants import HTTP_METHODS | ||||
| from sanic.exceptions import NotFound | ||||
| from sanic.request import Request | ||||
| from sanic.response import json, text | ||||
| from sanic.router import ParameterNameConflicts, RouteDoesNotExist, RouteExists | ||||
|  | ||||
|  | ||||
| # ------------------------------------------------------------ # | ||||
| #  UTF-8 | ||||
| # ------------------------------------------------------------ # | ||||
| @pytest.mark.parametrize( | ||||
|     "path,headers,expected", | ||||
|     ( | ||||
|         # app base | ||||
|         (b"/", {}, 200), | ||||
|         (b"/", {"host": "maybe.com"}, 200), | ||||
|         (b"/host", {"host": "matching.com"}, 200), | ||||
|         (b"/host", {"host": "wrong.com"}, 404), | ||||
|         # app strict_slashes default | ||||
|         (b"/without", {}, 200), | ||||
|         (b"/without/", {}, 200), | ||||
|         (b"/with", {}, 200), | ||||
|         (b"/with/", {}, 200), | ||||
|         # app strict_slashes off - expressly | ||||
|         (b"/expwithout", {}, 200), | ||||
|         (b"/expwithout/", {}, 200), | ||||
|         (b"/expwith", {}, 200), | ||||
|         (b"/expwith/", {}, 200), | ||||
|         # app strict_slashes on | ||||
|         (b"/without/strict", {}, 200), | ||||
|         (b"/without/strict/", {}, 404), | ||||
|         (b"/with/strict", {}, 404), | ||||
|         (b"/with/strict/", {}, 200), | ||||
|         # bp1 base | ||||
|         (b"/bp1", {}, 200), | ||||
|         (b"/bp1", {"host": "maybe.com"}, 200), | ||||
|         (b"/bp1/host", {"host": "matching.com"}, 200),  # BROKEN ON MASTER | ||||
|         (b"/bp1/host", {"host": "wrong.com"}, 404), | ||||
|         # bp1 strict_slashes default | ||||
|         (b"/bp1/without", {}, 200), | ||||
|         (b"/bp1/without/", {}, 200), | ||||
|         (b"/bp1/with", {}, 200), | ||||
|         (b"/bp1/with/", {}, 200), | ||||
|         # bp1 strict_slashes off - expressly | ||||
|         (b"/bp1/expwithout", {}, 200), | ||||
|         (b"/bp1/expwithout/", {}, 200), | ||||
|         (b"/bp1/expwith", {}, 200), | ||||
|         (b"/bp1/expwith/", {}, 200), | ||||
|         # bp1 strict_slashes on | ||||
|         (b"/bp1/without/strict", {}, 200), | ||||
|         (b"/bp1/without/strict/", {}, 404), | ||||
|         (b"/bp1/with/strict", {}, 404), | ||||
|         (b"/bp1/with/strict/", {}, 200), | ||||
|         # bp2 base | ||||
|         (b"/bp2/", {}, 200), | ||||
|         (b"/bp2/", {"host": "maybe.com"}, 200), | ||||
|         (b"/bp2/host", {"host": "matching.com"}, 200),  # BROKEN ON MASTER | ||||
|         (b"/bp2/host", {"host": "wrong.com"}, 404), | ||||
|         # bp2 strict_slashes default | ||||
|         (b"/bp2/without", {}, 200), | ||||
|         (b"/bp2/without/", {}, 404), | ||||
|         (b"/bp2/with", {}, 404), | ||||
|         (b"/bp2/with/", {}, 200), | ||||
|         # # bp2 strict_slashes off - expressly | ||||
|         (b"/bp2/expwithout", {}, 200), | ||||
|         (b"/bp2/expwithout/", {}, 200), | ||||
|         (b"/bp2/expwith", {}, 200), | ||||
|         (b"/bp2/expwith/", {}, 200), | ||||
|         # # bp2 strict_slashes on | ||||
|         (b"/bp2/without/strict", {}, 200), | ||||
|         (b"/bp2/without/strict/", {}, 404), | ||||
|         (b"/bp2/with/strict", {}, 404), | ||||
|         (b"/bp2/with/strict/", {}, 200), | ||||
|         # bp3 base | ||||
|         (b"/bp3", {}, 200), | ||||
|         (b"/bp3", {"host": "maybe.com"}, 200), | ||||
|         (b"/bp3/host", {"host": "matching.com"}, 200),  # BROKEN ON MASTER | ||||
|         (b"/bp3/host", {"host": "wrong.com"}, 404), | ||||
|         # bp3 strict_slashes default | ||||
|         (b"/bp3/without", {}, 200), | ||||
|         (b"/bp3/without/", {}, 200), | ||||
|         (b"/bp3/with", {}, 200), | ||||
|         (b"/bp3/with/", {}, 200), | ||||
|         # bp3 strict_slashes off - expressly | ||||
|         (b"/bp3/expwithout", {}, 200), | ||||
|         (b"/bp3/expwithout/", {}, 200), | ||||
|         (b"/bp3/expwith", {}, 200), | ||||
|         (b"/bp3/expwith/", {}, 200), | ||||
|         # bp3 strict_slashes on | ||||
|         (b"/bp3/without/strict", {}, 200), | ||||
|         (b"/bp3/without/strict/", {}, 404), | ||||
|         (b"/bp3/with/strict", {}, 404), | ||||
|         (b"/bp3/with/strict/", {}, 200), | ||||
|         # bp4 base | ||||
|         (b"/bp4", {}, 404), | ||||
|         (b"/bp4", {"host": "maybe.com"}, 200), | ||||
|         (b"/bp4/host", {"host": "matching.com"}, 200),  # BROKEN ON MASTER | ||||
|         (b"/bp4/host", {"host": "wrong.com"}, 404), | ||||
|         # bp4 strict_slashes default | ||||
|         (b"/bp4/without", {}, 404), | ||||
|         (b"/bp4/without/", {}, 404), | ||||
|         (b"/bp4/with", {}, 404), | ||||
|         (b"/bp4/with/", {}, 404), | ||||
|         # bp4 strict_slashes off - expressly | ||||
|         (b"/bp4/expwithout", {}, 404), | ||||
|         (b"/bp4/expwithout/", {}, 404), | ||||
|         (b"/bp4/expwith", {}, 404), | ||||
|         (b"/bp4/expwith/", {}, 404), | ||||
|         # bp4 strict_slashes on | ||||
|         (b"/bp4/without/strict", {}, 404), | ||||
|         (b"/bp4/without/strict/", {}, 404), | ||||
|         (b"/bp4/with/strict", {}, 404), | ||||
|         (b"/bp4/with/strict/", {}, 404), | ||||
|     ), | ||||
| ) | ||||
| def test_matching(path, headers, expected): | ||||
|     app = Sanic("dev") | ||||
|     bp1 = Blueprint("bp1", url_prefix="/bp1") | ||||
|     bp2 = Blueprint("bp2", url_prefix="/bp2", strict_slashes=True) | ||||
|     bp3 = Blueprint("bp3", url_prefix="/bp3", strict_slashes=False) | ||||
|     bp4 = Blueprint("bp4", url_prefix="/bp4", host="maybe.com") | ||||
|  | ||||
|     def handler(request): | ||||
|         return text("Hello!") | ||||
|  | ||||
|     defs = ( | ||||
|         ("/", None, None), | ||||
|         ("/host", None, "matching.com"), | ||||
|         ("/without", None, None), | ||||
|         ("/with/", None, None), | ||||
|         ("/expwithout", False, None), | ||||
|         ("/expwith/", False, None), | ||||
|         ("/without/strict", True, None), | ||||
|         ("/with/strict/", True, None), | ||||
|     ) | ||||
|     for uri, strict_slashes, host in defs: | ||||
|         params = {"uri": uri} | ||||
|         if strict_slashes is not None: | ||||
|             params["strict_slashes"] = strict_slashes | ||||
|         if host is not None: | ||||
|             params["host"] = host | ||||
|         app.route(**params)(handler) | ||||
|         bp1.route(**params)(handler) | ||||
|         bp2.route(**params)(handler) | ||||
|         bp3.route(**params)(handler) | ||||
|         bp4.route(**params)(handler) | ||||
|  | ||||
|     app.blueprint(bp1) | ||||
|     app.blueprint(bp2) | ||||
|     app.blueprint(bp3) | ||||
|     app.blueprint(bp4) | ||||
|  | ||||
|     app.router.finalize() | ||||
|  | ||||
|     request = Request(path, headers, None, "GET", None, app) | ||||
|  | ||||
|     try: | ||||
|         app.router.get(request=request) | ||||
|     except NotFound: | ||||
|         response = 404 | ||||
|     except Exception: | ||||
|         response = 500 | ||||
|     else: | ||||
|         response = 200 | ||||
|  | ||||
|     assert response == expected | ||||
|  | ||||
|  | ||||
| # # ------------------------------------------------------------ # | ||||
| # #  UTF-8 | ||||
| # # ------------------------------------------------------------ # | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("method", HTTP_METHODS) | ||||
| @@ -164,7 +326,6 @@ def test_route_optional_slash(app): | ||||
|  | ||||
| def test_route_strict_slashes_set_to_false_and_host_is_a_list(app): | ||||
|     # Part of regression test for issue #1120 | ||||
|  | ||||
|     test_client = SanicTestClient(app, port=42101) | ||||
|     site1 = f"127.0.0.1:{test_client.port}" | ||||
|  | ||||
| @@ -176,6 +337,8 @@ def test_route_strict_slashes_set_to_false_and_host_is_a_list(app): | ||||
|     request, response = test_client.get("http://" + site1 + "/get") | ||||
|     assert response.text == "OK" | ||||
|  | ||||
|     app.router.finalized = False | ||||
|  | ||||
|     @app.post("/post", host=[site1, "site2.com"], strict_slashes=False) | ||||
|     def post_handler(request): | ||||
|         return text("OK") | ||||
| @@ -183,6 +346,8 @@ def test_route_strict_slashes_set_to_false_and_host_is_a_list(app): | ||||
|     request, response = test_client.post("http://" + site1 + "/post") | ||||
|     assert response.text == "OK" | ||||
|  | ||||
|     app.router.finalized = False | ||||
|  | ||||
|     @app.put("/put", host=[site1, "site2.com"], strict_slashes=False) | ||||
|     def put_handler(request): | ||||
|         return text("OK") | ||||
| @@ -190,6 +355,8 @@ def test_route_strict_slashes_set_to_false_and_host_is_a_list(app): | ||||
|     request, response = test_client.put("http://" + site1 + "/put") | ||||
|     assert response.text == "OK" | ||||
|  | ||||
|     app.router.finalized = False | ||||
|  | ||||
|     @app.delete("/delete", host=[site1, "site2.com"], strict_slashes=False) | ||||
|     def delete_handler(request): | ||||
|         return text("OK") | ||||
| @@ -294,6 +461,8 @@ def test_dynamic_route(app): | ||||
|         results.append(name) | ||||
|         return text("OK") | ||||
|  | ||||
|     app.router.finalize(False) | ||||
|  | ||||
|     request, response = app.test_client.get("/folder/test123") | ||||
|  | ||||
|     assert response.text == "OK" | ||||
| @@ -368,6 +537,9 @@ def test_dynamic_route_regex(app): | ||||
|     async def handler(request, folder_id): | ||||
|         return text("OK") | ||||
|  | ||||
|     app.router.finalize() | ||||
|     print(app.router.find_route_src) | ||||
|  | ||||
|     request, response = app.test_client.get("/folder/test") | ||||
|     assert response.status == 200 | ||||
|  | ||||
| @@ -415,6 +587,8 @@ def test_dynamic_route_path(app): | ||||
|     request, response = app.test_client.get("/info") | ||||
|     assert response.status == 404 | ||||
|  | ||||
|     app.router.reset() | ||||
|  | ||||
|     @app.route("/<path:path>") | ||||
|     async def handler1(request, path): | ||||
|         return text("OK") | ||||
| @@ -774,7 +948,7 @@ def test_removing_slash(app): | ||||
|     def post(_): | ||||
|         pass | ||||
|  | ||||
|     assert len(app.router.routes_all.keys()) == 2 | ||||
|     assert len(app.router.routes_all.keys()) == 1 | ||||
|  | ||||
|  | ||||
| def test_overload_routes(app): | ||||
| @@ -798,6 +972,7 @@ def test_overload_routes(app): | ||||
|     request, response = app.test_client.delete("/overload") | ||||
|     assert response.status == 405 | ||||
|  | ||||
|     app.router.reset() | ||||
|     with pytest.raises(RouteExists): | ||||
|  | ||||
|         @app.route("/overload", methods=["PUT", "DELETE"]) | ||||
| @@ -810,11 +985,18 @@ def test_unmergeable_overload_routes(app): | ||||
|     async def handler1(request): | ||||
|         return text("OK1") | ||||
|  | ||||
|     with pytest.raises(RouteExists): | ||||
|     @app.route("/overload_whole", methods=["POST", "PUT"]) | ||||
|     async def handler2(request): | ||||
|         return text("OK1") | ||||
|  | ||||
|         @app.route("/overload_whole", methods=["POST", "PUT"]) | ||||
|         async def handler2(request): | ||||
|             return text("Duplicated") | ||||
|     assert ( | ||||
|         len( | ||||
|             dict(list(app.router.static_routes.values())[0].handlers)[ | ||||
|                 "overload_whole" | ||||
|             ] | ||||
|         ) | ||||
|         == 3 | ||||
|     ) | ||||
|  | ||||
|     request, response = app.test_client.get("/overload_whole") | ||||
|     assert response.text == "OK1" | ||||
| @@ -822,6 +1004,11 @@ def test_unmergeable_overload_routes(app): | ||||
|     request, response = app.test_client.post("/overload_whole") | ||||
|     assert response.text == "OK1" | ||||
|  | ||||
|     request, response = app.test_client.put("/overload_whole") | ||||
|     assert response.text == "OK1" | ||||
|  | ||||
|     app.router.reset() | ||||
|  | ||||
|     @app.route("/overload_part", methods=["GET"]) | ||||
|     async def handler3(request): | ||||
|         return text("OK1") | ||||
| @@ -847,7 +1034,9 @@ def test_unicode_routes(app): | ||||
|     request, response = app.test_client.get("/你好") | ||||
|     assert response.text == "OK1" | ||||
|  | ||||
|     @app.route("/overload/<param>", methods=["GET"]) | ||||
|     app.router.reset() | ||||
|  | ||||
|     @app.route("/overload/<param>", methods=["GET"], unquote=True) | ||||
|     async def handler2(request, param): | ||||
|         return text("OK2 " + param) | ||||
|  | ||||
| @@ -865,20 +1054,38 @@ def test_uri_with_different_method_and_different_params(app): | ||||
|         return json({"action": action}) | ||||
|  | ||||
|     request, response = app.test_client.get("/ads/1234") | ||||
|     assert response.status == 200 | ||||
|     assert response.json == {"ad_id": "1234"} | ||||
|     assert response.status == 405 | ||||
|  | ||||
|     request, response = app.test_client.post("/ads/post") | ||||
|     assert response.status == 200 | ||||
|     assert response.json == {"action": "post"} | ||||
|  | ||||
|  | ||||
| def test_route_raise_ParameterNameConflicts(app): | ||||
|     with pytest.raises(ParameterNameConflicts): | ||||
| def test_uri_with_different_method_and_same_params(app): | ||||
|     @app.route("/ads/<ad_id>", methods=["GET"]) | ||||
|     async def ad_get(request, ad_id): | ||||
|         return json({"ad_id": ad_id}) | ||||
|  | ||||
|         @app.get("/api/v1/<user>/<user>/") | ||||
|         def handler(request, user): | ||||
|             return text("OK") | ||||
|     @app.route("/ads/<ad_id>", methods=["POST"]) | ||||
|     async def ad_post(request, ad_id): | ||||
|         return json({"ad_id": ad_id}) | ||||
|  | ||||
|     request, response = app.test_client.get("/ads/1234") | ||||
|     assert response.status == 200 | ||||
|     assert response.json == {"ad_id": "1234"} | ||||
|  | ||||
|     request, response = app.test_client.post("/ads/post") | ||||
|     assert response.status == 200 | ||||
|     assert response.json == {"ad_id": "post"} | ||||
|  | ||||
|  | ||||
| def test_route_raise_ParameterNameConflicts(app): | ||||
|     @app.get("/api/v1/<user>/<user>/") | ||||
|     def handler(request, user): | ||||
|         return text("OK") | ||||
|  | ||||
|     with pytest.raises(ParameterNameConflicts): | ||||
|         app.router.finalize() | ||||
|  | ||||
|  | ||||
| def test_route_invalid_host(app): | ||||
|   | ||||
| @@ -106,6 +106,7 @@ def test_static_file_bytes(app, static_file_directory, file_name): | ||||
|     [dict(), list(), object()], | ||||
| ) | ||||
| def test_static_file_invalid_path(app, static_file_directory, file_name): | ||||
|     app.route("/")(lambda x: x) | ||||
|     with pytest.raises(ValueError): | ||||
|         app.static("/testing.file", file_name) | ||||
|     request, response = app.test_client.get("/testing.file") | ||||
|   | ||||
| @@ -7,6 +7,7 @@ import pytest as pytest | ||||
| from sanic_testing.testing import HOST as test_host | ||||
| from sanic_testing.testing import PORT as test_port | ||||
|  | ||||
| from sanic import Sanic | ||||
| from sanic.blueprints import Blueprint | ||||
| from sanic.exceptions import URLBuildError | ||||
| from sanic.response import text | ||||
| @@ -98,36 +99,36 @@ def test_url_for_with_server_name(app): | ||||
|     assert response.text == "this should pass" | ||||
|  | ||||
|  | ||||
| def test_fails_if_endpoint_not_found(app): | ||||
| def test_fails_if_endpoint_not_found(): | ||||
|     app = Sanic("app") | ||||
|  | ||||
|     @app.route("/fail") | ||||
|     def fail(request): | ||||
|         return text("this should fail") | ||||
|  | ||||
|     with pytest.raises(URLBuildError) as e: | ||||
|         app.url_for("passes") | ||||
|  | ||||
|     assert str(e.value) == "Endpoint with name `passes` was not found" | ||||
|         e.match("Endpoint with name `app.passes` was not found") | ||||
|  | ||||
|  | ||||
| def test_fails_url_build_if_param_not_passed(app): | ||||
|     url = "/" | ||||
|  | ||||
|     for letter in string.ascii_letters: | ||||
|     for letter in string.ascii_lowercase: | ||||
|         url += f"<{letter}>/" | ||||
|  | ||||
|     @app.route(url) | ||||
|     def fail(request): | ||||
|         return text("this should fail") | ||||
|  | ||||
|     fail_args = list(string.ascii_letters) | ||||
|     fail_args = list(string.ascii_lowercase) | ||||
|     fail_args.pop() | ||||
|  | ||||
|     fail_kwargs = {l: l for l in fail_args} | ||||
|  | ||||
|     with pytest.raises(URLBuildError) as e: | ||||
|         app.url_for("fail", **fail_kwargs) | ||||
|  | ||||
|     assert "Required parameter `Z` was not passed to url_for" in str(e.value) | ||||
|         assert e.match("Required parameter `z` was not passed to url_for") | ||||
|  | ||||
|  | ||||
| def test_fails_url_build_if_params_not_passed(app): | ||||
| @@ -137,8 +138,7 @@ def test_fails_url_build_if_params_not_passed(app): | ||||
|  | ||||
|     with pytest.raises(ValueError) as e: | ||||
|         app.url_for("fail", _scheme="http") | ||||
|  | ||||
|     assert str(e.value) == "When specifying _scheme, _external must be True" | ||||
|         assert e.match("When specifying _scheme, _external must be True") | ||||
|  | ||||
|  | ||||
| COMPLEX_PARAM_URL = ( | ||||
| @@ -168,7 +168,7 @@ def test_fails_with_int_message(app): | ||||
|  | ||||
|     expected_error = ( | ||||
|         r'Value "not_int" for parameter `foo` ' | ||||
|         r"does not match pattern for type `int`: -?\d+" | ||||
|         r"does not match pattern for type `int`: ^-?\d+" | ||||
|     ) | ||||
|     assert str(e.value) == expected_error | ||||
|  | ||||
| @@ -199,13 +199,10 @@ def test_fails_with_two_letter_string_message(app): | ||||
|  | ||||
|     with pytest.raises(URLBuildError) as e: | ||||
|         app.url_for("fail", **failing_kwargs) | ||||
|  | ||||
|     expected_error = ( | ||||
|         'Value "foobar" for parameter `two_letter_string` ' | ||||
|         "does not satisfy pattern [A-z]{2}" | ||||
|     ) | ||||
|  | ||||
|     assert str(e.value) == expected_error | ||||
|         e.match( | ||||
|             'Value "foobar" for parameter `two_letter_string` ' | ||||
|             "does not satisfy pattern ^[A-z]{2}$" | ||||
|         ) | ||||
|  | ||||
|  | ||||
| def test_fails_with_number_message(app): | ||||
| @@ -218,13 +215,10 @@ def test_fails_with_number_message(app): | ||||
|  | ||||
|     with pytest.raises(URLBuildError) as e: | ||||
|         app.url_for("fail", **failing_kwargs) | ||||
|  | ||||
|     expected_error = ( | ||||
|         'Value "foo" for parameter `some_number` ' | ||||
|         r"does not match pattern for type `float`: -?(?:\d+(?:\.\d*)?|\.\d+)" | ||||
|     ) | ||||
|  | ||||
|     assert str(e.value) == expected_error | ||||
|         e.match( | ||||
|             'Value "foo" for parameter `some_number` ' | ||||
|             r"does not match pattern for type `float`: ^-?(?:\d+(?:\.\d*)?|\.\d+)$" | ||||
|         ) | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("number", [3, -3, 13.123, -13.123]) | ||||
| @@ -259,7 +253,8 @@ def test_adds_other_supplied_values_as_query_string(app): | ||||
|  | ||||
|  | ||||
| @pytest.fixture | ||||
| def blueprint_app(app): | ||||
| def blueprint_app(): | ||||
|     app = Sanic("app") | ||||
|  | ||||
|     first_print = Blueprint("first", url_prefix="/first") | ||||
|     second_print = Blueprint("second", url_prefix="/second") | ||||
| @@ -273,11 +268,11 @@ def blueprint_app(app): | ||||
|         return text(f"foo from first : {param}") | ||||
|  | ||||
|     @second_print.route("/foo")  # noqa | ||||
|     def foo(request): | ||||
|     def bar(request): | ||||
|         return text("foo from second") | ||||
|  | ||||
|     @second_print.route("/foo/<param>")  # noqa | ||||
|     def foo_with_param(request, param): | ||||
|     def bar_with_param(request, param): | ||||
|         return text(f"foo from second : {param}") | ||||
|  | ||||
|     app.blueprint(first_print) | ||||
| @@ -290,7 +285,7 @@ def test_blueprints_are_named_correctly(blueprint_app): | ||||
|     first_url = blueprint_app.url_for("first.foo") | ||||
|     assert first_url == "/first/foo" | ||||
|  | ||||
|     second_url = blueprint_app.url_for("second.foo") | ||||
|     second_url = blueprint_app.url_for("second.bar") | ||||
|     assert second_url == "/second/foo" | ||||
|  | ||||
|  | ||||
| @@ -298,7 +293,7 @@ def test_blueprints_work_with_params(blueprint_app): | ||||
|     first_url = blueprint_app.url_for("first.foo_with_param", param="bar") | ||||
|     assert first_url == "/first/foo/bar" | ||||
|  | ||||
|     second_url = blueprint_app.url_for("second.foo_with_param", param="bar") | ||||
|     second_url = blueprint_app.url_for("second.bar_with_param", param="bar") | ||||
|     assert second_url == "/second/foo/bar" | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -1,18 +1,18 @@ | ||||
| import asyncio | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| from sanic_testing.testing import SanicTestClient | ||||
|  | ||||
| from sanic.blueprints import Blueprint | ||||
|  | ||||
|  | ||||
| def test_routes_with_host(app): | ||||
|     @app.route("/") | ||||
|     @app.route("/", name="hostindex", host="example.com") | ||||
|     @app.route("/path", name="hostpath", host="path.example.com") | ||||
|     def index(request): | ||||
|         pass | ||||
|  | ||||
|     assert app.url_for("index") == "/" | ||||
|     assert app.url_for("hostindex") == "/" | ||||
|     assert app.url_for("hostpath") == "/path" | ||||
|     assert app.url_for("hostindex", _external=True) == "http://example.com/" | ||||
| @@ -22,6 +22,27 @@ def test_routes_with_host(app): | ||||
|     ) | ||||
|  | ||||
|  | ||||
| def test_routes_with_multiple_hosts(app): | ||||
|     @app.route("/", name="hostindex", host=["example.com", "path.example.com"]) | ||||
|     def index(request): | ||||
|         pass | ||||
|  | ||||
|     assert app.url_for("hostindex") == "/" | ||||
|     assert ( | ||||
|         app.url_for("hostindex", _host="example.com") == "http://example.com/" | ||||
|     ) | ||||
|  | ||||
|     with pytest.raises(ValueError) as e: | ||||
|         assert app.url_for("hostindex", _external=True) | ||||
|     assert str(e.value).startswith("Host is ambiguous") | ||||
|  | ||||
|     with pytest.raises(ValueError) as e: | ||||
|         assert app.url_for("hostindex", _host="unknown.com") | ||||
|     assert str(e.value).startswith( | ||||
|         "Requested host (unknown.com) is not available for this route" | ||||
|     ) | ||||
|  | ||||
|  | ||||
| def test_websocket_bp_route_name(app): | ||||
|     """Tests that blueprint websocket route is named.""" | ||||
|     event = asyncio.Event() | ||||
| @@ -63,3 +84,7 @@ def test_websocket_bp_route_name(app): | ||||
|  | ||||
|     uri = app.url_for("test_bp.foobar_3") | ||||
|     assert uri == "/bp/route3" | ||||
|  | ||||
|  | ||||
| # TODO: add test with a route with multiple hosts | ||||
| # TODO: add test with a route with _host in url_for | ||||
|   | ||||
| @@ -3,6 +3,7 @@ import os | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| from sanic import Sanic | ||||
| from sanic.blueprints import Blueprint | ||||
|  | ||||
|  | ||||
| @@ -26,9 +27,15 @@ def get_file_content(static_file_directory, file_name): | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize( | ||||
|     "file_name", ["test.file", "decode me.txt", "python.png"] | ||||
|     "file_name", | ||||
|     [ | ||||
|         "test.file", | ||||
|         "decode me.txt", | ||||
|         "python.png", | ||||
|     ], | ||||
| ) | ||||
| def test_static_file(app, static_file_directory, file_name): | ||||
| def test_static_file(static_file_directory, file_name): | ||||
|     app = Sanic("qq") | ||||
|     app.static( | ||||
|         "/testing.file", get_file_path(static_file_directory, file_name) | ||||
|     ) | ||||
| @@ -38,6 +45,8 @@ def test_static_file(app, static_file_directory, file_name): | ||||
|         name="testing_file", | ||||
|     ) | ||||
|  | ||||
|     app.router.finalize() | ||||
|  | ||||
|     uri = app.url_for("static") | ||||
|     uri2 = app.url_for("static", filename="any") | ||||
|     uri3 = app.url_for("static", name="static", filename="any") | ||||
| @@ -46,10 +55,14 @@ def test_static_file(app, static_file_directory, file_name): | ||||
|     assert uri == uri2 | ||||
|     assert uri2 == uri3 | ||||
|  | ||||
|     app.router.reset() | ||||
|  | ||||
|     request, response = app.test_client.get(uri) | ||||
|     assert response.status == 200 | ||||
|     assert response.body == get_file_content(static_file_directory, file_name) | ||||
|  | ||||
|     app.router.reset() | ||||
|  | ||||
|     bp = Blueprint("test_bp_static", url_prefix="/bp") | ||||
|  | ||||
|     bp.static("/testing.file", get_file_path(static_file_directory, file_name)) | ||||
| @@ -61,19 +74,14 @@ def test_static_file(app, static_file_directory, file_name): | ||||
|  | ||||
|     app.blueprint(bp) | ||||
|  | ||||
|     uri = app.url_for("static", name="test_bp_static.static") | ||||
|     uri2 = app.url_for("static", name="test_bp_static.static", filename="any") | ||||
|     uri3 = app.url_for("test_bp_static.static") | ||||
|     uri4 = app.url_for("test_bp_static.static", name="any") | ||||
|     uri5 = app.url_for("test_bp_static.static", filename="any") | ||||
|     uri6 = app.url_for("test_bp_static.static", name="any", filename="any") | ||||
|     uris = [ | ||||
|         app.url_for("static", name="test_bp_static.static"), | ||||
|         app.url_for("static", name="test_bp_static.static", filename="any"), | ||||
|         app.url_for("test_bp_static.static"), | ||||
|         app.url_for("test_bp_static.static", filename="any"), | ||||
|     ] | ||||
|  | ||||
|     assert uri == "/bp/testing.file" | ||||
|     assert uri == uri2 | ||||
|     assert uri2 == uri3 | ||||
|     assert uri3 == uri4 | ||||
|     assert uri4 == uri5 | ||||
|     assert uri5 == uri6 | ||||
|     assert all(uri == "/bp/testing.file" for uri in uris) | ||||
|  | ||||
|     request, response = app.test_client.get(uri) | ||||
|     assert response.status == 200 | ||||
| @@ -112,7 +120,9 @@ def test_static_file(app, static_file_directory, file_name): | ||||
|  | ||||
| @pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"]) | ||||
| @pytest.mark.parametrize("base_uri", ["/static", "", "/dir"]) | ||||
| def test_static_directory(app, file_name, base_uri, static_file_directory): | ||||
| def test_static_directory(file_name, base_uri, static_file_directory): | ||||
|     app = Sanic("base") | ||||
|  | ||||
|     app.static(base_uri, static_file_directory) | ||||
|     base_uri2 = base_uri + "/2" | ||||
|     app.static(base_uri2, static_file_directory, name="uploads") | ||||
| @@ -141,6 +151,8 @@ def test_static_directory(app, file_name, base_uri, static_file_directory): | ||||
|  | ||||
|     bp.static(base_uri, static_file_directory) | ||||
|     bp.static(base_uri2, static_file_directory, name="uploads") | ||||
|  | ||||
|     app.router.reset() | ||||
|     app.blueprint(bp) | ||||
|  | ||||
|     uri = app.url_for( | ||||
| @@ -169,7 +181,8 @@ def test_static_directory(app, file_name, base_uri, static_file_directory): | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"]) | ||||
| def test_static_head_request(app, file_name, static_file_directory): | ||||
| def test_static_head_request(file_name, static_file_directory): | ||||
|     app = Sanic("base") | ||||
|     app.static( | ||||
|         "/testing.file", | ||||
|         get_file_path(static_file_directory, file_name), | ||||
| @@ -214,7 +227,8 @@ def test_static_head_request(app, file_name, static_file_directory): | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"]) | ||||
| def test_static_content_range_correct(app, file_name, static_file_directory): | ||||
| def test_static_content_range_correct(file_name, static_file_directory): | ||||
|     app = Sanic("base") | ||||
|     app.static( | ||||
|         "/testing.file", | ||||
|         get_file_path(static_file_directory, file_name), | ||||
| @@ -252,11 +266,6 @@ def test_static_content_range_correct(app, file_name, static_file_directory): | ||||
|         "static", name="test_bp_static.static", filename="any" | ||||
|     ) | ||||
|     assert uri == app.url_for("test_bp_static.static") | ||||
|     assert uri == app.url_for("test_bp_static.static", name="any") | ||||
|     assert uri == app.url_for("test_bp_static.static", filename="any") | ||||
|     assert uri == app.url_for( | ||||
|         "test_bp_static.static", name="any", filename="any" | ||||
|     ) | ||||
|  | ||||
|     request, response = app.test_client.get(uri, headers=headers) | ||||
|     assert response.status == 206 | ||||
| @@ -270,7 +279,8 @@ def test_static_content_range_correct(app, file_name, static_file_directory): | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"]) | ||||
| def test_static_content_range_front(app, file_name, static_file_directory): | ||||
| def test_static_content_range_front(file_name, static_file_directory): | ||||
|     app = Sanic("base") | ||||
|     app.static( | ||||
|         "/testing.file", | ||||
|         get_file_path(static_file_directory, file_name), | ||||
| @@ -308,11 +318,7 @@ def test_static_content_range_front(app, file_name, static_file_directory): | ||||
|         "static", name="test_bp_static.static", filename="any" | ||||
|     ) | ||||
|     assert uri == app.url_for("test_bp_static.static") | ||||
|     assert uri == app.url_for("test_bp_static.static", name="any") | ||||
|     assert uri == app.url_for("test_bp_static.static", filename="any") | ||||
|     assert uri == app.url_for( | ||||
|         "test_bp_static.static", name="any", filename="any" | ||||
|     ) | ||||
|  | ||||
|     request, response = app.test_client.get(uri, headers=headers) | ||||
|     assert response.status == 206 | ||||
| @@ -326,7 +332,8 @@ def test_static_content_range_front(app, file_name, static_file_directory): | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"]) | ||||
| def test_static_content_range_back(app, file_name, static_file_directory): | ||||
| def test_static_content_range_back(file_name, static_file_directory): | ||||
|     app = Sanic("base") | ||||
|     app.static( | ||||
|         "/testing.file", | ||||
|         get_file_path(static_file_directory, file_name), | ||||
| @@ -364,11 +371,7 @@ def test_static_content_range_back(app, file_name, static_file_directory): | ||||
|         "static", name="test_bp_static.static", filename="any" | ||||
|     ) | ||||
|     assert uri == app.url_for("test_bp_static.static") | ||||
|     assert uri == app.url_for("test_bp_static.static", name="any") | ||||
|     assert uri == app.url_for("test_bp_static.static", filename="any") | ||||
|     assert uri == app.url_for( | ||||
|         "test_bp_static.static", name="any", filename="any" | ||||
|     ) | ||||
|  | ||||
|     request, response = app.test_client.get(uri, headers=headers) | ||||
|     assert response.status == 206 | ||||
| @@ -382,7 +385,8 @@ def test_static_content_range_back(app, file_name, static_file_directory): | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"]) | ||||
| def test_static_content_range_empty(app, file_name, static_file_directory): | ||||
| def test_static_content_range_empty(file_name, static_file_directory): | ||||
|     app = Sanic("base") | ||||
|     app.static( | ||||
|         "/testing.file", | ||||
|         get_file_path(static_file_directory, file_name), | ||||
| @@ -420,11 +424,7 @@ def test_static_content_range_empty(app, file_name, static_file_directory): | ||||
|         "static", name="test_bp_static.static", filename="any" | ||||
|     ) | ||||
|     assert uri == app.url_for("test_bp_static.static") | ||||
|     assert uri == app.url_for("test_bp_static.static", name="any") | ||||
|     assert uri == app.url_for("test_bp_static.static", filename="any") | ||||
|     assert uri == app.url_for( | ||||
|         "test_bp_static.static", name="any", filename="any" | ||||
|     ) | ||||
|  | ||||
|     request, response = app.test_client.get(uri) | ||||
|     assert response.status == 200 | ||||
| @@ -440,6 +440,7 @@ def test_static_content_range_empty(app, file_name, static_file_directory): | ||||
|  | ||||
| @pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"]) | ||||
| def test_static_content_range_error(app, file_name, static_file_directory): | ||||
|     app = Sanic("base") | ||||
|     app.static( | ||||
|         "/testing.file", | ||||
|         get_file_path(static_file_directory, file_name), | ||||
| @@ -475,11 +476,7 @@ def test_static_content_range_error(app, file_name, static_file_directory): | ||||
|         "static", name="test_bp_static.static", filename="any" | ||||
|     ) | ||||
|     assert uri == app.url_for("test_bp_static.static") | ||||
|     assert uri == app.url_for("test_bp_static.static", name="any") | ||||
|     assert uri == app.url_for("test_bp_static.static", filename="any") | ||||
|     assert uri == app.url_for( | ||||
|         "test_bp_static.static", name="any", filename="any" | ||||
|     ) | ||||
|  | ||||
|     request, response = app.test_client.get(uri, headers=headers) | ||||
|     assert response.status == 416 | ||||
|   | ||||
| @@ -1,7 +1,14 @@ | ||||
| import pytest | ||||
|  | ||||
| from sanic_routing.exceptions import RouteExists | ||||
|  | ||||
| from sanic import Sanic | ||||
| from sanic.response import text | ||||
|  | ||||
|  | ||||
| def test_vhosts(app): | ||||
| def test_vhosts(): | ||||
|     app = Sanic("app") | ||||
|  | ||||
|     @app.route("/", host="example.com") | ||||
|     async def handler1(request): | ||||
|         return text("You're at example.com!") | ||||
| @@ -38,13 +45,12 @@ def test_vhosts_with_defaults(app): | ||||
|     async def handler1(request): | ||||
|         return text("Hello, world!") | ||||
|  | ||||
|     @app.route("/") | ||||
|     async def handler2(request): | ||||
|         return text("default") | ||||
|     with pytest.raises(RouteExists): | ||||
|  | ||||
|         @app.route("/") | ||||
|         async def handler2(request): | ||||
|             return text("default") | ||||
|  | ||||
|     headers = {"Host": "hello.com"} | ||||
|     request, response = app.test_client.get("/", headers=headers) | ||||
|     assert response.text == "Hello, world!" | ||||
|  | ||||
|     request, response = app.test_client.get("/") | ||||
|     assert response.text == "default" | ||||
|   | ||||
| @@ -45,9 +45,9 @@ def test_unexisting_methods(app): | ||||
|  | ||||
|     app.add_route(DummyView.as_view(), "/") | ||||
|     request, response = app.test_client.get("/") | ||||
|     assert response.text == "I am get method" | ||||
|     assert response.body == b"I am get method" | ||||
|     request, response = app.test_client.post("/") | ||||
|     assert "Method POST not allowed for URL /" in response.text | ||||
|     assert b"Method POST not allowed for URL /" in response.body | ||||
|  | ||||
|  | ||||
| def test_argument_methods(app): | ||||
| @@ -215,17 +215,18 @@ def test_composition_view_runs_methods_as_expected(app, method): | ||||
|  | ||||
|     if method in ["GET", "POST", "PUT"]: | ||||
|         request, response = getattr(app.test_client, method.lower())("/") | ||||
|         assert response.status == 200 | ||||
|         assert response.text == "first method" | ||||
|  | ||||
|         response = view(request) | ||||
|         assert response.body.decode() == "first method" | ||||
|         # response = view(request) | ||||
|         # assert response.body.decode() == "first method" | ||||
|  | ||||
|     if method in ["DELETE", "PATCH"]: | ||||
|         request, response = getattr(app.test_client, method.lower())("/") | ||||
|         assert response.text == "second method" | ||||
|     # if method in ["DELETE", "PATCH"]: | ||||
|     #     request, response = getattr(app.test_client, method.lower())("/") | ||||
|     #     assert response.text == "second method" | ||||
|  | ||||
|         response = view(request) | ||||
|         assert response.body.decode() == "second method" | ||||
|     #     response = view(request) | ||||
|     #     assert response.body.decode() == "second method" | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("method", HTTP_METHODS) | ||||
|   | ||||
| @@ -9,6 +9,8 @@ from unittest import mock | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| from sanic_testing.testing import ASGI_PORT as PORT | ||||
|  | ||||
| from sanic.app import Sanic | ||||
| from sanic.worker import GunicornWorker | ||||
|  | ||||
| @@ -17,7 +19,7 @@ from sanic.worker import GunicornWorker | ||||
| def gunicorn_worker(): | ||||
|     command = ( | ||||
|         "gunicorn " | ||||
|         "--bind 127.0.0.1:1337 " | ||||
|         f"--bind 127.0.0.1:{PORT} " | ||||
|         "--worker-class sanic.worker.GunicornWorker " | ||||
|         "examples.simple_server:app" | ||||
|     ) | ||||
| @@ -31,7 +33,7 @@ def gunicorn_worker(): | ||||
| def gunicorn_worker_with_access_logs(): | ||||
|     command = ( | ||||
|         "gunicorn " | ||||
|         "--bind 127.0.0.1:1338 " | ||||
|         f"--bind 127.0.0.1:{PORT + 1} " | ||||
|         "--worker-class sanic.worker.GunicornWorker " | ||||
|         "examples.simple_server:app" | ||||
|     ) | ||||
| @@ -45,7 +47,7 @@ def gunicorn_worker_with_env_var(): | ||||
|     command = ( | ||||
|         'env SANIC_ACCESS_LOG="False" ' | ||||
|         "gunicorn " | ||||
|         "--bind 127.0.0.1:1339 " | ||||
|         f"--bind 127.0.0.1:{PORT + 2} " | ||||
|         "--worker-class sanic.worker.GunicornWorker " | ||||
|         "--log-level info " | ||||
|         "examples.simple_server:app" | ||||
| @@ -56,7 +58,7 @@ def gunicorn_worker_with_env_var(): | ||||
|  | ||||
|  | ||||
| def test_gunicorn_worker(gunicorn_worker): | ||||
|     with urllib.request.urlopen("http://localhost:1337/") as f: | ||||
|     with urllib.request.urlopen(f"http://localhost:{PORT}/") as f: | ||||
|         res = json.loads(f.read(100).decode()) | ||||
|     assert res["test"] | ||||
|  | ||||
| @@ -65,7 +67,7 @@ def test_gunicorn_worker_no_logs(gunicorn_worker_with_env_var): | ||||
|     """ | ||||
|     if SANIC_ACCESS_LOG was set to False do not show access logs | ||||
|     """ | ||||
|     with urllib.request.urlopen("http://localhost:1339/") as _: | ||||
|     with urllib.request.urlopen(f"http://localhost:{PORT + 2}/") as _: | ||||
|         gunicorn_worker_with_env_var.kill() | ||||
|         assert not gunicorn_worker_with_env_var.stdout.read() | ||||
|  | ||||
| @@ -74,7 +76,7 @@ def test_gunicorn_worker_with_logs(gunicorn_worker_with_access_logs): | ||||
|     """ | ||||
|     default - show access logs | ||||
|     """ | ||||
|     with urllib.request.urlopen("http://localhost:1338/") as _: | ||||
|     with urllib.request.urlopen(f"http://localhost:{PORT + 1}/") as _: | ||||
|         gunicorn_worker_with_access_logs.kill() | ||||
|         assert ( | ||||
|             b"(sanic.access)[INFO][127.0.0.1" | ||||
|   | ||||
							
								
								
									
										4
									
								
								tox.ini
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								tox.ini
									
									
									
									
									
								
							| @@ -7,7 +7,7 @@ setenv = | ||||
|     {py36,py37,py38,py39,pyNightly}-no-ext: SANIC_NO_UJSON=1 | ||||
|     {py36,py37,py38,py39,pyNightly}-no-ext: SANIC_NO_UVLOOP=1 | ||||
| deps = | ||||
|     sanic-testing==0.1.2 | ||||
|     sanic-testing | ||||
|     coverage==5.3 | ||||
|     pytest==5.2.1 | ||||
|     pytest-cov | ||||
| @@ -35,7 +35,7 @@ deps = | ||||
| commands = | ||||
|     flake8 sanic | ||||
|     black --config ./.black.toml --check --verbose sanic/ | ||||
|     isort --check-only sanic | ||||
|     isort --check-only sanic --profile=black | ||||
|  | ||||
| [testenv:type-checking] | ||||
| deps = | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Adam Hopkins
					Adam Hopkins