Merge in latest from sanic-routing branch

This commit is contained in:
Adam Hopkins 2021-02-15 17:20:07 +02:00
commit 973c315790
44 changed files with 1299 additions and 609 deletions

67
.github/workflows/codeql-analysis.yml vendored Normal file
View 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

View File

@ -63,17 +63,14 @@ ifdef include_tests
isort -rc sanic tests isort -rc sanic tests
else else
$(info Sorting Imports) $(info Sorting Imports)
isort -rc sanic tests isort -rc sanic tests --profile=black
endif endif
endif endif
black: black:
black --config ./.black.toml sanic tests black --config ./.black.toml sanic tests
isort: pretty: beautify
isort sanic tests
pretty: black isort
docs-clean: docs-clean:
cd docs && make clean cd docs && make clean

View File

@ -6,14 +6,15 @@ Sanic releases long term support release once a year in December. LTS releases r
| Version | LTS | Supported | | Version | LTS | Supported |
| ------- | ------------- | ------------------ | | ------- | ------------- | ------------------ |
| 20.9 | | :heavy_check_mark: | | 20.12 | until 2022-12 | :heavy_check_mark: |
| 20.9 | | :x: |
| 20.6 | | :x: | | 20.6 | | :x: |
| 20.3 | | :x: | | 20.3 | | :x: |
| 19.12 | until 2021-12 | :white_check_mark: | | 19.12 | until 2021-12 | :white_check_mark: |
| 19.9 | | :x: | | 19.9 | | :x: |
| 19.6 | | :x: | | 19.6 | | :x: |
| 19.3 | | :x: | | 19.3 | | :x: |
| 18.12 | until 2020-12 | :white_check_mark: | | 18.12 | | :x: |
| 0.8.3 | | :x: | | 0.8.3 | | :x: |
| 0.7.0 | | :x: | | 0.7.0 | | :x: |
| 0.6.0 | | :x: | | 0.6.0 | | :x: |

View File

@ -25,27 +25,24 @@ from typing import (
) )
from urllib.parse import urlencode, urlunparse 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 import reloader_helpers
from sanic.asgi import ASGIApp from sanic.asgi import ASGIApp
from sanic.base import BaseSanic
from sanic.blueprint_group import BlueprintGroup from sanic.blueprint_group import BlueprintGroup
from sanic.blueprints import Blueprint from sanic.blueprints import Blueprint
from sanic.config import BASE_LOGO, Config from sanic.config import BASE_LOGO, Config
from sanic.exceptions import ( from sanic.exceptions import (
InvalidUsage, InvalidUsage,
NotFound,
SanicException, SanicException,
ServerError, ServerError,
URLBuildError, URLBuildError,
) )
from sanic.handlers import ErrorHandler, ListenerType, MiddlewareType from sanic.handlers import ErrorHandler, ListenerType, MiddlewareType
from sanic.log import LOGGING_CONFIG_DEFAULTS, error_logger, logger from sanic.log import LOGGING_CONFIG_DEFAULTS, error_logger, logger
from sanic.mixins.base import BaseMixin from sanic.mixins.listeners import ListenerEvent
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.models.futures import ( from sanic.models.futures import (
FutureException, FutureException,
FutureListener, FutureListener,
@ -68,9 +65,7 @@ from sanic.static import register as static_register
from sanic.websocket import ConnectionClosed, WebSocketProtocol from sanic.websocket import ConnectionClosed, WebSocketProtocol
class Sanic( class Sanic(BaseSanic):
BaseMixin, RouteMixin, MiddlewareMixin, ListenerMixin, ExceptionMixin
):
""" """
The main application instance The main application instance
""" """
@ -103,9 +98,7 @@ class Sanic(
self.name = name self.name = name
self.asgi = False self.asgi = False
self.router = router or Router( self.router = router or Router()
exception=NotFound, method_handler_exception=NotFound
)
self.request_class = request_class self.request_class = request_class
self.error_handler = error_handler or ErrorHandler() self.error_handler = error_handler or ErrorHandler()
self.config = Config(load_env=load_env) self.config = Config(load_env=load_env)
@ -124,6 +117,9 @@ class Sanic(
self.websocket_tasks: Set[Future] = set() self.websocket_tasks: Set[Future] = set()
self.named_request_middleware: Dict[str, Deque[MiddlewareType]] = {} self.named_request_middleware: Dict[str, Deque[MiddlewareType]] = {}
self.named_response_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._test_client = None
self._asgi_client = None self._asgi_client = None
# Register alternative method names # Register alternative method names
@ -135,6 +131,8 @@ class Sanic(
if self.config.REGISTER: if self.config.REGISTER:
self.__class__.register_app(self) self.__class__.register_app(self)
self.router.ctx.app = self
@property @property
def loop(self): def loop(self):
""" """
@ -175,10 +173,6 @@ class Sanic(
partial(self._loop_add_task, task) 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: def register_listener(self, listener: Callable, event: str) -> Any:
""" """
Register the listener for a given event. Register the listener for a given event.
@ -197,42 +191,6 @@ class Sanic(
self.listeners[_event].append(listener) self.listeners[_event].append(listener)
return 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"): def register_middleware(self, middleware, attach_to: str = "request"):
""" """
Register an application level middleware that will be attached Register an application level middleware that will be attached
@ -288,13 +246,49 @@ class Sanic(
if middleware not in self.named_response_middleware[_rn]: if middleware not in self.named_response_middleware[_rn]:
self.named_response_middleware[_rn].appendleft(middleware) 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( def _apply_middleware(
self, self,
middleware: FutureMiddleware, middleware: FutureMiddleware,
route_names: Optional[List[str]] = None, route_names: Optional[List[str]] = None,
): ):
print(f"{middleware=}")
if route_names: if route_names:
return self.register_named_middleware( return self.register_named_middleware(
middleware.middleware, route_names, middleware.attach_to middleware.middleware, route_names, middleware.attach_to
@ -304,6 +298,19 @@ class Sanic(
middleware.middleware, middleware.attach_to 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): def blueprint(self, blueprint, **options):
"""Register a blueprint on the application. """Register a blueprint on the application.
@ -323,6 +330,12 @@ class Sanic(
else: else:
self.blueprints[blueprint.name] = blueprint self.blueprints[blueprint.name] = blueprint
self._blueprint_order.append(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) blueprint.register(self, options)
def url_for(self, view_name: str, **kwargs): def url_for(self, view_name: str, **kwargs):
@ -351,30 +364,28 @@ class Sanic(
# find the route by the supplied view name # find the route by the supplied view name
kw: Dict[str, str] = {} kw: Dict[str, str] = {}
# special static files url_for # special static files url_for
if view_name == "static":
kw.update(name=kwargs.pop("name", "static")) if "." not in view_name:
elif view_name.endswith(".static"): # blueprint.static view_name = f"{self.name}.{view_name}"
kwargs.pop("name", None)
if view_name.endswith(".static"):
name = kwargs.pop("name", None)
if name:
view_name = view_name.replace("static", name)
kw.update(name=view_name) kw.update(name=view_name)
uri, route = self.router.find_route_by_view_name(view_name, **kw) route = self.router.find_route_by_view_name(view_name, **kw)
if not (uri and route): if not route:
raise URLBuildError( raise URLBuildError(
f"Endpoint with name `{view_name}` was not found" f"Endpoint with name `{view_name}` was not found"
) )
# If the route has host defined, split that off uri = route.path
# TODO: Retain netloc and path separately in Route objects
host = uri.find("/")
if host > 0:
host, uri = uri[:host], uri[host:]
else:
host = None
if view_name == "static" or view_name.endswith(".static"): if getattr(route.ctx, "static", None):
filename = kwargs.pop("filename", None) filename = kwargs.pop("filename", "")
# it's static folder # it's static folder
if "<file_uri:" in uri: if "file_uri" in uri:
folder_ = uri.split("<file_uri:", 1)[0] folder_ = uri.split("<file_uri:", 1)[0]
if folder_.endswith("/"): if folder_.endswith("/"):
folder_ = folder_[:-1] folder_ = folder_[:-1]
@ -382,22 +393,36 @@ class Sanic(
if filename.startswith("/"): if filename.startswith("/"):
filename = filename[1:] filename = filename[1:]
uri = f"{folder_}/{filename}" kwargs["file_uri"] = filename
if uri != "/" and uri.endswith("/"): if uri != "/" and uri.endswith("/"):
uri = uri[:-1] uri = uri[:-1]
out = uri if not uri.startswith("/"):
uri = f"/{uri}"
# find all the parameters we will need to build in the URL out = uri
matched_params = re.findall(self.router.parameter_pattern, uri)
# _method is only a placeholder now, don't know how to support it # _method is only a placeholder now, don't know how to support it
kwargs.pop("_method", None) kwargs.pop("_method", None)
anchor = kwargs.pop("_anchor", "") anchor = kwargs.pop("_anchor", "")
# _external need SERVER_NAME in config or pass _server arg # _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", "") 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: if scheme and not external:
raise ValueError("When specifying _scheme, _external must be True") raise ValueError("When specifying _scheme, _external must be True")
@ -415,44 +440,44 @@ class Sanic(
if "://" in netloc[:8]: if "://" in netloc[:8]:
netloc = netloc.split("://", 1)[-1] netloc = netloc.split("://", 1)[-1]
for match in matched_params: # find all the parameters we will need to build in the URL
name, _type, pattern = self.router.parse_parameter_string(match) # 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 # we only want to match against each individual parameter
specific_pattern = f"^{pattern}$"
supplied_param = None
if name in kwargs: try:
supplied_param = kwargs.get(name) supplied_param = str(kwargs.pop(param_info.name))
del kwargs[name] except KeyError:
else:
raise URLBuildError( 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
# determine if the parameter supplied by the caller passes the test # passes the test in the URL
# in the URL if param_info.pattern:
passes_pattern = re.match(specific_pattern, supplied_param) passes_pattern = param_info.pattern.match(supplied_param)
if not passes_pattern:
if not passes_pattern: if param_info.cast != str:
if _type != str: msg = (
type_name = _type.__name__ f'Value "{supplied_param}" '
f"for parameter `{param_info.name}` does "
msg = ( "not match pattern for type "
f'Value "{supplied_param}" ' f"`{param_info.cast.__name__}`: "
f"for parameter `{name}` does not " f"{param_info.pattern.pattern}"
f"match pattern for type `{type_name}`: {pattern}" )
) else:
else: msg = (
msg = ( f'Value "{supplied_param}" for parameter '
f'Value "{supplied_param}" for parameter `{name}` ' f"`{param_info.name}` does not satisfy "
f"does not satisfy pattern {pattern}" f"pattern {param_info.pattern.pattern}"
) )
raise URLBuildError(msg) raise URLBuildError(msg)
# replace the parameter in the URL with the supplied value # 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) out = re.sub(replacement_regex, supplied_param, out)
# parse the remainder of the keyword arguments into a querystring # parse the remainder of the keyword arguments into a querystring
@ -545,14 +570,13 @@ class Sanic(
# Fetch handler from router # Fetch handler from router
( (
handler, handler,
args,
kwargs, kwargs,
uri, uri,
name, name,
endpoint,
ignore_body, ignore_body,
) = self.router.get(request) ) = self.router.get(request)
request.name = name request.name = name
request._match_info = kwargs
if ( if (
request.stream request.stream
@ -578,7 +602,7 @@ class Sanic(
# Execute Handler # Execute Handler
# -------------------------------------------- # # -------------------------------------------- #
request.uri_template = uri request.uri_template = f"/{uri}"
if handler is None: if handler is None:
raise ServerError( raise ServerError(
( (
@ -587,10 +611,10 @@ class Sanic(
) )
) )
request.endpoint = endpoint request.endpoint = request.name
# Run response handler # Run response handler
response = handler(request, *args, **kwargs) response = handler(request, **kwargs)
if isawaitable(response): if isawaitable(response):
response = await response response = await response
if response: if response:
@ -615,26 +639,60 @@ class Sanic(
except CancelledError: except CancelledError:
raise raise
except Exception as e: except Exception as e:
# -------------------------------------------- #
# Response Generation Failed # Response Generation Failed
# -------------------------------------------- #
await self.handle_exception(request, e) 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 # Testing
# -------------------------------------------------------------------- # # -------------------------------------------------------------------- #
@property @property
def test_client(self): def test_client(self): # noqa
if self._test_client: if self._test_client:
return 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 from sanic_testing.testing import SanicTestClient # type: ignore
self._test_client = SanicTestClient(self) self._test_client = SanicTestClient(self)
return self._test_client return self._test_client
@property @property
def asgi_client(self): def asgi_client(self): # noqa
""" """
A testing client that uses ASGI to reach into the application to A testing client that uses ASGI to reach into the application to
execute hanlers. execute hanlers.
@ -644,6 +702,8 @@ class Sanic(
""" """
if self._asgi_client: if self._asgi_client:
return 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 from sanic_testing.testing import SanicASGITestClient # type: ignore
self._asgi_client = SanicASGITestClient(self) self._asgi_client = SanicASGITestClient(self)
@ -915,7 +975,11 @@ class Sanic(
): ):
"""Helper function used by `run` and `create_server`.""" """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): if isinstance(ssl, dict):
# try common aliaseses # try common aliaseses
@ -950,9 +1014,7 @@ class Sanic(
"backlog": backlog, "backlog": backlog,
} }
# -------------------------------------------- #
# Register start/stop events # Register start/stop events
# -------------------------------------------- #
for event_name, settings_name, reverse in ( for event_name, settings_name, reverse in (
("before_server_start", "before_start", False), ("before_server_start", "before_start", False),
@ -1014,40 +1076,6 @@ class Sanic(
for task in app.websocket_tasks: for task in app.websocket_tasks:
task.cancel() task.cancel()
async def _websocket_handler(
self, handler, request, *args, subprotocols=None, **kwargs
):
request.app = self
if not getattr(handler, "__blueprintname__", False):
request.endpoint = handler.__name__
else:
request.endpoint = (
getattr(handler, "__blueprintname__", "") + handler.__name__
)
pass
if self.asgi:
ws = request.transport.get_websocket_connection()
else:
protocol = request.transport.get_protocol()
protocol.app = self
ws = await protocol.websocket_handshake(request, subprotocols)
# schedule the application handler
# its future is kept in self.websocket_tasks in case it
# needs to be cancelled due to the server being stopped
fut = ensure_future(handler(request, ws, *args, **kwargs))
self.websocket_tasks.add(fut)
try:
await fut
except (CancelledError, ConnectionClosed):
pass
finally:
self.websocket_tasks.remove(fut)
await ws.close()
# -------------------------------------------------------------------- # # -------------------------------------------------------------------- #
# ASGI # ASGI
# -------------------------------------------------------------------- # # -------------------------------------------------------------------- #
@ -1057,9 +1085,10 @@ class Sanic(
To be ASGI compliant, our instance must be a callable that accepts To be ASGI compliant, our instance must be a callable that accepts
three arguments: scope, receive, send. See the ASGI reference for more three arguments: scope, receive, send. See the ASGI reference for more
details: https://asgi.readthedocs.io/en/latest details: https://asgi.readthedocs.io/en/latest
/""" """
self.asgi = True 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() await asgi_app()
_asgi_single_callable = True # We conform to ASGI 3.0 single-callable _asgi_single_callable = True # We conform to ASGI 3.0 single-callable

View File

@ -131,6 +131,7 @@ class Lifespan:
in sequence since the ASGI lifespan protocol only supports a single in sequence since the ASGI lifespan protocol only supports a single
startup event. startup event.
""" """
self.asgi_app.sanic_app.router.finalize()
listeners = self.asgi_app.sanic_app.listeners.get( listeners = self.asgi_app.sanic_app.listeners.get(
"before_server_start", [] "before_server_start", []
) + self.asgi_app.sanic_app.listeners.get("after_server_start", []) ) + self.asgi_app.sanic_app.listeners.get("after_server_start", [])

36
sanic/base.py Normal file
View 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,
):
...

View File

@ -1,18 +1,12 @@
from collections import defaultdict, namedtuple from collections import defaultdict
from typing import Iterable, Optional from typing import Optional
from sanic.base import BaseSanic
from sanic.blueprint_group import BlueprintGroup 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 from sanic.models.futures import FutureRoute, FutureStatic
class Blueprint( class Blueprint(BaseSanic):
BaseMixin, RouteMixin, MiddlewareMixin, ListenerMixin, ExceptionMixin
):
""" """
In *Sanic* terminology, a **Blueprint** is a logical collection of In *Sanic* terminology, a **Blueprint** is a logical collection of
URLs that perform a specific set of tasks which can be identified by 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 # Prepend the blueprint URI prefix if available
uri = url_prefix + future.uri if url_prefix else future.uri 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( apply_route = FutureRoute(
future.handler, future.handler,
uri[1:] if uri.startswith("//") else uri, uri[1:] if uri.startswith("//") else uri,
future.methods, future.methods,
future.host or self.host, future.host or self.host,
future.strict_slashes, strict_slashes,
future.stream, future.stream,
future.version or self.version, future.version or self.version,
future.name, name,
future.ignore_body, future.ignore_body,
future.websocket,
future.subprotocols,
future.unquote,
future.static,
) )
route = app._apply_route(apply_route) route = app._apply_route(apply_route)
routes.append(route) operation = (
routes.extend if isinstance(route, list) else routes.append
)
operation(route)
# Static Files # Static Files
for future in self._future_statics: for future in self._future_statics:
@ -148,8 +157,9 @@ class Blueprint(
route_names = [route.name for route in routes if route] route_names = [route.name for route in routes if route]
# Middleware # Middleware
for future in self._future_middleware: if route_names:
app._apply_middleware(future, route_names) for future in self._future_middleware:
app._apply_middleware(future, route_names)
# Exceptions # Exceptions
for future in self._future_exceptions: for future in self._future_exceptions:
@ -158,6 +168,3 @@ class Blueprint(
# Event listeners # Event listeners
for listener in self._future_listeners: for listener in self._future_listeners:
app._apply_listener(listener) app._apply_listener(listener)
def _generate_name(self, handler, name: str) -> str:
return f"{self.name}.{name or handler.__name__}"

View File

@ -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):
...

View File

@ -1,5 +1,3 @@
from enum import Enum, auto
from functools import partial
from typing import Set from typing import Set
from sanic.models.futures import FutureException from sanic.models.futures import FutureException
@ -29,6 +27,9 @@ class ExceptionMixin:
nonlocal apply nonlocal apply
nonlocal exceptions nonlocal exceptions
if isinstance(exceptions[0], list):
exceptions = tuple(*exceptions)
future_exception = FutureException(handler, exceptions) future_exception = FutureException(handler, exceptions)
self._future_exceptions.add(future_exception) self._future_exceptions.add(future_exception)
if apply: if apply:

View File

@ -1,6 +1,6 @@
from enum import Enum, auto from enum import Enum, auto
from functools import partial 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 from sanic.models.futures import FutureListener
@ -17,7 +17,7 @@ class ListenerEvent(str, Enum):
class ListenerMixin: class ListenerMixin:
def __init__(self, *args, **kwargs) -> None: def __init__(self, *args, **kwargs) -> None:
self._future_listeners: Set[FutureListener] = set() self._future_listeners: List[FutureListener] = list()
def _apply_listener(self, listener: FutureListener): def _apply_listener(self, listener: FutureListener):
raise NotImplementedError raise NotImplementedError
@ -51,7 +51,7 @@ class ListenerMixin:
nonlocal apply nonlocal apply
future_listener = FutureListener(listener, event) future_listener = FutureListener(listener, event)
self._future_listeners.add(future_listener) self._future_listeners.append(future_listener)
if apply: if apply:
self._apply_listener(future_listener) self._apply_listener(future_listener)
return listener return listener

View File

@ -1,12 +1,12 @@
from functools import partial from functools import partial
from typing import Set from typing import List
from sanic.models.futures import FutureMiddleware from sanic.models.futures import FutureMiddleware
class MiddlewareMixin: class MiddlewareMixin:
def __init__(self, *args, **kwargs) -> None: def __init__(self, *args, **kwargs) -> None:
self._future_middleware: Set[FutureMiddleware] = set() self._future_middleware: List[FutureMiddleware] = list()
def _apply_middleware(self, middleware: FutureMiddleware): def _apply_middleware(self, middleware: FutureMiddleware):
raise NotImplementedError raise NotImplementedError
@ -30,7 +30,7 @@ class MiddlewareMixin:
nonlocal apply nonlocal apply
future_middleware = FutureMiddleware(middleware, attach_to) future_middleware = FutureMiddleware(middleware, attach_to)
self._future_middleware.add(future_middleware) self._future_middleware.append(future_middleware)
if apply: if apply:
self._apply_middleware(future_middleware) self._apply_middleware(future_middleware)
return middleware return middleware

View File

@ -1,9 +1,8 @@
from functools import partial
from inspect import signature from inspect import signature
from pathlib import PurePath from pathlib import PurePath
from typing import Iterable, List, Optional, Set, Union 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.constants import HTTP_METHODS
from sanic.models.futures import FutureRoute, FutureStatic from sanic.models.futures import FutureRoute, FutureStatic
@ -36,6 +35,8 @@ class RouteMixin:
apply: bool = True, apply: bool = True,
subprotocols: Optional[List[str]] = None, subprotocols: Optional[List[str]] = None,
websocket: bool = False, websocket: bool = False,
unquote: bool = False,
static: bool = False,
): ):
""" """
Decorate a function to be registered as a route Decorate a function to be registered as a route
@ -52,9 +53,6 @@ class RouteMixin:
:return: tuple of routes, decorated function :return: tuple of routes, decorated function
""" """
if websocket:
self.enable_websocket()
# Fix case where the user did not prefix the URL with a / # Fix case where the user did not prefix the URL with a /
# and will probably get confused as to why it's not working # and will probably get confused as to why it's not working
if not uri.startswith("/"): if not uri.startswith("/"):
@ -63,6 +61,9 @@ class RouteMixin:
if strict_slashes is None: if strict_slashes is None:
strict_slashes = self.strict_slashes strict_slashes = self.strict_slashes
if not methods and not websocket:
methods = frozenset({"GET"})
def decorator(handler): def decorator(handler):
nonlocal uri nonlocal uri
nonlocal methods nonlocal methods
@ -74,39 +75,43 @@ class RouteMixin:
nonlocal ignore_body nonlocal ignore_body
nonlocal subprotocols nonlocal subprotocols
nonlocal websocket nonlocal websocket
nonlocal static
if isinstance(handler, tuple): if isinstance(handler, tuple):
# if a handler fn is already wrapped in a route, the handler # if a handler fn is already wrapped in a route, the handler
# variable will be a tuple of (existing routes, handler fn) # variable will be a tuple of (existing routes, handler fn)
_, handler = handler _, handler = handler
if websocket: name = self._generate_name(name, handler)
websocket_handler = partial(
self._websocket_handler,
handler,
subprotocols=subprotocols,
)
websocket_handler.__name__ = (
"websocket_handler_" + handler.__name__
)
websocket_handler.is_websocket = True
handler = websocket_handler
# TODO: if isinstance(host, str):
# - THink this thru.... do we want all routes namespaced? host = frozenset([host])
# - elif host and not isinstance(host, frozenset):
name = self._generate_name(handler, name) 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( route = FutureRoute(
handler, handler,
uri, uri,
frozenset(methods), None if websocket else frozenset([x.upper() for x in methods]),
host, host,
strict_slashes, strict_slashes,
stream, stream,
version, version,
name, name,
ignore_body, ignore_body,
websocket,
subprotocols,
unquote,
static,
) )
self._future_routes.add(route) self._future_routes.add(route)
@ -441,6 +446,7 @@ class RouteMixin:
subprotocols: Optional[List[str]] = None, subprotocols: Optional[List[str]] = None,
version: Optional[int] = None, version: Optional[int] = None,
name: Optional[str] = None, name: Optional[str] = None,
apply: bool = True,
): ):
""" """
Decorate a function to be registered as a websocket route Decorate a function to be registered as a websocket route
@ -543,12 +549,16 @@ class RouteMixin:
:rtype: List[sanic.router.Route] :rtype: List[sanic.router.Route]
""" """
if not name.startswith(self.name + "."): name = self._generate_name(name)
name = f"{self.name}.{name}"
if strict_slashes is None and self.strict_slashes is not None: if strict_slashes is None and self.strict_slashes is not None:
strict_slashes = self.strict_slashes 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( static = FutureStatic(
uri, uri,
file_or_directory, file_or_directory,
@ -566,5 +576,29 @@ class RouteMixin:
if apply: if apply:
self._apply_static(static) self._apply_static(static)
def _generate_name(self, handler, name: str) -> str: def _generate_name(self, *objects) -> str:
return name or handler.__name__ 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

View File

@ -13,6 +13,10 @@ FutureRoute = namedtuple(
"version", "version",
"name", "name",
"ignore_body", "ignore_body",
"websocket",
"subprotocols",
"unquote",
"static",
], ],
) )
FutureListener = namedtuple("FutureListener", ["listener", "event"]) FutureListener = namedtuple("FutureListener", ["listener", "event"])

View File

@ -87,6 +87,7 @@ class Request:
"_port", "_port",
"_remote_addr", "_remote_addr",
"_socket", "_socket",
"_match_info",
"app", "app",
"body", "body",
"conn_info", "conn_info",
@ -147,6 +148,7 @@ class Request:
self.uri_template: Optional[str] = None self.uri_template: Optional[str] = None
self.request_middleware_started = False self.request_middleware_started = False
self._cookies: Dict[str, str] = {} self._cookies: Dict[str, str] = {}
self._match_info = {}
self.stream: Optional[Http] = None self.stream: Optional[Http] = None
self.endpoint: Optional[str] = None self.endpoint: Optional[str] = None
@ -455,7 +457,7 @@ class Request:
""" """
:return: matched info after resolving route :return: matched info after resolving route
""" """
return self.app.router.get(self)[2] return self._match_info
# Transport properties (obtained from local interface only) # Transport properties (obtained from local interface only)

View File

@ -1,14 +1,22 @@
from functools import lru_cache 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 import BaseRouter # type: ignore
from sanic_routing.route import Route 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.constants import HTTP_METHODS
from sanic.exceptions import MethodNotSupported, NotFound
from sanic.handlers import RouteHandler from sanic.handlers import RouteHandler
from sanic.request import Request from sanic.request import Request
ROUTER_CACHE_SIZE = 1024
class Router(BaseRouter): class Router(BaseRouter):
""" """
The router implementation responsible for routing a :class:`Request` object The router implementation responsible for routing a :class:`Request` object
@ -18,18 +26,38 @@ class Router(BaseRouter):
DEFAULT_METHOD = "GET" DEFAULT_METHOD = "GET"
ALLOWED_METHODS = HTTP_METHODS ALLOWED_METHODS = HTTP_METHODS
@lru_cache # Putting the lru_cache on Router.get() performs better for the benchmarsk
def get( # at tests/benchmark/test_route_resolution_benchmark.py
self, request: Request # However, overall application performance is significantly improved
) -> Tuple[ # with the lru_cache on this method.
RouteHandler, @lru_cache(maxsize=ROUTER_CACHE_SIZE)
Tuple[Any, ...], def _get(
Dict[str, Any], self, path, method, host
str, ) -> Tuple[RouteHandler, Dict[str, Any], str, str, bool,]:
str, try:
Optional[str], route, handler, params = self.resolve(
bool, 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 Retrieve a `Route` object containg the details about how to handle
a response for a given request a response for a given request
@ -41,23 +69,8 @@ class Router(BaseRouter):
:rtype: Tuple[ RouteHandler, Tuple[Any, ...], Dict[str, Any], str, str, :rtype: Tuple[ RouteHandler, Tuple[Any, ...], Dict[str, Any], str, str,
Optional[str], bool, ] Optional[str], bool, ]
""" """
route, handler, params = self.resolve( return self._get(
path=request.path, request.path, request.method, request.headers.get("host")
method=request.method,
)
# TODO: Implement response
# - args,
# - endpoint,
return (
handler,
(),
params,
route.path,
route.name,
None,
route.ctx.ignore_body,
) )
def add( def add(
@ -65,13 +78,15 @@ class Router(BaseRouter):
uri: str, uri: str,
methods: Iterable[str], methods: Iterable[str],
handler: RouteHandler, handler: RouteHandler,
host: Optional[str] = None, host: Optional[Union[str, Iterable[str]]] = None,
strict_slashes: bool = False, strict_slashes: bool = False,
stream: bool = False, stream: bool = False,
ignore_body: bool = False, ignore_body: bool = False,
version: Union[str, float, int] = None, version: Union[str, float, int] = None,
name: Optional[str] = None, name: Optional[str] = None,
) -> Route: unquote: bool = False,
static: bool = False,
) -> Union[Route, List[Route]]:
""" """
Add a handler to the router Add a handler to the router
@ -99,19 +114,93 @@ class Router(BaseRouter):
:return: the route object :return: the route object
:rtype: Route :rtype: Route
""" """
# TODO: Implement
# - host
# - strict_slashes
# - ignore_body
# - stream
if version is not None: if version is not None:
version = str(version).strip("/").lstrip("v") version = str(version).strip("/").lstrip("v")
uri = "/".join([f"/v{version}", uri.lstrip("/")]) uri = "/".join([f"/v{version}", uri.lstrip("/")])
route = super().add( params = dict(
path=uri, handler=handler, methods=methods, name=name 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 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

View File

@ -157,11 +157,11 @@ def register(
# If we're not trying to match a file directly, # If we're not trying to match a file directly,
# serve from the folder # serve from the folder
if not path.isfile(file_or_directory): if not path.isfile(file_or_directory):
uri += "<file_uri:" + static.pattern + ">" uri += "/<file_uri>"
# special prefix for static files # special prefix for static files
if not static.name.startswith("_static_"): # if not static.name.startswith("_static_"):
name = f"_static_{static.name}" # name = f"_static_{static.name}"
_handler = wraps(_static_request_handler)( _handler = wraps(_static_request_handler)(
partial( partial(
@ -174,11 +174,13 @@ def register(
) )
) )
_routes, _ = app.route( route, _ = app.route(
uri=uri, uri=uri,
methods=["GET", "HEAD"], methods=["GET", "HEAD"],
name=name, name=name,
host=static.host, host=static.host,
strict_slashes=static.strict_slashes, strict_slashes=static.strict_slashes,
static=True,
)(_handler) )(_handler)
return _routes
return route

View File

@ -83,6 +83,7 @@ ujson = "ujson>=1.35" + env_dependency
uvloop = "uvloop>=0.5.3" + env_dependency uvloop = "uvloop>=0.5.3" + env_dependency
requirements = [ requirements = [
"sanic-routing",
"httptools>=0.0.10", "httptools>=0.0.10",
uvloop, uvloop,
ujson, ujson,

View File

@ -4,6 +4,8 @@ from pytest import mark
import sanic.router import sanic.router
from sanic.request import Request
seed("Pack my box with five dozen liquor jugs.") seed("Pack my box with five dozen liquor jugs.")
@ -23,8 +25,17 @@ class TestSanicRouteResolution:
route_to_call = choice(simple_routes) route_to_call = choice(simple_routes)
result = benchmark.pedantic( result = benchmark.pedantic(
router._get, router.get,
("/{}".format(route_to_call[-1]), route_to_call[0], "localhost"), (
Request(
"/{}".format(route_to_call[-1]).encode(),
{"host": "localhost"},
"v1",
route_to_call[0],
None,
None,
),
),
iterations=1000, iterations=1000,
rounds=1000, rounds=1000,
) )
@ -47,8 +58,17 @@ class TestSanicRouteResolution:
print("{} -> {}".format(route_to_call[-1], url)) print("{} -> {}".format(route_to_call[-1], url))
result = benchmark.pedantic( result = benchmark.pedantic(
router._get, router.get,
("/{}".format(url), route_to_call[0], "localhost"), (
Request(
"/{}".format(url).encode(),
{"host": "localhost"},
"v1",
route_to_call[0],
None,
None,
),
),
iterations=1000, iterations=1000,
rounds=1000, rounds=1000,
) )

View File

@ -4,12 +4,16 @@ import string
import sys import sys
import uuid import uuid
from typing import Tuple
import pytest import pytest
from sanic_routing.exceptions import RouteExists
from sanic_testing import TestManager from sanic_testing import TestManager
from sanic import Sanic 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.") random.seed("Pack my box with five dozen liquor jugs.")
@ -38,12 +42,12 @@ async def _handler(request):
TYPE_TO_GENERATOR_MAP = { TYPE_TO_GENERATOR_MAP = {
"string": lambda: "".join( "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)), "int": lambda: random.choice(range(1000000)),
"number": lambda: random.random(), "number": lambda: random.random(),
"alpha": lambda: "".join( "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()), "uuid": lambda: str(uuid.uuid1()),
} }
@ -52,7 +56,7 @@ TYPE_TO_GENERATOR_MAP = {
class RouteStringGenerator: class RouteStringGenerator:
ROUTE_COUNT_PER_DEPTH = 100 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"] ROUTE_PARAM_TYPES = ["string", "int", "number", "alpha", "uuid"]
def generate_random_direct_route(self, max_route_depth=4): def generate_random_direct_route(self, max_route_depth=4):
@ -105,12 +109,12 @@ class RouteStringGenerator:
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def sanic_router(app): def sanic_router(app):
# noinspection PyProtectedMember # noinspection PyProtectedMember
def _setup(route_details: tuple) -> (Router, tuple): def _setup(route_details: tuple) -> Tuple[Router, tuple]:
router = Router(app) router = Router()
added_router = [] added_router = []
for method, route in route_details: for method, route in route_details:
try: try:
router._add( router.add(
uri=f"/{route}", uri=f"/{route}",
methods=frozenset({method}), methods=frozenset({method}),
host="localhost", host="localhost",
@ -119,6 +123,7 @@ def sanic_router(app):
added_router.append((method, route)) added_router.append((method, route))
except RouteExists: except RouteExists:
pass pass
router.finalize()
return router, added_router return router, added_router
return _setup return _setup
@ -137,5 +142,4 @@ def url_param_generator():
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def app(request): def app(request):
app = Sanic(request.node.name) app = Sanic(request.node.name)
# TestManager(app)
return app return app

View File

@ -118,7 +118,7 @@ def test_app_route_raise_value_error(app):
def test_app_handle_request_handler_is_none(app, monkeypatch): def test_app_handle_request_handler_is_none(app, monkeypatch):
def mockreturn(*args, **kwargs): 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. # Not sure how to make app.router.get() return None, so use mock here.
monkeypatch.setattr(app.router, "get", mockreturn) monkeypatch.setattr(app.router, "get", mockreturn)

View File

@ -45,7 +45,8 @@ def protocol(transport):
return transport.get_protocol() return transport.get_protocol()
def test_listeners_triggered(app): def test_listeners_triggered():
app = Sanic("app")
before_server_start = False before_server_start = False
after_server_start = False after_server_start = False
before_server_stop = False before_server_stop = False
@ -71,6 +72,10 @@ def test_listeners_triggered(app):
nonlocal after_server_stop nonlocal after_server_stop
after_server_stop = True after_server_stop = True
@app.route("/")
def handler(request):
return text("...")
class CustomServer(uvicorn.Server): class CustomServer(uvicorn.Server):
def install_signal_handlers(self): def install_signal_handlers(self):
pass pass
@ -121,6 +126,10 @@ def test_listeners_triggered_async(app):
nonlocal after_server_stop nonlocal after_server_stop
after_server_stop = True after_server_stop = True
@app.route("/")
def handler(request):
return text("...")
class CustomServer(uvicorn.Server): class CustomServer(uvicorn.Server):
def install_signal_handlers(self): def install_signal_handlers(self):
pass pass
@ -325,7 +334,7 @@ async def test_cookie_customization(app):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_json_content_type(app): async def test_content_type(app):
@app.get("/json") @app.get("/json")
def send_json(request): def send_json(request):
return json({"foo": "bar"}) return json({"foo": "bar"})

View File

@ -4,6 +4,8 @@ import asyncio
def test_bad_request_response(app): def test_bad_request_response(app):
lines = [] lines = []
app.get("/")(lambda x: ...)
@app.listener("after_server_start") @app.listener("after_server_start")
async def _request(sanic, loop): async def _request(sanic, loop):
connect = asyncio.open_connection("127.0.0.1", 42101) connect = asyncio.open_connection("127.0.0.1", 42101)

View File

@ -209,18 +209,28 @@ def test_bp_with_host(app):
app.blueprint(bp) app.blueprint(bp)
headers = {"Host": "example.com"} headers = {"Host": "example.com"}
request, response = app.test_client.get("/test1/", headers=headers) request, response = app.test_client.get("/test1/", headers=headers)
assert response.text == "Hello" assert response.text == "Hello"
headers = {"Host": "sub.example.com"} headers = {"Host": "sub.example.com"}
request, response = app.test_client.get("/test1/", headers=headers) request, response = app.test_client.get("/test1/", headers=headers)
assert response.body == b"Hello subdomain!"
assert response.text == "Hello subdomain!"
def test_several_bp_with_host(app): def test_several_bp_with_host(app):
bp = Blueprint("test_text", url_prefix="/test", host="example.com") bp = Blueprint(
bp2 = Blueprint("test_text2", url_prefix="/test", host="sub.example.com") "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("/") @bp.route("/")
def handler(request): def handler(request):
@ -240,6 +250,7 @@ def test_several_bp_with_host(app):
assert bp.host == "example.com" assert bp.host == "example.com"
headers = {"Host": "example.com"} headers = {"Host": "example.com"}
request, response = app.test_client.get("/test/", headers=headers) request, response = app.test_client.get("/test/", headers=headers)
assert response.text == "Hello" assert response.text == "Hello"
assert bp2.host == "sub.example.com" assert bp2.host == "sub.example.com"
@ -352,6 +363,29 @@ def test_bp_middleware(app):
assert response.text == "FAIL" 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): def test_bp_middleware_order(app):
blueprint = Blueprint("test_bp_middleware_order") blueprint = Blueprint("test_bp_middleware_order")
order = list() order = list()
@ -425,6 +459,7 @@ def test_bp_exception_handler(app):
def test_bp_listeners(app): def test_bp_listeners(app):
app.route("/")(lambda x: x)
blueprint = Blueprint("test_middleware") blueprint = Blueprint("test_middleware")
order = [] order = []
@ -537,19 +572,19 @@ def test_bp_shorthand(app):
app.blueprint(blueprint) app.blueprint(blueprint)
request, response = app.test_client.get("/get") request, response = app.test_client.get("/get")
assert response.text == "OK" assert response.body == b"OK"
request, response = app.test_client.post("/get") request, response = app.test_client.post("/get")
assert response.status == 405 assert response.status == 405
request, response = app.test_client.put("/put") request, response = app.test_client.put("/put")
assert response.text == "OK" assert response.body == b"OK"
request, response = app.test_client.get("/post") request, response = app.test_client.get("/post")
assert response.status == 405 assert response.status == 405
request, response = app.test_client.post("/post") request, response = app.test_client.post("/post")
assert response.text == "OK" assert response.body == b"OK"
request, response = app.test_client.get("/post") request, response = app.test_client.get("/post")
assert response.status == 405 assert response.status == 405
@ -561,19 +596,19 @@ def test_bp_shorthand(app):
assert response.status == 405 assert response.status == 405
request, response = app.test_client.options("/options") request, response = app.test_client.options("/options")
assert response.text == "OK" assert response.body == b"OK"
request, response = app.test_client.get("/options") request, response = app.test_client.get("/options")
assert response.status == 405 assert response.status == 405
request, response = app.test_client.patch("/patch") request, response = app.test_client.patch("/patch")
assert response.text == "OK" assert response.body == b"OK"
request, response = app.test_client.get("/patch") request, response = app.test_client.get("/patch")
assert response.status == 405 assert response.status == 405
request, response = app.test_client.delete("/delete") request, response = app.test_client.delete("/delete")
assert response.text == "OK" assert response.body == b"OK"
request, response = app.test_client.get("/delete") request, response = app.test_client.get("/delete")
assert response.status == 405 assert response.status == 405
@ -699,7 +734,8 @@ def test_blueprint_middleware_with_args(app: Sanic):
@pytest.mark.parametrize("file_name", ["test.file"]) @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()) current_file = inspect.getfile(inspect.currentframe())
with open(current_file, "rb") as file: with open(current_file, "rb") as file:
file.read() 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 app.strict_slashes = True
bp = Blueprint("bp")
bp2 = Blueprint("bp2", strict_slashes=False)
@app.get("/test") @app.get("/test")
def handler_test(request): def handler_test(request):
return text("Test") return text("Test")
assert app.test_client.get("/test")[1].status == 200 @app.get("/f1", strict_slashes=False)
assert app.test_client.get("/test/")[1].status == 404 def f1(request):
return text("f1")
bp = Blueprint("bp")
@bp.get("/one", strict_slashes=False) @bp.get("/one", strict_slashes=False)
def one(request): def one(request):
@ -834,7 +872,15 @@ def test_strict_slashes_behavior_adoption(app):
def second(request): def second(request):
return text("second") return text("second")
@bp2.get("/third")
def third(request):
return text("third")
app.blueprint(bp) 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
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 == 200
assert app.test_client.get("/second/")[1].status == 404 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
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
assert app.test_client.get("/f1/")[1].status == 200 assert app.test_client.get("/f1/")[1].status == 200

View File

@ -43,7 +43,7 @@ async def test_cookies_asgi(app):
response_cookies = SimpleCookie() response_cookies = SimpleCookie()
response_cookies.load(response.headers.get("set-cookie", {})) 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" assert response_cookies["right_back"].value == "at you"

View File

@ -1,44 +1,44 @@
import pytest # import pytest
from sanic.response import text # from sanic.response import text
from sanic.router import RouteExists # from sanic.router import RouteExists
@pytest.mark.parametrize( # @pytest.mark.parametrize(
"method,attr, expected", # "method,attr, expected",
[ # [
("get", "text", "OK1 test"), # ("get", "text", "OK1 test"),
("post", "text", "OK2 test"), # ("post", "text", "OK2 test"),
("put", "text", "OK2 test"), # ("put", "text", "OK2 test"),
("delete", "status", 405), # ("delete", "status", 405),
], # ],
) # )
def test_overload_dynamic_routes(app, method, attr, expected): # def test_overload_dynamic_routes(app, method, attr, expected):
@app.route("/overload/<param>", methods=["GET"]) # @app.route("/overload/<param>", methods=["GET"])
async def handler1(request, param): # async def handler1(request, param):
return text("OK1 " + param) # return text("OK1 " + param)
@app.route("/overload/<param>", methods=["POST", "PUT"]) # @app.route("/overload/<param>", methods=["POST", "PUT"])
async def handler2(request, param): # async def handler2(request, param):
return text("OK2 " + param) # return text("OK2 " + param)
request, response = getattr(app.test_client, method)("/overload/test") # request, response = getattr(app.test_client, method)("/overload/test")
assert getattr(response, attr) == expected # assert getattr(response, attr) == expected
def test_overload_dynamic_routes_exist(app): # def test_overload_dynamic_routes_exist(app):
@app.route("/overload/<param>", methods=["GET"]) # @app.route("/overload/<param>", methods=["GET"])
async def handler1(request, param): # async def handler1(request, param):
return text("OK1 " + param) # return text("OK1 " + param)
@app.route("/overload/<param>", methods=["POST", "PUT"]) # @app.route("/overload/<param>", methods=["POST", "PUT"])
async def handler2(request, param): # async def handler2(request, param):
return text("OK2 " + param) # return text("OK2 " + param)
# if this doesn't raise an error, than at least the below should happen: # # if this doesn't raise an error, than at least the below should happen:
# assert response.text == 'Duplicated' # # assert response.text == 'Duplicated'
with pytest.raises(RouteExists): # with pytest.raises(RouteExists):
@app.route("/overload/<param>", methods=["PUT", "DELETE"]) # @app.route("/overload/<param>", methods=["PUT", "DELETE"])
async def handler3(request, param): # async def handler3(request, param):
return text("Duplicated") # return text("Duplicated")

View File

@ -126,8 +126,9 @@ def test_html_traceback_output_in_debug_mode():
assert response.status == 500 assert response.status == 500
soup = BeautifulSoup(response.body, "html.parser") soup = BeautifulSoup(response.body, "html.parser")
html = str(soup) 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 "handler_4" in html
assert "foo = bar" in html assert "foo = bar" in html
@ -151,7 +152,7 @@ def test_chained_exception_handler():
soup = BeautifulSoup(response.body, "html.parser") soup = BeautifulSoup(response.body, "html.parser")
html = str(soup) html = str(soup)
assert "response = handler(request, *args, **kwargs)" in html assert "response = handler(request, **kwargs)" in html
assert "handler_6" in html assert "handler_6" in html
assert "foo = 1 / arg" in html assert "foo = 1 / arg" in html
assert "ValueError" in html assert "ValueError" in html

View File

@ -103,7 +103,13 @@ def test_logging_pass_customer_logconfig():
assert fmt._fmt == modified_config["formatters"]["access"]["format"] 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): def test_log_connection_lost(app, debug, monkeypatch):
""" Should not log Connection lost exception on non debug """ """ Should not log Connection lost exception on non debug """
stream = StringIO() stream = StringIO()
@ -117,7 +123,7 @@ def test_log_connection_lost(app, debug, monkeypatch):
request.transport.close() request.transport.close()
return response 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 assert res is None
log = stream.getvalue() log = stream.getvalue()

View File

@ -102,6 +102,7 @@ def test_middleware_response_raise_exception(app, caplog):
async def process_response(request, response): async def process_response(request, response):
raise Exception("Exception at response middleware") raise Exception("Exception at response middleware")
app.route("/")(lambda x: x)
with caplog.at_level(logging.ERROR): with caplog.at_level(logging.ERROR):
reqrequest, response = app.test_client.get("/fail") reqrequest, response = app.test_client.get("/fail")
@ -129,7 +130,7 @@ def test_middleware_override_request(app):
async def handler(request): async def handler(request):
return text("FAIL") 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.status == 200
assert response.text == "OK" assert response.text == "OK"

View File

@ -68,9 +68,12 @@ def handler(request):
@pytest.mark.parametrize("protocol", [3, 4]) @pytest.mark.parametrize("protocol", [3, 4])
def test_pickle_app(app, protocol): def test_pickle_app(app, protocol):
app.route("/")(handler) app.route("/")(handler)
app.router.finalize()
app.router.reset()
p_app = pickle.dumps(app, protocol=protocol) p_app = pickle.dumps(app, protocol=protocol)
del app del app
up_p_app = pickle.loads(p_app) up_p_app = pickle.loads(p_app)
up_p_app.router.finalize()
assert up_p_app assert up_p_app
request, response = up_p_app.test_client.get("/") request, response = up_p_app.test_client.get("/")
assert response.text == "Hello" assert response.text == "Hello"
@ -81,9 +84,12 @@ def test_pickle_app_with_bp(app, protocol):
bp = Blueprint("test_text") bp = Blueprint("test_text")
bp.route("/")(handler) bp.route("/")(handler)
app.blueprint(bp) app.blueprint(bp)
app.router.finalize()
app.router.reset()
p_app = pickle.dumps(app, protocol=protocol) p_app = pickle.dumps(app, protocol=protocol)
del app del app
up_p_app = pickle.loads(p_app) up_p_app = pickle.loads(p_app)
up_p_app.router.finalize()
assert up_p_app assert up_p_app
request, response = up_p_app.test_client.get("/") request, response = up_p_app.test_client.get("/")
assert response.text == "Hello" assert response.text == "Hello"
@ -93,9 +99,12 @@ def test_pickle_app_with_bp(app, protocol):
def test_pickle_app_with_static(app, protocol): def test_pickle_app_with_static(app, protocol):
app.route("/")(handler) app.route("/")(handler)
app.static("/static", "/tmp/static") app.static("/static", "/tmp/static")
app.router.finalize()
app.router.reset()
p_app = pickle.dumps(app, protocol=protocol) p_app = pickle.dumps(app, protocol=protocol)
del app del app
up_p_app = pickle.loads(p_app) up_p_app = pickle.loads(p_app)
up_p_app.router.finalize()
assert up_p_app assert up_p_app
request, response = up_p_app.test_client.get("/static/missing.txt") request, response = up_p_app.test_client.get("/static/missing.txt")
assert response.status == 404 assert response.status == 404

View File

@ -5,6 +5,7 @@ import asyncio
import pytest import pytest
from sanic import Sanic
from sanic.blueprints import Blueprint from sanic.blueprints import Blueprint
from sanic.constants import HTTP_METHODS from sanic.constants import HTTP_METHODS
from sanic.exceptions import URLBuildError from sanic.exceptions import URLBuildError
@ -17,7 +18,9 @@ from sanic.response import text
@pytest.mark.parametrize("method", HTTP_METHODS) @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") bp = Blueprint("test_bp", url_prefix="/bp")
method = method.lower() method = method.lower()
@ -32,7 +35,6 @@ def test_versioned_named_routes_get(app, method):
return text("OK") return text("OK")
else: else:
print(func)
raise raise
func = getattr(bp, method) func = getattr(bp, method)
@ -43,15 +45,28 @@ def test_versioned_named_routes_get(app, method):
return text("OK") return text("OK")
else: else:
print(func)
raise raise
app.blueprint(bp) 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}"] route = app.router.routes_all[
assert route.name == f"test_bp.{route_name2}" (
"v1",
"bp",
method,
)
]
assert route.name == f"app.test_bp.{route_name2}"
assert app.url_for(route_name) == f"/v1/{method}" assert app.url_for(route_name) == f"/v1/{method}"
url = app.url_for(f"test_bp.{route_name2}") 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") app.url_for("handler")
def test_shorthand_default_routes_get(app): def test_shorthand_default_routes_get():
app = Sanic("app")
@app.get("/get") @app.get("/get")
def handler(request): def handler(request):
return text("OK") 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" 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") bp = Blueprint("test_bp", url_prefix="/bp")
@app.get("/get", name="route_get") @app.get("/get", name="route_get")
@ -82,84 +100,106 @@ def test_shorthand_named_routes_get(app):
app.blueprint(bp) 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" assert app.url_for("route_get") == "/get"
with pytest.raises(URLBuildError): with pytest.raises(URLBuildError):
app.url_for("handler") 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" assert app.url_for("test_bp.route_bp") == "/bp/get"
with pytest.raises(URLBuildError): with pytest.raises(URLBuildError):
app.url_for("test_bp.handler2") 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") @app.post("/post", name="route_name")
def handler(request): def handler(request):
return text("OK") 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" assert app.url_for("route_name") == "/post"
with pytest.raises(URLBuildError): with pytest.raises(URLBuildError):
app.url_for("handler") 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") @app.put("/put", name="route_put")
def handler(request): def handler(request):
return text("OK") 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" assert app.url_for("route_put") == "/put"
with pytest.raises(URLBuildError): with pytest.raises(URLBuildError):
app.url_for("handler") 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") @app.delete("/delete", name="route_delete")
def handler(request): def handler(request):
return text("OK") 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" assert app.url_for("route_delete") == "/delete"
with pytest.raises(URLBuildError): with pytest.raises(URLBuildError):
app.url_for("handler") 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") @app.patch("/patch", name="route_patch")
def handler(request): def handler(request):
return text("OK") 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" assert app.url_for("route_patch") == "/patch"
with pytest.raises(URLBuildError): with pytest.raises(URLBuildError):
app.url_for("handler") 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") @app.head("/head", name="route_head")
def handler(request): def handler(request):
return text("OK") 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" assert app.url_for("route_head") == "/head"
with pytest.raises(URLBuildError): with pytest.raises(URLBuildError):
app.url_for("handler") 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") @app.options("/options", name="route_options")
def handler(request): def handler(request):
return text("OK") 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" assert app.url_for("route_options") == "/options"
with pytest.raises(URLBuildError): with pytest.raises(URLBuildError):
app.url_for("handler") app.url_for("handler")
def test_named_static_routes(app): def test_named_static_routes():
app = Sanic("app")
@app.route("/test", name="route_test") @app.route("/test", name="route_test")
async def handler1(request): async def handler1(request):
return text("OK1") return text("OK1")
@ -168,20 +208,21 @@ def test_named_static_routes(app):
async def handler2(request): async def handler2(request):
return text("OK2") return text("OK2")
assert app.router.routes_all["/test"].name == "route_test" assert app.router.routes_all[("test",)].name == "app.route_test"
assert app.router.routes_static["/test"].name == "route_test" assert app.router.routes_static[("test",)].name == "app.route_test"
assert app.url_for("route_test") == "/test" assert app.url_for("route_test") == "/test"
with pytest.raises(URLBuildError): with pytest.raises(URLBuildError):
app.url_for("handler1") app.url_for("handler1")
assert app.router.routes_all["/pizazz"].name == "route_pizazz" assert app.router.routes_all[("pizazz",)].name == "app.route_pizazz"
assert app.router.routes_static["/pizazz"].name == "route_pizazz" assert app.router.routes_static[("pizazz",)].name == "app.route_pizazz"
assert app.url_for("route_pizazz") == "/pizazz" assert app.url_for("route_pizazz") == "/pizazz"
with pytest.raises(URLBuildError): with pytest.raises(URLBuildError):
app.url_for("handler2") app.url_for("handler2")
def test_named_dynamic_route(app): def test_named_dynamic_route():
app = Sanic("app")
results = [] results = []
@app.route("/folder/<name>", name="route_dynamic") @app.route("/folder/<name>", name="route_dynamic")
@ -189,52 +230,83 @@ def test_named_dynamic_route(app):
results.append(name) results.append(name)
return text("OK") 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" assert app.url_for("route_dynamic", name="test") == "/folder/test"
with pytest.raises(URLBuildError): with pytest.raises(URLBuildError):
app.url_for("handler") 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") @app.route("/folder/<folder_id:[A-Za-z0-9]{0,4}>", name="route_re")
async def handler(request, folder_id): async def handler(request, folder_id):
return text("OK") return text("OK")
route = app.router.routes_all["/folder/<folder_id:[A-Za-z0-9]{0,4}>"] route = app.router.routes_all[
assert route.name == "route_re" (
"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" assert app.url_for("route_re", folder_id="test") == "/folder/test"
with pytest.raises(URLBuildError): with pytest.raises(URLBuildError):
app.url_for("handler") 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") @app.route("/<path:path>/info", name="route_dynamic_path")
async def handler(request, path): async def handler(request, path):
return text("OK") return text("OK")
route = app.router.routes_all["/<path:path>/info"] route = app.router.routes_all[
assert route.name == "route_dynamic_path" (
"<path:path>",
"info",
)
]
assert route.name == "app.route_dynamic_path"
assert app.url_for("route_dynamic_path", path="path/1") == "/path/1/info" assert app.url_for("route_dynamic_path", path="path/1") == "/path/1/info"
with pytest.raises(URLBuildError): with pytest.raises(URLBuildError):
app.url_for("handler") app.url_for("handler")
def test_dynamic_named_route_unhashable(app): def test_dynamic_named_route_unhashable():
app = Sanic("app")
@app.route( @app.route(
"/folder/<unhashable:[A-Za-z0-9/]+>/end/", name="route_unhashable" "/folder/<unhashable:[A-Za-z0-9/]+>/end/", name="route_unhashable"
) )
async def handler(request, unhashable): async def handler(request, unhashable):
return text("OK") return text("OK")
route = app.router.routes_all["/folder/<unhashable:[A-Za-z0-9/]+>/end/"] route = app.router.routes_all[
assert route.name == "route_unhashable" (
"folder",
"<unhashable:[A-Za-z0-9/]+>",
"end",
)
]
assert route.name == "app.route_unhashable"
url = app.url_for("route_unhashable", unhashable="test/asdf") url = app.url_for("route_unhashable", unhashable="test/asdf")
assert url == "/folder/test/asdf/end" assert url == "/folder/test/asdf/end"
with pytest.raises(URLBuildError): with pytest.raises(URLBuildError):
app.url_for("handler") app.url_for("handler")
def test_websocket_named_route(app): def test_websocket_named_route():
app = Sanic("app")
ev = asyncio.Event() ev = asyncio.Event()
@app.websocket("/ws", name="route_ws") @app.websocket("/ws", name="route_ws")
@ -242,26 +314,29 @@ def test_websocket_named_route(app):
assert ws.subprotocol is None assert ws.subprotocol is None
ev.set() 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" assert app.url_for("route_ws") == "/ws"
with pytest.raises(URLBuildError): with pytest.raises(URLBuildError):
app.url_for("handler") app.url_for("handler")
def test_websocket_named_route_with_subprotocols(app): def test_websocket_named_route_with_subprotocols():
app = Sanic("app")
results = [] results = []
@app.websocket("/ws", subprotocols=["foo", "bar"], name="route_ws") @app.websocket("/ws", subprotocols=["foo", "bar"], name="route_ws")
async def handler(request, ws): async def handler(request, ws):
results.append(ws.subprotocol) 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" assert app.url_for("route_ws") == "/ws"
with pytest.raises(URLBuildError): with pytest.raises(URLBuildError):
app.url_for("handler") app.url_for("handler")
def test_static_add_named_route(app): def test_static_add_named_route():
app = Sanic("app")
async def handler1(request): async def handler1(request):
return text("OK1") 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(handler1, "/test", name="route_test")
app.add_route(handler2, "/test2", name="route_test2") app.add_route(handler2, "/test2", name="route_test2")
assert app.router.routes_all["/test"].name == "route_test" assert app.router.routes_all[("test",)].name == "app.route_test"
assert app.router.routes_static["/test"].name == "route_test" assert app.router.routes_static[("test",)].name == "app.route_test"
assert app.url_for("route_test") == "/test" assert app.url_for("route_test") == "/test"
with pytest.raises(URLBuildError): with pytest.raises(URLBuildError):
app.url_for("handler1") app.url_for("handler1")
assert app.router.routes_all["/test2"].name == "route_test2" assert app.router.routes_all[("test2",)].name == "app.route_test2"
assert app.router.routes_static["/test2"].name == "route_test2" assert app.router.routes_static[("test2",)].name == "app.route_test2"
assert app.url_for("route_test2") == "/test2" assert app.url_for("route_test2") == "/test2"
with pytest.raises(URLBuildError): with pytest.raises(URLBuildError):
app.url_for("handler2") app.url_for("handler2")
def test_dynamic_add_named_route(app): def test_dynamic_add_named_route():
app = Sanic("app")
results = [] results = []
async def handler(request, name): async def handler(request, name):
@ -292,13 +368,17 @@ def test_dynamic_add_named_route(app):
return text("OK") return text("OK")
app.add_route(handler, "/folder/<name>", name="route_dynamic") 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" assert app.url_for("route_dynamic", name="test") == "/folder/test"
with pytest.raises(URLBuildError): with pytest.raises(URLBuildError):
app.url_for("handler") 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): async def handler(request, unhashable):
return text("OK") return text("OK")
@ -307,15 +387,23 @@ def test_dynamic_add_named_route_unhashable(app):
"/folder/<unhashable:[A-Za-z0-9/]+>/end/", "/folder/<unhashable:[A-Za-z0-9/]+>/end/",
name="route_unhashable", name="route_unhashable",
) )
route = app.router.routes_all["/folder/<unhashable:[A-Za-z0-9/]+>/end/"] route = app.router.routes_all[
assert route.name == "route_unhashable" (
"folder",
"<unhashable:[A-Za-z0-9/]+>",
"end",
)
]
assert route.name == "app.route_unhashable"
url = app.url_for("route_unhashable", unhashable="folder1") url = app.url_for("route_unhashable", unhashable="folder1")
assert url == "/folder/folder1/end" assert url == "/folder/folder1/end"
with pytest.raises(URLBuildError): with pytest.raises(URLBuildError):
app.url_for("handler") app.url_for("handler")
def test_overload_routes(app): def test_overload_routes():
app = Sanic("app")
@app.route("/overload", methods=["GET"], name="route_first") @app.route("/overload", methods=["GET"], name="route_first")
async def handler1(request): async def handler1(request):
return text("OK1") return text("OK1")
@ -342,7 +430,7 @@ def test_overload_routes(app):
request, response = app.test_client.put(app.url_for("route_second")) request, response = app.test_client.put(app.url_for("route_second"))
assert response.text == "OK2" 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): with pytest.raises(URLBuildError):
app.url_for("handler1") app.url_for("handler1")

View File

@ -13,7 +13,7 @@ def test_payload_too_large_from_error_handler(app):
def handler_exception(request, exception): def handler_exception(request, exception):
return text("Payload Too Large from error_handler.", 413) 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.status == 413
assert response.text == "Payload Too Large from error_handler." 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): async def handler2(request):
return text("OK") 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 response.status == 413
assert "Request header" in response.text assert "Request header" in response.text
@ -38,6 +38,6 @@ def test_payload_too_large_at_on_header_default(app):
return text("OK") return text("OK")
data = "a" * 1000 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 response.status == 413
assert "Request body" in response.text assert "Request body" in response.text

View File

@ -1,4 +1,4 @@
from urllib.parse import quote from urllib.parse import quote, unquote
import pytest import pytest
@ -109,7 +109,14 @@ def test_redirect_with_header_injection(redirect_app):
assert not response.text.startswith("test-body") 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): def test_redirect_with_params(app, test_str):
use_in_uri = quote(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): async def init_handler(request, test):
return redirect(f"/api/v2/test/{use_in_uri}/") 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): async def target_handler(request, test):
assert test == test_str assert test == test_str
return text("OK") 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}/") _, response = app.test_client.get(f"/api/v1/test/{use_in_uri}/")
assert response.status == 200 assert response.status == 200
assert response.content == b"OK" assert response.body == b"OK"

View File

@ -42,6 +42,8 @@ def write_app(filename, **runargs):
app = Sanic(__name__) app = Sanic(__name__)
app.route("/")(lambda x: x)
@app.listener("after_server_start") @app.listener("after_server_start")
def complete(*args): def complete(*args):
print("complete", os.getpid(), {text!r}) print("complete", os.getpid(), {text!r})

View File

@ -10,7 +10,6 @@ import pytest
from sanic_testing.testing import ( from sanic_testing.testing import (
ASGI_BASE_URL, ASGI_BASE_URL,
ASGI_HOST,
ASGI_PORT, ASGI_PORT,
HOST, HOST,
PORT, PORT,
@ -19,7 +18,7 @@ from sanic_testing.testing import (
from sanic import Blueprint, Sanic from sanic import Blueprint, Sanic
from sanic.exceptions import ServerError 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 from sanic.response import html, json, text
@ -35,7 +34,7 @@ def test_sync(app):
request, response = app.test_client.get("/") request, response = app.test_client.get("/")
assert response.text == "Hello" assert response.body == b"Hello"
@pytest.mark.asyncio @pytest.mark.asyncio
@ -46,7 +45,7 @@ async def test_sync_asgi(app):
request, response = await app.asgi_client.get("/") request, response = await app.asgi_client.get("/")
assert response.text == "Hello" assert response.body == b"Hello"
def test_ip(app): def test_ip(app):
@ -56,7 +55,7 @@ def test_ip(app):
request, response = app.test_client.get("/") request, response = app.test_client.get("/")
assert response.text == "127.0.0.1" assert response.body == b"127.0.0.1"
@pytest.mark.asyncio @pytest.mark.asyncio
@ -67,10 +66,12 @@ async def test_url_asgi(app):
request, response = await app.asgi_client.get("/") request, response = await app.asgi_client.get("/")
if response.text.endswith("/") and not ASGI_BASE_URL.endswith("/"): if response.body.decode().endswith("/") and not ASGI_BASE_URL.endswith(
response.text[:-1] == ASGI_BASE_URL "/"
):
response.body[:-1] == ASGI_BASE_URL.encode()
else: else:
assert response.text == ASGI_BASE_URL assert response.body == ASGI_BASE_URL.encode()
def test_text(app): def test_text(app):
@ -80,7 +81,7 @@ def test_text(app):
request, response = app.test_client.get("/") request, response = app.test_client.get("/")
assert response.text == "Hello" assert response.body == b"Hello"
def test_html(app): def test_html(app):
@ -109,13 +110,13 @@ def test_html(app):
request, response = app.test_client.get("/") request, response = app.test_client.get("/")
assert response.content_type == "text/html; charset=utf-8" 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") 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") 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 @pytest.mark.asyncio
@ -126,7 +127,7 @@ async def test_text_asgi(app):
request, response = await app.asgi_client.get("/") request, response = await app.asgi_client.get("/")
assert response.text == "Hello" assert response.body == b"Hello"
def test_headers(app): def test_headers(app):
@ -186,7 +187,7 @@ def test_invalid_response(app):
request, response = app.test_client.get("/") request, response = app.test_client.get("/")
assert response.status == 500 assert response.status == 500
assert response.text == "Internal Server Error." assert response.body == b"Internal Server Error."
@pytest.mark.asyncio @pytest.mark.asyncio
@ -201,7 +202,7 @@ async def test_invalid_response_asgi(app):
request, response = await app.asgi_client.get("/") request, response = await app.asgi_client.get("/")
assert response.status == 500 assert response.status == 500
assert response.text == "Internal Server Error." assert response.body == b"Internal Server Error."
def test_json(app): def test_json(app):
@ -224,7 +225,7 @@ async def test_json_asgi(app):
request, response = await app.asgi_client.get("/") request, response = await app.asgi_client.get("/")
results = json_loads(response.text) results = json_loads(response.body)
assert results.get("test") is True assert results.get("test") is True
@ -237,7 +238,7 @@ def test_empty_json(app):
request, response = app.test_client.get("/") request, response = app.test_client.get("/")
assert response.status == 200 assert response.status == 200
assert response.text == "null" assert response.body == b"null"
@pytest.mark.asyncio @pytest.mark.asyncio
@ -249,7 +250,7 @@ async def test_empty_json_asgi(app):
request, response = await app.asgi_client.get("/") request, response = await app.asgi_client.get("/")
assert response.status == 200 assert response.status == 200
assert response.text == "null" assert response.body == b"null"
def test_invalid_json(app): def test_invalid_json(app):
@ -423,12 +424,12 @@ def test_content_type(app):
request, response = app.test_client.get("/") request, response = app.test_client.get("/")
assert request.content_type == DEFAULT_HTTP_CONTENT_TYPE 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"} headers = {"content-type": "application/json"}
request, response = app.test_client.get("/", headers=headers) request, response = app.test_client.get("/", headers=headers)
assert request.content_type == "application/json" assert request.content_type == "application/json"
assert response.text == "application/json" assert response.body == b"application/json"
@pytest.mark.asyncio @pytest.mark.asyncio
@ -439,12 +440,12 @@ async def test_content_type_asgi(app):
request, response = await app.asgi_client.get("/") request, response = await app.asgi_client.get("/")
assert request.content_type == DEFAULT_HTTP_CONTENT_TYPE 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"} headers = {"content-type": "application/json"}
request, response = await app.asgi_client.get("/", headers=headers) request, response = await app.asgi_client.get("/", headers=headers)
assert request.content_type == "application/json" assert request.content_type == "application/json"
assert response.text == "application/json" assert response.body == b"application/json"
def test_standard_forwarded(app): def test_standard_forwarded(app):
@ -581,14 +582,15 @@ async def test_standard_forwarded_asgi(app):
"X-Scheme": "ws", "X-Scheme": "ws",
} }
request, response = await app.asgi_client.get("/", headers=headers) 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.remote_addr == "127.0.0.2"
assert request.scheme == "ws" assert request.scheme == "ws"
assert request.server_port == ASGI_PORT assert request.server_port == ASGI_PORT
app.config.FORWARDED_SECRET = "mySecret" app.config.FORWARDED_SECRET = "mySecret"
request, response = await app.asgi_client.get("/", headers=headers) request, response = await app.asgi_client.get("/", headers=headers)
assert response.json() == { assert response.json == {
"for": "[::2]", "for": "[::2]",
"proto": "https", "proto": "https",
"host": "me.tld", "host": "me.tld",
@ -603,13 +605,13 @@ async def test_standard_forwarded_asgi(app):
# Empty Forwarded header -> use X-headers # Empty Forwarded header -> use X-headers
headers["Forwarded"] = "" headers["Forwarded"] = ""
request, response = await app.asgi_client.get("/", headers=headers) 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 # Header present but not matching anything
request, response = await app.asgi_client.get( request, response = await app.asgi_client.get(
"/", headers={"Forwarded": "."} "/", headers={"Forwarded": "."}
) )
assert response.json() == {} assert response.json == {}
# Forwarded header present but no matching secret -> use X-headers # Forwarded header present but no matching secret -> use X-headers
headers = { headers = {
@ -617,13 +619,13 @@ async def test_standard_forwarded_asgi(app):
"X-Real-IP": "127.0.0.2", "X-Real-IP": "127.0.0.2",
} }
request, response = await app.asgi_client.get("/", headers=headers) 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" assert request.remote_addr == "127.0.0.2"
# Different formatting and hitting both ends of the header # Different formatting and hitting both ends of the header
headers = {"Forwarded": 'Secret="mySecret";For=127.0.0.4;Port=1234'} headers = {"Forwarded": 'Secret="mySecret";For=127.0.0.4;Port=1234'}
request, response = await app.asgi_client.get("/", headers=headers) request, response = await app.asgi_client.get("/", headers=headers)
assert response.json() == { assert response.json == {
"for": "127.0.0.4", "for": "127.0.0.4",
"port": 1234, "port": 1234,
"secret": "mySecret", "secret": "mySecret",
@ -632,7 +634,7 @@ async def test_standard_forwarded_asgi(app):
# Test escapes (modify this if you see anyone implementing quoted-pairs) # Test escapes (modify this if you see anyone implementing quoted-pairs)
headers = {"Forwarded": 'for=test;quoted="\\,x=x;y=\\";secret=mySecret'} headers = {"Forwarded": 'for=test;quoted="\\,x=x;y=\\";secret=mySecret'}
request, response = await app.asgi_client.get("/", headers=headers) request, response = await app.asgi_client.get("/", headers=headers)
assert response.json() == { assert response.json == {
"for": "test", "for": "test",
"quoted": "\\,x=x;y=\\", "quoted": "\\,x=x;y=\\",
"secret": "mySecret", "secret": "mySecret",
@ -641,17 +643,17 @@ async def test_standard_forwarded_asgi(app):
# Secret insulated by malformed field #1 # Secret insulated by malformed field #1
headers = {"Forwarded": "for=test;secret=mySecret;b0rked;proto=wss;"} headers = {"Forwarded": "for=test;secret=mySecret;b0rked;proto=wss;"}
request, response = await app.asgi_client.get("/", headers=headers) 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 # Secret insulated by malformed field #2
headers = {"Forwarded": "for=test;b0rked;secret=mySecret;proto=wss"} headers = {"Forwarded": "for=test;b0rked;secret=mySecret;proto=wss"}
request, response = await app.asgi_client.get("/", headers=headers) 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 # Unexpected termination should not lose existing acceptable values
headers = {"Forwarded": "b0rked;secret=mySecret;proto=wss"} headers = {"Forwarded": "b0rked;secret=mySecret;proto=wss"}
request, response = await app.asgi_client.get("/", headers=headers) 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 # Field normalization
headers = { headers = {
@ -659,7 +661,7 @@ async def test_standard_forwarded_asgi(app):
'PATH="/With%20Spaces%22Quoted%22/sanicApp?key=val";SECRET=mySecret' 'PATH="/With%20Spaces%22Quoted%22/sanicApp?key=val";SECRET=mySecret'
} }
request, response = await app.asgi_client.get("/", headers=headers) request, response = await app.asgi_client.get("/", headers=headers)
assert response.json() == { assert response.json == {
"proto": "wss", "proto": "wss",
"by": "[cafe::8000]", "by": "[cafe::8000]",
"host": "a:2", "host": "a:2",
@ -671,7 +673,10 @@ async def test_standard_forwarded_asgi(app):
app.config.FORWARDED_SECRET = "_proxySecret" app.config.FORWARDED_SECRET = "_proxySecret"
headers = {"Forwarded": "for=1.2.3.4; by=_proxySecret"} headers = {"Forwarded": "for=1.2.3.4; by=_proxySecret"}
request, response = await app.asgi_client.get("/", headers=headers) 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): 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"} headers = {"X-Real-IP": "127.0.0.2", "X-Forwarded-For": "127.0.1.1"}
request, response = app.test_client.get("/", headers=headers) request, response = app.test_client.get("/", headers=headers)
assert request.remote_addr == "127.0.0.2" 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"} headers = {"X-Forwarded-For": "127.0.1.1"}
request, response = app.test_client.get("/", headers=headers) request, response = app.test_client.get("/", headers=headers)
assert request.remote_addr == "" assert request.remote_addr == ""
assert response.text == "" assert response.body == b""
headers = {"X-Forwarded-For": "127.0.0.1, 127.0.1.2"} headers = {"X-Forwarded-For": "127.0.0.1, 127.0.1.2"}
request, response = app.test_client.get("/", headers=headers) request, response = app.test_client.get("/", headers=headers)
assert request.remote_addr == "127.0.0.1" 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("/") request, response = app.test_client.get("/")
assert request.remote_addr == "" assert request.remote_addr == ""
assert response.text == "" assert response.body == b""
headers = {"X-Forwarded-For": "127.0.0.1, , ,,127.0.1.2"} headers = {"X-Forwarded-For": "127.0.0.1, , ,,127.0.1.2"}
request, response = app.test_client.get("/", headers=headers) request, response = app.test_client.get("/", headers=headers)
assert request.remote_addr == "127.0.0.1" assert request.remote_addr == "127.0.0.1"
assert response.text == "127.0.0.1" assert response.body == b"127.0.0.1"
headers = { headers = {
"X-Forwarded-For": ", 127.0.2.2, , ,127.0.0.1, , ,,127.0.1.2" "X-Forwarded-For": ", 127.0.2.2, , ,127.0.0.1, , ,,127.0.1.2"
} }
request, response = app.test_client.get("/", headers=headers) request, response = app.test_client.get("/", headers=headers)
assert request.remote_addr == "127.0.0.1" 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 @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"} headers = {"X-Real-IP": "127.0.0.2", "X-Forwarded-For": "127.0.1.1"}
request, response = await app.asgi_client.get("/", headers=headers) request, response = await app.asgi_client.get("/", headers=headers)
assert request.remote_addr == "127.0.0.2" 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"} headers = {"X-Forwarded-For": "127.0.1.1"}
request, response = await app.asgi_client.get("/", headers=headers) request, response = await app.asgi_client.get("/", headers=headers)
assert request.remote_addr == "" assert request.remote_addr == ""
assert response.text == "" assert response.body == b""
headers = {"X-Forwarded-For": "127.0.0.1, 127.0.1.2"} headers = {"X-Forwarded-For": "127.0.0.1, 127.0.1.2"}
request, response = await app.asgi_client.get("/", headers=headers) request, response = await app.asgi_client.get("/", headers=headers)
assert request.remote_addr == "127.0.0.1" 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("/") request, response = await app.asgi_client.get("/")
assert request.remote_addr == "" assert request.remote_addr == ""
assert response.text == "" assert response.body == b""
headers = {"X-Forwarded-For": "127.0.0.1, , ,,127.0.1.2"} headers = {"X-Forwarded-For": "127.0.0.1, , ,,127.0.1.2"}
request, response = await app.asgi_client.get("/", headers=headers) request, response = await app.asgi_client.get("/", headers=headers)
assert request.remote_addr == "127.0.0.1" assert request.remote_addr == "127.0.0.1"
assert response.text == "127.0.0.1" assert response.body == b"127.0.0.1"
headers = { headers = {
"X-Forwarded-For": ", 127.0.2.2, , ,127.0.0.1, , ,,127.0.1.2" "X-Forwarded-For": ", 127.0.2.2, , ,127.0.0.1, , ,,127.0.1.2"
} }
request, response = await app.asgi_client.get("/", headers=headers) request, response = await app.asgi_client.get("/", headers=headers)
assert request.remote_addr == "127.0.0.1" 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): 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"} headers = {"X-Real-IP": "127.0.0.2", "X-Forwarded-For": "127.0.1.1"}
request, response = app.test_client.get("/", headers=headers) request, response = app.test_client.get("/", headers=headers)
assert request.remote_addr == "" assert request.remote_addr == ""
assert response.text == "" assert response.body == b""
headers = {"X-Forwarded-For": "127.0.1.1"} headers = {"X-Forwarded-For": "127.0.1.1"}
request, response = app.test_client.get("/", headers=headers) request, response = app.test_client.get("/", headers=headers)
assert request.remote_addr == "" assert request.remote_addr == ""
assert response.text == "" assert response.body == b""
headers = {"X-Forwarded-For": "127.0.0.1, 127.0.1.2"} headers = {"X-Forwarded-For": "127.0.0.1, 127.0.1.2"}
request, response = app.test_client.get("/", headers=headers) request, response = app.test_client.get("/", headers=headers)
assert request.remote_addr == "" assert request.remote_addr == ""
assert response.text == "" assert response.body == b""
@pytest.mark.asyncio @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"} headers = {"X-Real-IP": "127.0.0.2", "X-Forwarded-For": "127.0.1.1"}
request, response = await app.asgi_client.get("/", headers=headers) request, response = await app.asgi_client.get("/", headers=headers)
assert request.remote_addr == "" assert request.remote_addr == ""
assert response.text == "" assert response.body == b""
headers = {"X-Forwarded-For": "127.0.1.1"} headers = {"X-Forwarded-For": "127.0.1.1"}
request, response = await app.asgi_client.get("/", headers=headers) request, response = await app.asgi_client.get("/", headers=headers)
assert request.remote_addr == "" assert request.remote_addr == ""
assert response.text == "" assert response.body == b""
headers = {"X-Forwarded-For": "127.0.0.1, 127.0.1.2"} headers = {"X-Forwarded-For": "127.0.0.1, 127.0.1.2"}
request, response = await app.asgi_client.get("/", headers=headers) request, response = await app.asgi_client.get("/", headers=headers)
assert request.remote_addr == "" assert request.remote_addr == ""
assert response.text == "" assert response.body == b""
def test_remote_addr_custom_headers(app): 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"} headers = {"X-Real-IP": "127.0.0.2", "Forwarded": "127.0.1.1"}
request, response = app.test_client.get("/", headers=headers) request, response = app.test_client.get("/", headers=headers)
assert request.remote_addr == "127.0.1.1" 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"} headers = {"X-Forwarded-For": "127.0.1.1"}
request, response = app.test_client.get("/", headers=headers) request, response = app.test_client.get("/", headers=headers)
assert request.remote_addr == "" assert request.remote_addr == ""
assert response.text == "" assert response.body == b""
headers = {"Client-IP": "127.0.0.2", "Forwarded": "127.0.1.1"} headers = {"Client-IP": "127.0.0.2", "Forwarded": "127.0.1.1"}
request, response = app.test_client.get("/", headers=headers) request, response = app.test_client.get("/", headers=headers)
assert request.remote_addr == "127.0.0.2" 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 @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"} headers = {"X-Real-IP": "127.0.0.2", "Forwarded": "127.0.1.1"}
request, response = await app.asgi_client.get("/", headers=headers) request, response = await app.asgi_client.get("/", headers=headers)
assert request.remote_addr == "127.0.1.1" 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"} headers = {"X-Forwarded-For": "127.0.1.1"}
request, response = await app.asgi_client.get("/", headers=headers) request, response = await app.asgi_client.get("/", headers=headers)
assert request.remote_addr == "" assert request.remote_addr == ""
assert response.text == "" assert response.body == b""
headers = {"Client-IP": "127.0.0.2", "Forwarded": "127.0.1.1"} headers = {"Client-IP": "127.0.0.2", "Forwarded": "127.0.1.1"}
request, response = await app.asgi_client.get("/", headers=headers) request, response = await app.asgi_client.get("/", headers=headers)
assert request.remote_addr == "127.0.0.2" 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): 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/") request, response = await app.asgi_client.get("/api/v1/user/sanic_user/")
assert request.match_info == {"user_id": "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"
assert request.json.get("test") == "OK" # for request.parsed_json assert request.json.get("test") == "OK" # for request.parsed_json
assert response.text == "OK" assert response.body == b"OK"
@pytest.mark.asyncio @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"
assert request.json.get("test") == "OK" # for request.parsed_json 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): def test_post_form_urlencoded(app):
@ -2136,7 +2141,7 @@ def test_safe_method_with_body_ignored(app):
assert request.body == b"" assert request.body == b""
assert request.json == None assert request.json == None
assert response.text == "OK" assert response.body == b"OK"
def test_safe_method_with_body(app): 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.body == data.encode("utf-8")
assert request.json.get("test") == "OK" assert request.json.get("test") == "OK"
assert response.text == "OK" assert response.body == b"OK"

View File

@ -14,6 +14,7 @@ import pytest
from aiofiles import os as async_os from aiofiles import os as async_os
from sanic_testing.testing import HOST, PORT from sanic_testing.testing import HOST, PORT
from sanic import Sanic
from sanic.response import ( from sanic.response import (
HTTPResponse, HTTPResponse,
StreamingHTTPResponse, StreamingHTTPResponse,
@ -51,16 +52,22 @@ async def sample_streaming_fn(response):
await response.write("bar") await response.write("bar")
def test_method_not_allowed(app): def test_method_not_allowed():
app = Sanic("app")
@app.get("/") @app.get("/")
async def test_get(request): async def test_get(request):
return response.json({"hello": "world"}) return response.json({"hello": "world"})
request, response = app.test_client.head("/") request, response = app.test_client.head("/")
assert response.headers["Allow"] == "GET" assert set(response.headers["Allow"].split(", ")) == {
"GET",
}
request, response = app.test_client.post("/") request, response = app.test_client.post("/")
assert response.headers["Allow"] == "GET" assert set(response.headers["Allow"].split(", ")) == {"GET", "HEAD"}
app.router.reset()
@app.post("/") @app.post("/")
async def test_post(request): async def test_post(request):
@ -68,12 +75,20 @@ def test_method_not_allowed(app):
request, response = app.test_client.head("/") request, response = app.test_client.head("/")
assert response.status == 405 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" assert response.headers["Content-Length"] == "0"
request, response = app.test_client.patch("/") request, response = app.test_client.patch("/")
assert response.status == 405 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" assert response.headers["Content-Length"] == "0"
@ -237,7 +252,7 @@ def test_chunked_streaming_returns_correct_content(streaming_app):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_chunked_streaming_returns_correct_content_asgi(streaming_app): async def test_chunked_streaming_returns_correct_content_asgi(streaming_app):
request, response = await streaming_app.asgi_client.get("/") 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): def test_non_chunked_streaming_adds_correct_headers(non_chunked_streaming_app):

View File

@ -1,18 +1,180 @@
import asyncio import asyncio
from unittest.mock import Mock
import pytest import pytest
from sanic_routing.exceptions import ParameterNameConflicts, RouteExists
from sanic_testing.testing import SanicTestClient from sanic_testing.testing import SanicTestClient
from sanic import Sanic from sanic import Blueprint, Sanic
from sanic.constants import HTTP_METHODS from sanic.constants import HTTP_METHODS
from sanic.exceptions import NotFound
from sanic.request import Request
from sanic.response import json, text from sanic.response import json, text
from sanic.router import ParameterNameConflicts, RouteDoesNotExist, RouteExists
# ------------------------------------------------------------ # @pytest.mark.parametrize(
# UTF-8 "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) @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): def test_route_strict_slashes_set_to_false_and_host_is_a_list(app):
# Part of regression test for issue #1120 # Part of regression test for issue #1120
test_client = SanicTestClient(app, port=42101) test_client = SanicTestClient(app, port=42101)
site1 = f"127.0.0.1:{test_client.port}" 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") request, response = test_client.get("http://" + site1 + "/get")
assert response.text == "OK" assert response.text == "OK"
app.router.finalized = False
@app.post("/post", host=[site1, "site2.com"], strict_slashes=False) @app.post("/post", host=[site1, "site2.com"], strict_slashes=False)
def post_handler(request): def post_handler(request):
return text("OK") 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") request, response = test_client.post("http://" + site1 + "/post")
assert response.text == "OK" assert response.text == "OK"
app.router.finalized = False
@app.put("/put", host=[site1, "site2.com"], strict_slashes=False) @app.put("/put", host=[site1, "site2.com"], strict_slashes=False)
def put_handler(request): def put_handler(request):
return text("OK") 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") request, response = test_client.put("http://" + site1 + "/put")
assert response.text == "OK" assert response.text == "OK"
app.router.finalized = False
@app.delete("/delete", host=[site1, "site2.com"], strict_slashes=False) @app.delete("/delete", host=[site1, "site2.com"], strict_slashes=False)
def delete_handler(request): def delete_handler(request):
return text("OK") return text("OK")
@ -294,6 +461,8 @@ def test_dynamic_route(app):
results.append(name) results.append(name)
return text("OK") return text("OK")
app.router.finalize(False)
request, response = app.test_client.get("/folder/test123") request, response = app.test_client.get("/folder/test123")
assert response.text == "OK" assert response.text == "OK"
@ -368,6 +537,9 @@ def test_dynamic_route_regex(app):
async def handler(request, folder_id): async def handler(request, folder_id):
return text("OK") return text("OK")
app.router.finalize()
print(app.router.find_route_src)
request, response = app.test_client.get("/folder/test") request, response = app.test_client.get("/folder/test")
assert response.status == 200 assert response.status == 200
@ -415,6 +587,8 @@ def test_dynamic_route_path(app):
request, response = app.test_client.get("/info") request, response = app.test_client.get("/info")
assert response.status == 404 assert response.status == 404
app.router.reset()
@app.route("/<path:path>") @app.route("/<path:path>")
async def handler1(request, path): async def handler1(request, path):
return text("OK") return text("OK")
@ -774,7 +948,7 @@ def test_removing_slash(app):
def post(_): def post(_):
pass pass
assert len(app.router.routes_all.keys()) == 2 assert len(app.router.routes_all.keys()) == 1
def test_overload_routes(app): def test_overload_routes(app):
@ -798,6 +972,7 @@ def test_overload_routes(app):
request, response = app.test_client.delete("/overload") request, response = app.test_client.delete("/overload")
assert response.status == 405 assert response.status == 405
app.router.reset()
with pytest.raises(RouteExists): with pytest.raises(RouteExists):
@app.route("/overload", methods=["PUT", "DELETE"]) @app.route("/overload", methods=["PUT", "DELETE"])
@ -810,11 +985,18 @@ def test_unmergeable_overload_routes(app):
async def handler1(request): async def handler1(request):
return text("OK1") 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"]) assert (
async def handler2(request): len(
return text("Duplicated") dict(list(app.router.static_routes.values())[0].handlers)[
"overload_whole"
]
)
== 3
)
request, response = app.test_client.get("/overload_whole") request, response = app.test_client.get("/overload_whole")
assert response.text == "OK1" assert response.text == "OK1"
@ -822,6 +1004,11 @@ def test_unmergeable_overload_routes(app):
request, response = app.test_client.post("/overload_whole") request, response = app.test_client.post("/overload_whole")
assert response.text == "OK1" 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"]) @app.route("/overload_part", methods=["GET"])
async def handler3(request): async def handler3(request):
return text("OK1") return text("OK1")
@ -847,7 +1034,9 @@ def test_unicode_routes(app):
request, response = app.test_client.get("/你好") request, response = app.test_client.get("/你好")
assert response.text == "OK1" 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): async def handler2(request, param):
return text("OK2 " + param) return text("OK2 " + param)
@ -865,20 +1054,38 @@ def test_uri_with_different_method_and_different_params(app):
return json({"action": action}) return json({"action": action})
request, response = app.test_client.get("/ads/1234") request, response = app.test_client.get("/ads/1234")
assert response.status == 200 assert response.status == 405
assert response.json == {"ad_id": "1234"}
request, response = app.test_client.post("/ads/post") request, response = app.test_client.post("/ads/post")
assert response.status == 200 assert response.status == 200
assert response.json == {"action": "post"} assert response.json == {"action": "post"}
def test_route_raise_ParameterNameConflicts(app): def test_uri_with_different_method_and_same_params(app):
with pytest.raises(ParameterNameConflicts): @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>/") @app.route("/ads/<ad_id>", methods=["POST"])
def handler(request, user): async def ad_post(request, ad_id):
return text("OK") 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): def test_route_invalid_host(app):

View File

@ -106,6 +106,7 @@ def test_static_file_bytes(app, static_file_directory, file_name):
[dict(), list(), object()], [dict(), list(), object()],
) )
def test_static_file_invalid_path(app, static_file_directory, file_name): def test_static_file_invalid_path(app, static_file_directory, file_name):
app.route("/")(lambda x: x)
with pytest.raises(ValueError): with pytest.raises(ValueError):
app.static("/testing.file", file_name) app.static("/testing.file", file_name)
request, response = app.test_client.get("/testing.file") request, response = app.test_client.get("/testing.file")

View File

@ -7,6 +7,7 @@ import pytest as pytest
from sanic_testing.testing import HOST as test_host from sanic_testing.testing import HOST as test_host
from sanic_testing.testing import PORT as test_port from sanic_testing.testing import PORT as test_port
from sanic import Sanic
from sanic.blueprints import Blueprint from sanic.blueprints import Blueprint
from sanic.exceptions import URLBuildError from sanic.exceptions import URLBuildError
from sanic.response import text from sanic.response import text
@ -98,36 +99,36 @@ def test_url_for_with_server_name(app):
assert response.text == "this should pass" 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") @app.route("/fail")
def fail(request): def fail(request):
return text("this should fail") return text("this should fail")
with pytest.raises(URLBuildError) as e: with pytest.raises(URLBuildError) as e:
app.url_for("passes") app.url_for("passes")
e.match("Endpoint with name `app.passes` was not found")
assert str(e.value) == "Endpoint with name `passes` was not found"
def test_fails_url_build_if_param_not_passed(app): def test_fails_url_build_if_param_not_passed(app):
url = "/" url = "/"
for letter in string.ascii_letters: for letter in string.ascii_lowercase:
url += f"<{letter}>/" url += f"<{letter}>/"
@app.route(url) @app.route(url)
def fail(request): def fail(request):
return text("this should fail") return text("this should fail")
fail_args = list(string.ascii_letters) fail_args = list(string.ascii_lowercase)
fail_args.pop() fail_args.pop()
fail_kwargs = {l: l for l in fail_args} fail_kwargs = {l: l for l in fail_args}
with pytest.raises(URLBuildError) as e: with pytest.raises(URLBuildError) as e:
app.url_for("fail", **fail_kwargs) app.url_for("fail", **fail_kwargs)
assert e.match("Required parameter `z` was not passed to url_for")
assert "Required parameter `Z` was not passed to url_for" in str(e.value)
def test_fails_url_build_if_params_not_passed(app): 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: with pytest.raises(ValueError) as e:
app.url_for("fail", _scheme="http") app.url_for("fail", _scheme="http")
assert e.match("When specifying _scheme, _external must be True")
assert str(e.value) == "When specifying _scheme, _external must be True"
COMPLEX_PARAM_URL = ( COMPLEX_PARAM_URL = (
@ -168,7 +168,7 @@ def test_fails_with_int_message(app):
expected_error = ( expected_error = (
r'Value "not_int" for parameter `foo` ' 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 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: with pytest.raises(URLBuildError) as e:
app.url_for("fail", **failing_kwargs) app.url_for("fail", **failing_kwargs)
e.match(
expected_error = ( 'Value "foobar" for parameter `two_letter_string` '
'Value "foobar" for parameter `two_letter_string` ' "does not satisfy pattern ^[A-z]{2}$"
"does not satisfy pattern [A-z]{2}" )
)
assert str(e.value) == expected_error
def test_fails_with_number_message(app): def test_fails_with_number_message(app):
@ -218,13 +215,10 @@ def test_fails_with_number_message(app):
with pytest.raises(URLBuildError) as e: with pytest.raises(URLBuildError) as e:
app.url_for("fail", **failing_kwargs) app.url_for("fail", **failing_kwargs)
e.match(
expected_error = ( 'Value "foo" for parameter `some_number` '
'Value "foo" for parameter `some_number` ' r"does not match pattern for type `float`: ^-?(?:\d+(?:\.\d*)?|\.\d+)$"
r"does not match pattern for type `float`: -?(?:\d+(?:\.\d*)?|\.\d+)" )
)
assert str(e.value) == expected_error
@pytest.mark.parametrize("number", [3, -3, 13.123, -13.123]) @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 @pytest.fixture
def blueprint_app(app): def blueprint_app():
app = Sanic("app")
first_print = Blueprint("first", url_prefix="/first") first_print = Blueprint("first", url_prefix="/first")
second_print = Blueprint("second", url_prefix="/second") second_print = Blueprint("second", url_prefix="/second")
@ -273,11 +268,11 @@ def blueprint_app(app):
return text(f"foo from first : {param}") return text(f"foo from first : {param}")
@second_print.route("/foo") # noqa @second_print.route("/foo") # noqa
def foo(request): def bar(request):
return text("foo from second") return text("foo from second")
@second_print.route("/foo/<param>") # noqa @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}") return text(f"foo from second : {param}")
app.blueprint(first_print) app.blueprint(first_print)
@ -290,7 +285,7 @@ def test_blueprints_are_named_correctly(blueprint_app):
first_url = blueprint_app.url_for("first.foo") first_url = blueprint_app.url_for("first.foo")
assert first_url == "/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" 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") first_url = blueprint_app.url_for("first.foo_with_param", param="bar")
assert first_url == "/first/foo/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" assert second_url == "/second/foo/bar"

View File

@ -1,18 +1,18 @@
import asyncio import asyncio
import pytest
from sanic_testing.testing import SanicTestClient from sanic_testing.testing import SanicTestClient
from sanic.blueprints import Blueprint from sanic.blueprints import Blueprint
def test_routes_with_host(app): def test_routes_with_host(app):
@app.route("/")
@app.route("/", name="hostindex", host="example.com") @app.route("/", name="hostindex", host="example.com")
@app.route("/path", name="hostpath", host="path.example.com") @app.route("/path", name="hostpath", host="path.example.com")
def index(request): def index(request):
pass pass
assert app.url_for("index") == "/"
assert app.url_for("hostindex") == "/" assert app.url_for("hostindex") == "/"
assert app.url_for("hostpath") == "/path" assert app.url_for("hostpath") == "/path"
assert app.url_for("hostindex", _external=True) == "http://example.com/" assert app.url_for("hostindex", _external=True) == "http://example.com/"
@ -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): def test_websocket_bp_route_name(app):
"""Tests that blueprint websocket route is named.""" """Tests that blueprint websocket route is named."""
event = asyncio.Event() event = asyncio.Event()
@ -63,3 +84,7 @@ def test_websocket_bp_route_name(app):
uri = app.url_for("test_bp.foobar_3") uri = app.url_for("test_bp.foobar_3")
assert uri == "/bp/route3" assert uri == "/bp/route3"
# TODO: add test with a route with multiple hosts
# TODO: add test with a route with _host in url_for

View File

@ -3,6 +3,7 @@ import os
import pytest import pytest
from sanic import Sanic
from sanic.blueprints import Blueprint from sanic.blueprints import Blueprint
@ -26,9 +27,15 @@ def get_file_content(static_file_directory, file_name):
@pytest.mark.parametrize( @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( app.static(
"/testing.file", get_file_path(static_file_directory, file_name) "/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", name="testing_file",
) )
app.router.finalize()
uri = app.url_for("static") uri = app.url_for("static")
uri2 = app.url_for("static", filename="any") uri2 = app.url_for("static", filename="any")
uri3 = app.url_for("static", name="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 uri == uri2
assert uri2 == uri3 assert uri2 == uri3
app.router.reset()
request, response = app.test_client.get(uri) request, response = app.test_client.get(uri)
assert response.status == 200 assert response.status == 200
assert response.body == get_file_content(static_file_directory, file_name) assert response.body == get_file_content(static_file_directory, file_name)
app.router.reset()
bp = Blueprint("test_bp_static", url_prefix="/bp") bp = Blueprint("test_bp_static", url_prefix="/bp")
bp.static("/testing.file", get_file_path(static_file_directory, file_name)) 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) app.blueprint(bp)
uri = app.url_for("static", name="test_bp_static.static") uris = [
uri2 = app.url_for("static", name="test_bp_static.static", filename="any") app.url_for("static", name="test_bp_static.static"),
uri3 = app.url_for("test_bp_static.static") app.url_for("static", name="test_bp_static.static", filename="any"),
uri4 = app.url_for("test_bp_static.static", name="any") app.url_for("test_bp_static.static"),
uri5 = app.url_for("test_bp_static.static", filename="any") app.url_for("test_bp_static.static", filename="any"),
uri6 = app.url_for("test_bp_static.static", name="any", filename="any") ]
assert uri == "/bp/testing.file" assert all(uri == "/bp/testing.file" for uri in uris)
assert uri == uri2
assert uri2 == uri3
assert uri3 == uri4
assert uri4 == uri5
assert uri5 == uri6
request, response = app.test_client.get(uri) request, response = app.test_client.get(uri)
assert response.status == 200 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("file_name", ["test.file", "decode me.txt"])
@pytest.mark.parametrize("base_uri", ["/static", "", "/dir"]) @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) app.static(base_uri, static_file_directory)
base_uri2 = base_uri + "/2" base_uri2 = base_uri + "/2"
app.static(base_uri2, static_file_directory, name="uploads") 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_uri, static_file_directory)
bp.static(base_uri2, static_file_directory, name="uploads") bp.static(base_uri2, static_file_directory, name="uploads")
app.router.reset()
app.blueprint(bp) app.blueprint(bp)
uri = app.url_for( 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"]) @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( app.static(
"/testing.file", "/testing.file",
get_file_path(static_file_directory, file_name), 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"]) @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( app.static(
"/testing.file", "/testing.file",
get_file_path(static_file_directory, file_name), 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" "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")
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) request, response = app.test_client.get(uri, headers=headers)
assert response.status == 206 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"]) @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( app.static(
"/testing.file", "/testing.file",
get_file_path(static_file_directory, file_name), 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" "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")
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", filename="any")
assert uri == app.url_for(
"test_bp_static.static", name="any", filename="any"
)
request, response = app.test_client.get(uri, headers=headers) request, response = app.test_client.get(uri, headers=headers)
assert response.status == 206 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"]) @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( app.static(
"/testing.file", "/testing.file",
get_file_path(static_file_directory, file_name), 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" "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")
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", filename="any")
assert uri == app.url_for(
"test_bp_static.static", name="any", filename="any"
)
request, response = app.test_client.get(uri, headers=headers) request, response = app.test_client.get(uri, headers=headers)
assert response.status == 206 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"]) @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( app.static(
"/testing.file", "/testing.file",
get_file_path(static_file_directory, file_name), 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" "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")
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", filename="any")
assert uri == app.url_for(
"test_bp_static.static", name="any", filename="any"
)
request, response = app.test_client.get(uri) request, response = app.test_client.get(uri)
assert response.status == 200 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"]) @pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"])
def test_static_content_range_error(app, file_name, static_file_directory): def test_static_content_range_error(app, file_name, static_file_directory):
app = Sanic("base")
app.static( app.static(
"/testing.file", "/testing.file",
get_file_path(static_file_directory, file_name), 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" "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")
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", filename="any")
assert uri == app.url_for(
"test_bp_static.static", name="any", filename="any"
)
request, response = app.test_client.get(uri, headers=headers) request, response = app.test_client.get(uri, headers=headers)
assert response.status == 416 assert response.status == 416

View File

@ -1,7 +1,14 @@
import pytest
from sanic_routing.exceptions import RouteExists
from sanic import Sanic
from sanic.response import text from sanic.response import text
def test_vhosts(app): def test_vhosts():
app = Sanic("app")
@app.route("/", host="example.com") @app.route("/", host="example.com")
async def handler1(request): async def handler1(request):
return text("You're at example.com!") return text("You're at example.com!")
@ -38,13 +45,12 @@ def test_vhosts_with_defaults(app):
async def handler1(request): async def handler1(request):
return text("Hello, world!") return text("Hello, world!")
@app.route("/") with pytest.raises(RouteExists):
async def handler2(request):
return text("default") @app.route("/")
async def handler2(request):
return text("default")
headers = {"Host": "hello.com"} headers = {"Host": "hello.com"}
request, response = app.test_client.get("/", headers=headers) request, response = app.test_client.get("/", headers=headers)
assert response.text == "Hello, world!" assert response.text == "Hello, world!"
request, response = app.test_client.get("/")
assert response.text == "default"

View File

@ -45,9 +45,9 @@ def test_unexisting_methods(app):
app.add_route(DummyView.as_view(), "/") app.add_route(DummyView.as_view(), "/")
request, response = app.test_client.get("/") 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("/") 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): 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"]: if method in ["GET", "POST", "PUT"]:
request, response = getattr(app.test_client, method.lower())("/") request, response = getattr(app.test_client, method.lower())("/")
assert response.status == 200
assert response.text == "first method" assert response.text == "first method"
response = view(request) # response = view(request)
assert response.body.decode() == "first method" # assert response.body.decode() == "first method"
if method in ["DELETE", "PATCH"]: # if method in ["DELETE", "PATCH"]:
request, response = getattr(app.test_client, method.lower())("/") # request, response = getattr(app.test_client, method.lower())("/")
assert response.text == "second method" # assert response.text == "second method"
response = view(request) # response = view(request)
assert response.body.decode() == "second method" # assert response.body.decode() == "second method"
@pytest.mark.parametrize("method", HTTP_METHODS) @pytest.mark.parametrize("method", HTTP_METHODS)

View File

@ -9,6 +9,8 @@ from unittest import mock
import pytest import pytest
from sanic_testing.testing import ASGI_PORT as PORT
from sanic.app import Sanic from sanic.app import Sanic
from sanic.worker import GunicornWorker from sanic.worker import GunicornWorker
@ -17,7 +19,7 @@ from sanic.worker import GunicornWorker
def gunicorn_worker(): def gunicorn_worker():
command = ( command = (
"gunicorn " "gunicorn "
"--bind 127.0.0.1:1337 " f"--bind 127.0.0.1:{PORT} "
"--worker-class sanic.worker.GunicornWorker " "--worker-class sanic.worker.GunicornWorker "
"examples.simple_server:app" "examples.simple_server:app"
) )
@ -31,7 +33,7 @@ def gunicorn_worker():
def gunicorn_worker_with_access_logs(): def gunicorn_worker_with_access_logs():
command = ( command = (
"gunicorn " "gunicorn "
"--bind 127.0.0.1:1338 " f"--bind 127.0.0.1:{PORT + 1} "
"--worker-class sanic.worker.GunicornWorker " "--worker-class sanic.worker.GunicornWorker "
"examples.simple_server:app" "examples.simple_server:app"
) )
@ -45,7 +47,7 @@ def gunicorn_worker_with_env_var():
command = ( command = (
'env SANIC_ACCESS_LOG="False" ' 'env SANIC_ACCESS_LOG="False" '
"gunicorn " "gunicorn "
"--bind 127.0.0.1:1339 " f"--bind 127.0.0.1:{PORT + 2} "
"--worker-class sanic.worker.GunicornWorker " "--worker-class sanic.worker.GunicornWorker "
"--log-level info " "--log-level info "
"examples.simple_server:app" "examples.simple_server:app"
@ -56,7 +58,7 @@ def gunicorn_worker_with_env_var():
def test_gunicorn_worker(gunicorn_worker): 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()) res = json.loads(f.read(100).decode())
assert res["test"] 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 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() gunicorn_worker_with_env_var.kill()
assert not gunicorn_worker_with_env_var.stdout.read() 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 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() gunicorn_worker_with_access_logs.kill()
assert ( assert (
b"(sanic.access)[INFO][127.0.0.1" b"(sanic.access)[INFO][127.0.0.1"

View File

@ -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_UJSON=1
{py36,py37,py38,py39,pyNightly}-no-ext: SANIC_NO_UVLOOP=1 {py36,py37,py38,py39,pyNightly}-no-ext: SANIC_NO_UVLOOP=1
deps = deps =
sanic-testing==0.1.2 sanic-testing
coverage==5.3 coverage==5.3
pytest==5.2.1 pytest==5.2.1
pytest-cov pytest-cov
@ -35,7 +35,7 @@ deps =
commands = commands =
flake8 sanic flake8 sanic
black --config ./.black.toml --check --verbose sanic/ black --config ./.black.toml --check --verbose sanic/
isort --check-only sanic isort --check-only sanic --profile=black
[testenv:type-checking] [testenv:type-checking]
deps = deps =