Add more documentationand type annotations

This commit is contained in:
Adam Hopkins 2021-01-31 12:30:37 +02:00
parent b958cdc151
commit 4358a7eefd
9 changed files with 396 additions and 110 deletions

View File

@ -30,93 +30,109 @@ sanic.compat
.. automodule:: sanic.compat .. automodule:: sanic.compat
:members: :members:
:show-inheritance:
sanic.config sanic.config
------------ ------------
.. automodule:: sanic.config .. automodule:: sanic.config
:members: :members:
:show-inheritance:
sanic.cookies sanic.cookies
------------- -------------
.. automodule:: sanic.cookies .. automodule:: sanic.cookies
:members: :members:
:show-inheritance:
sanic.errorpages sanic.errorpages
---------------- ----------------
.. automodule:: sanic.errorpages .. automodule:: sanic.errorpages
:members: :members:
:show-inheritance:
sanic.exceptions sanic.exceptions
---------------- ----------------
.. automodule:: sanic.exceptions .. automodule:: sanic.exceptions
:members: :members:
:show-inheritance:
sanic.handlers sanic.handlers
-------------- --------------
.. automodule:: sanic.handlers .. automodule:: sanic.handlers
:members: :members:
:show-inheritance:
sanic.http sanic.http
---------- ----------
.. automodule:: sanic.http .. automodule:: sanic.http
:members: :members:
:show-inheritance:
sanic.log sanic.log
--------- ---------
.. automodule:: sanic.log .. automodule:: sanic.log
:members: :members:
:show-inheritance:
sanic.request sanic.request
------------- -------------
.. automodule:: sanic.request .. automodule:: sanic.request
:members: :members:
:show-inheritance:
sanic.response sanic.response
-------------- --------------
.. automodule:: sanic.response .. automodule:: sanic.response
:members: :members:
:show-inheritance:
sanic.router sanic.router
------------ ------------
.. automodule:: sanic.router .. automodule:: sanic.router
:members: :members:
:show-inheritance:
sanic.server sanic.server
------------ ------------
.. automodule:: sanic.server .. automodule:: sanic.server
:members: :members:
:show-inheritance:
sanic.static sanic.static
------------ ------------
.. automodule:: sanic.static .. automodule:: sanic.static
:members: :members:
:show-inheritance:
sanic.views sanic.views
----------- -----------
.. automodule:: sanic.views .. automodule:: sanic.views
:members: :members:
:show-inheritance:
sanic.websocket sanic.websocket
--------------- ---------------
.. automodule:: sanic.websocket .. automodule:: sanic.websocket
:members: :members:
:show-inheritance:
sanic.worker sanic.worker
------------ ------------
.. automodule:: sanic.worker .. automodule:: sanic.worker
:members: :members:
:show-inheritance:

View File

@ -21,6 +21,7 @@ from typing import (
Optional, Optional,
Set, Set,
Type, Type,
TypeVar,
Union, Union,
) )
from urllib.parse import urlencode, urlunparse from urllib.parse import urlencode, urlunparse
@ -85,7 +86,7 @@ class Sanic(
router: Router = None, router: Router = None,
error_handler: ErrorHandler = None, error_handler: ErrorHandler = None,
load_env: bool = True, load_env: bool = True,
request_class: Request = None, request_class: Type[Request] = None,
strict_slashes: bool = False, strict_slashes: bool = False,
log_config: Optional[Dict[str, Any]] = None, log_config: Optional[Dict[str, Any]] = None,
configure_logging: bool = True, configure_logging: bool = True,
@ -163,7 +164,7 @@ class Sanic(
also return a future, and the actual ensure_future call also return a future, and the actual ensure_future call
is delayed until before server start. is delayed until before server start.
`See user guide <https://sanicframework.org/guide/basics/tasks.html#background-tasks>`__ `See user guide <https://sanicframework.org/guide/basics/tasks.html#background-tasks>`_
:param task: future, couroutine or awaitable :param task: future, couroutine or awaitable
""" """

View File

@ -14,13 +14,13 @@ class Header(CIMultiDict):
""" """
Container used for both request and response headers. It is a subclass of Container used for both request and response headers. It is a subclass of
`CIMultiDict `CIMultiDict
<https://multidict.readthedocs.io/en/stable/multidict.html#cimultidictproxy>`__. <https://multidict.readthedocs.io/en/stable/multidict.html#cimultidictproxy>`_.
It allows for multiple values for a single key in keeping with the HTTP It allows for multiple values for a single key in keeping with the HTTP
spec. Also, all keys are *case in-sensitive*. spec. Also, all keys are *case in-sensitive*.
Please checkout `the MultiDict documentation Please checkout `the MultiDict documentation
<https://multidict.readthedocs.io/en/stable/multidict.html#multidict>`__ <https://multidict.readthedocs.io/en/stable/multidict.html#multidict>`_
for more details about how to use the object. In general, it should work for more details about how to use the object. In general, it should work
very similar to a regular dictionary. very similar to a regular dictionary.
""" """

View File

@ -26,6 +26,7 @@ MiddlewareType = Union[RequestMiddlewareType, ResponseMiddlewareType]
ListenerType = Callable[ ListenerType = Callable[
[Sanic, AbstractEventLoop], Optional[Coroutine[Any, Any, None]] [Sanic, AbstractEventLoop], Optional[Coroutine[Any, Any, None]]
] ]
RouteHandler = Callable[..., Coroutine[Any, Any, HTTPResponse]]
class ErrorHandler: class ErrorHandler:

View File

@ -6,6 +6,9 @@ from urllib.parse import unquote
from sanic.helpers import STATUS_CODES from sanic.helpers import STATUS_CODES
# TODO:
# - the Options object should be a typed object to allow for less casting
# across the application (in request.py for example)
HeaderIterable = Iterable[Tuple[str, Any]] # Values convertible to str HeaderIterable = Iterable[Tuple[str, Any]] # Values convertible to str
HeaderBytesIterable = Iterable[Tuple[bytes, bytes]] HeaderBytesIterable = Iterable[Tuple[bytes, bytes]]
Options = Dict[str, Union[int, str]] # key=value fields in various headers Options = Dict[str, Union[int, str]] # key=value fields in various headers

View File

@ -1,22 +1,27 @@
from __future__ import annotations from __future__ import annotations
from typing import ( from typing import (
Optional,
TYPE_CHECKING, TYPE_CHECKING,
Any,
DefaultDict, DefaultDict,
Dict, Dict,
List, List,
NamedTuple, NamedTuple,
Optional,
Tuple, Tuple,
Union,
) )
if TYPE_CHECKING: if TYPE_CHECKING:
from sanic.server import ConnInfo from sanic.server import ConnInfo
from sanic.app import Sanic from sanic.app import Sanic
from sanic.http import Http
from asyncio.transports import BaseTransport
import email.utils import email.utils
import uuid import uuid
from asyncio.transports import BaseTransport
from collections import defaultdict from collections import defaultdict
from http.cookies import SimpleCookie from http.cookies import SimpleCookie
from types import SimpleNamespace from types import SimpleNamespace
@ -27,6 +32,7 @@ from httptools import parse_url # type: ignore
from sanic.compat import CancelledErrors, Header from sanic.compat import CancelledErrors, Header
from sanic.exceptions import InvalidUsage from sanic.exceptions import InvalidUsage
from sanic.headers import ( from sanic.headers import (
Options,
parse_content_header, parse_content_header,
parse_forwarded, parse_forwarded,
parse_host, parse_host,
@ -49,16 +55,21 @@ DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream"
class RequestParameters(dict): class RequestParameters(dict):
"""Hosts a dict with lists as values where get returns the first """
Hosts a dict with lists as values where get returns the first
value of the list and getlist returns the whole shebang value of the list and getlist returns the whole shebang
""" """
def get(self, name, default=None): def get(self, name: str, default: Optional[Any] = None) -> Optional[Any]:
"""Return the first value, either the default or actual""" """Return the first value, either the default or actual"""
return super().get(name, [default])[0] return super().get(name, [default])[0]
def getlist(self, name, default=None): def getlist(
"""Return the entire list""" self, name: str, default: Optional[Any] = None
) -> Optional[Any]:
"""
Return the entire list
"""
return super().get(name, default) return super().get(name, default)
@ -110,7 +121,7 @@ class Request:
self.raw_url = url_bytes self.raw_url = url_bytes
# TODO: Content-Encoding detection # TODO: Content-Encoding detection
self._parsed_url = parse_url(url_bytes) self._parsed_url = parse_url(url_bytes)
self._id = None self._id: Optional[Union[uuid.UUID, str, int]] = None
self.app = app self.app = app
self.headers = headers self.headers = headers
@ -123,7 +134,7 @@ class Request:
self.conn_info: Optional[ConnInfo] = None self.conn_info: Optional[ConnInfo] = None
self.ctx = SimpleNamespace() self.ctx = SimpleNamespace()
self.name: Optional[str] = None self.name: Optional[str] = None
self.parsed_forwarded = None self.parsed_forwarded: Optional[Options] = None
self.parsed_json = None self.parsed_json = None
self.parsed_form = None self.parsed_form = None
self.parsed_files = None self.parsed_files = None
@ -136,7 +147,7 @@ class Request:
self.uri_template = None self.uri_template = None
self.request_middleware_started = False self.request_middleware_started = False
self._cookies: Dict[str, str] = {} self._cookies: Dict[str, str] = {}
self.stream = None self.stream: Optional[Http] = None
self.endpoint = None self.endpoint = None
def __repr__(self): def __repr__(self):
@ -148,24 +159,30 @@ class Request:
return uuid.uuid4() return uuid.uuid4()
async def respond( async def respond(
self, response=None, *, status=200, headers=None, content_type=None self,
response: Optional[BaseHTTPResponse] = None,
*,
status: int = 200,
headers: Optional[Union[Header, Dict[str, str]]] = None,
content_type: Optional[str] = None,
): ):
# This logic of determining which response to use is subject to change # This logic of determining which response to use is subject to change
if response is None: if response is None:
response = self.stream.response or HTTPResponse( response = (self.stream and self.stream.response) or HTTPResponse(
status=status, status=status,
headers=headers, headers=headers,
content_type=content_type, content_type=content_type,
) )
# Connect the response # Connect the response
if isinstance(response, BaseHTTPResponse): if isinstance(response, BaseHTTPResponse) and self.stream:
response = self.stream.respond(response) response = self.stream.respond(response)
# Run response middleware # Run response middleware
try: try:
response = await self.app._run_response_middleware( response = await self.app._run_response_middleware(
self, response, request_name=self.name self, response, request_name=self.name
) )
except CancelledErrors: # Redefining this as a tuple here satisfies mypy
except tuple(CancelledErrors):
raise raise
except Exception: except Exception:
error_logger.exception( error_logger.exception(
@ -186,11 +203,35 @@ class Request:
self.body = b"".join([data async for data in self.stream]) self.body = b"".join([data async for data in self.stream])
@property @property
def id(self): def id(self) -> Optional[Union[uuid.UUID, str, int]]:
"""
A request ID passed from the client, or generated from the backend.
By default, this will look in a request header defined at:
``self.app.config.REQUEST_ID_HEADER``. It defaults to
``X-Request-ID``. Sanic will try to cast the ID into a ``UUID`` or an
``int``. If there is not a UUID from the client, then Sanic will try
to generate an ID by calling ``Request.generate_id()``. The default
behavior is to generate a ``UUID``. You can customize this behavior
by subclassing ``Request``.
.. code-block:: python
from sanic import Request, Sanic
from itertools import count
class IntRequest(Request):
counter = count()
def generate_id(self):
return next(self.counter)
app = Sanic("MyApp", request_class=IntRequest)
"""
if not self._id: if not self._id:
self._id = self.headers.get( self._id = self.headers.get(
self.app.config.REQUEST_ID_HEADER, self.app.config.REQUEST_ID_HEADER,
self.__class__.generate_id(self), self.__class__.generate_id(self), # type: ignore
) )
# Try casting to a UUID or an integer # Try casting to a UUID or an integer
@ -199,11 +240,11 @@ class Request:
self._id = uuid.UUID(self._id) self._id = uuid.UUID(self._id)
except ValueError: except ValueError:
try: try:
self._id = int(self._id) self._id = int(self._id) # type: ignore
except ValueError: except ValueError:
... ...
return self._id return self._id # type: ignore
@property @property
def json(self): def json(self):
@ -378,9 +419,17 @@ class Request:
] ]
query_args = property(get_query_args) query_args = property(get_query_args)
"""
Convenience property to access :meth:`Request.get_query_args` with
default values.
"""
@property @property
def cookies(self) -> Dict[str, str]: def cookies(self) -> Dict[str, str]:
"""
:return: Incoming cookies on the request
:rtype: Dict[str, str]
"""
if self._cookies is None: if self._cookies is None:
cookie = self.headers.get("Cookie") cookie = self.headers.get("Cookie")
if cookie is not None: if cookie is not None:
@ -432,13 +481,16 @@ class Request:
@property @property
def path(self) -> str: def path(self) -> str:
"""Path of the local HTTP request.""" """
:return: path of the local HTTP request
:rtype: str
"""
return self._parsed_url.path.decode("utf-8") return self._parsed_url.path.decode("utf-8")
# Proxy properties (using SERVER_NAME/forwarded/request/transport info) # Proxy properties (using SERVER_NAME/forwarded/request/transport info)
@property @property
def forwarded(self): def forwarded(self) -> Options:
""" """
Active proxy information obtained from request headers, as specified in Active proxy information obtained from request headers, as specified in
Sanic configuration. Sanic configuration.
@ -449,6 +501,9 @@ class Request:
- path is url-unencoded - path is url-unencoded
Additional values may be available from new style Forwarded headers. Additional values may be available from new style Forwarded headers.
:return: forwarded address info
:rtype: Dict[str, str]
""" """
if self.parsed_forwarded is None: if self.parsed_forwarded is None:
self.parsed_forwarded = ( self.parsed_forwarded = (
@ -464,10 +519,14 @@ class Request:
Client IP address, if available. Client IP address, if available.
1. proxied remote address `self.forwarded['for']` 1. proxied remote address `self.forwarded['for']`
2. local remote address `self.ip` 2. local remote address `self.ip`
:return: IPv4, bracketed IPv6, UNIX socket name or arbitrary string :return: IPv4, bracketed IPv6, UNIX socket name or arbitrary string
:rtype: str
""" """
if not hasattr(self, "_remote_addr"): if not hasattr(self, "_remote_addr"):
self._remote_addr = self.forwarded.get("for", "") # or self.ip self._remote_addr = str(
self.forwarded.get("for", "")
) # or self.ip
return self._remote_addr return self._remote_addr
@property @property
@ -477,12 +536,14 @@ class Request:
1. `config.SERVER_NAME` if in full URL format 1. `config.SERVER_NAME` if in full URL format
2. proxied proto/scheme 2. proxied proto/scheme
3. local connection protocol 3. local connection protocol
:return: http|https|ws|wss or arbitrary value given by the headers. :return: http|https|ws|wss or arbitrary value given by the headers.
:rtype: str
""" """
if "//" in self.app.config.get("SERVER_NAME", ""): if "//" in self.app.config.get("SERVER_NAME", ""):
return self.app.config.SERVER_NAME.split("//")[0] return self.app.config.SERVER_NAME.split("//")[0]
if "proto" in self.forwarded: if "proto" in self.forwarded:
return self.forwarded["proto"] return str(self.forwarded["proto"])
if ( if (
self.app.websocket_enabled self.app.websocket_enabled
@ -506,17 +567,20 @@ class Request:
3. request host header 3. request host header
hostname and port may be separated by hostname and port may be separated by
`sanic.headers.parse_host(request.host)`. `sanic.headers.parse_host(request.host)`.
:return: the first matching host found, or empty string :return: the first matching host found, or empty string
:rtype: str
""" """
server_name = self.app.config.get("SERVER_NAME") server_name = self.app.config.get("SERVER_NAME")
if server_name: if server_name:
return server_name.split("//", 1)[-1].split("/", 1)[0] return server_name.split("//", 1)[-1].split("/", 1)[0]
return self.forwarded.get("host") or self.headers.get("host", "") return str(self.forwarded.get("host") or self.headers.get("host", ""))
@property @property
def server_name(self) -> str: def server_name(self) -> str:
""" """
The hostname the client connected to, by ``request.host``. :return: hostname the client connected to, by ``request.host``
:rtype: str
""" """
return parse_host(self.host)[0] or "" return parse_host(self.host)[0] or ""
@ -527,21 +591,26 @@ class Request:
``request.host``. ``request.host``.
Default port is returned as 80 and 443 based on ``request.scheme``. Default port is returned as 80 and 443 based on ``request.scheme``.
:return: port number
:rtype: int
""" """
port = self.forwarded.get("port") or parse_host(self.host)[1] port = self.forwarded.get("port") or parse_host(self.host)[1]
return port or (80 if self.scheme in ("http", "ws") else 443) return int(port or (80 if self.scheme in ("http", "ws") else 443))
@property @property
def server_path(self) -> str: def server_path(self) -> str:
""" """
Full path of current URL. Uses proxied or local path. :return: full path of current URL; uses proxied or local path
:rtype: str
""" """
return self.forwarded.get("path") or self.path return str(self.forwarded.get("path") or self.path)
@property @property
def query_string(self) -> str: def query_string(self) -> str:
""" """
Representation of the requested query :return: representation of the requested query
:rtype: str
""" """
if self._parsed_url.query: if self._parsed_url.query:
return self._parsed_url.query.decode("utf-8") return self._parsed_url.query.decode("utf-8")
@ -551,7 +620,8 @@ class Request:
@property @property
def url(self) -> str: def url(self) -> str:
""" """
The URL :return: the URL
:rtype: str
""" """
return urlunparse( return urlunparse(
(self.scheme, self.host, self.path, None, self.query_string, None) (self.scheme, self.host, self.path, None, self.query_string, None)
@ -592,7 +662,8 @@ class Request:
class File(NamedTuple): class File(NamedTuple):
""" """
Model for defining a file Model for defining a file. It is a ``namedtuple``, therefore you can
iterate over the object, or access the parameters by name.
:param type: The mimetype, defaults to text/plain :param type: The mimetype, defaults to text/plain
:param body: Bytes of the file :param body: Bytes of the file

View File

@ -1,10 +1,23 @@
from functools import partial from functools import partial
from mimetypes import guess_type from mimetypes import guess_type
from os import path from os import path
from typing import Optional from pathlib import PurePath
from typing import (
Any,
AnyStr,
Callable,
Coroutine,
Dict,
Iterator,
Optional,
Tuple,
Union,
)
from urllib.parse import quote_plus from urllib.parse import quote_plus
from warnings import warn from warnings import warn
from typing_extensions import Protocol
from sanic.compat import Header, open_async from sanic.compat import Header, open_async
from sanic.cookies import CookieJar from sanic.cookies import CookieJar
from sanic.helpers import has_message_body, remove_entity_headers from sanic.helpers import has_message_body, remove_entity_headers
@ -21,29 +34,79 @@ except ImportError:
json_dumps = partial(dumps, separators=(",", ":")) json_dumps = partial(dumps, separators=(",", ":"))
class HTMLProtocol(Protocol):
def __html__(self) -> AnyStr:
...
def _repr_html_(self) -> AnyStr:
...
class Range(Protocol):
def start(self) -> int:
...
def end(self) -> int:
...
def size(self) -> int:
...
def total(self) -> int:
...
class BaseHTTPResponse: class BaseHTTPResponse:
"""
The base class for all HTTP Responses
"""
def __init__(self): def __init__(self):
self.asgi: bool = False self.asgi: bool = False
self.body: Optional[bytes] = None self.body: Optional[bytes] = None
self.content_type: Optional[str] = None
self.stream: Http = None self.stream: Http = None
self.status: int = None self.status: int = None
self.headers = Header({})
self._cookies: Optional[CookieJar] = None
def _encode_body(self, data): def _encode_body(self, data: Optional[AnyStr]):
if data is None: if data is None:
return b"" return b""
return data.encode() if hasattr(data, "encode") else data return (
data.encode() if hasattr(data, "encode") else data # type: ignore
)
@property @property
def cookies(self): def cookies(self) -> CookieJar:
"""
The response cookies. Cookies should be set and written as follows:
.. code-block:: python
response.cookies["test"] = "It worked!"
response.cookies["test"]["domain"] = ".yummy-yummy-cookie.com"
response.cookies["test"]["httponly"] = True
`See user guide
<https://sanicframework.org/guide/basics/cookies.html>`_
:return: the cookie jar
:rtype: CookieJar
"""
if self._cookies is None: if self._cookies is None:
self._cookies = CookieJar(self.headers) self._cookies = CookieJar(self.headers)
return self._cookies return self._cookies
@property @property
def processed_headers(self): def processed_headers(self) -> Iterator[Tuple[bytes, bytes]]:
"""Obtain a list of header tuples encoded in bytes for sending. """
Obtain a list of header tuples encoded in bytes for sending.
Add and remove headers based on status and content_type. Add and remove headers based on status and content_type.
:return: response headers
:rtype: Tuple[Tuple[bytes, bytes], ...]
""" """
# TODO: Make a blacklist set of header names and then filter with that # TODO: Make a blacklist set of header names and then filter with that
if self.status in (304, 412): # Not Modified, Precondition Failed if self.status in (304, 412): # Not Modified, Precondition Failed
@ -56,22 +119,66 @@ class BaseHTTPResponse:
for name, value in self.headers.items() for name, value in self.headers.items()
) )
async def send(self, data=None, end_stream=None): async def send(
"""Send any pending response headers and the given data as body. self,
data: Optional[Union[AnyStr]] = None,
end_stream: Optional[bool] = None,
) -> None:
"""
Send any pending response headers and the given data as body.
:param data: str or bytes to be written :param data: str or bytes to be written
:end_stream: whether to close the stream after this block :param end_stream: whether to close the stream after this block
""" """
if data is None and end_stream is None: if data is None and end_stream is None:
end_stream = True end_stream = True
if end_stream and not data and self.stream.send is None: if end_stream and not data and self.stream.send is None:
return return
data = data.encode() if hasattr(data, "encode") else data or b"" data = (
data.encode() # type: ignore
if hasattr(data, "encode")
else data or b""
)
await self.stream.send(data, end_stream=end_stream) await self.stream.send(data, end_stream=end_stream)
StreamingFunction = Callable[[BaseHTTPResponse], Coroutine[Any, Any, None]]
class StreamingHTTPResponse(BaseHTTPResponse): class StreamingHTTPResponse(BaseHTTPResponse):
"""Old style streaming response. Use `request.respond()` instead of this in """
new code to avoid the callback.""" Old style streaming response where you pass a streaming function:
.. code-block:: python
async def sample_streaming_fn(response):
await response.write("foo")
await asyncio.sleep(1)
await response.write("bar")
await asyncio.sleep(1)
@app.post("/")
async def test(request):
return stream(sample_streaming_fn)
.. warning::
**Deprecated** and set for removal in v21.6. You can now achieve the
same functionality without a callback.
.. code-block:: python
@app.post("/")
async def test(request):
response = await request.respond()
await response.send("foo", False)
await asyncio.sleep(1)
await response.send("bar", False)
await asyncio.sleep(1)
await response.send("", True)
return response
"""
__slots__ = ( __slots__ = (
"streaming_fn", "streaming_fn",
@ -83,10 +190,10 @@ class StreamingHTTPResponse(BaseHTTPResponse):
def __init__( def __init__(
self, self,
streaming_fn, streaming_fn: StreamingFunction,
status=200, status: int = 200,
headers=None, headers: Optional[Dict[str, str]] = None,
content_type="text/plain; charset=utf-8", content_type: str = "text/plain; charset=utf-8",
chunked="deprecated", chunked="deprecated",
): ):
if chunked != "deprecated": if chunked != "deprecated":
@ -118,25 +225,40 @@ class StreamingHTTPResponse(BaseHTTPResponse):
class HTTPResponse(BaseHTTPResponse): class HTTPResponse(BaseHTTPResponse):
"""
HTTP response to be sent back to the client.
:param body: the body content to be returned
:type body: Optional[bytes]
:param status: HTTP response number. **Default=200**
:type status: int
:param headers: headers to be returned
:type headers: Optional;
:param content_type: content type to be returned (as a header)
:type content_type: Optional[str]
"""
__slots__ = ("body", "status", "content_type", "headers", "_cookies") __slots__ = ("body", "status", "content_type", "headers", "_cookies")
def __init__( def __init__(
self, self,
body=None, body: Optional[AnyStr] = None,
status=200, status: int = 200,
headers=None, headers: Optional[Dict[str, str]] = None,
content_type=None, content_type: Optional[str] = None,
): ):
super().__init__() super().__init__()
self.content_type = content_type self.content_type: Optional[str] = content_type
self.body = self._encode_body(body) self.body = self._encode_body(body)
self.status = status self.status = status
self.headers = Header(headers or {}) self.headers = Header(headers or {})
self._cookies = None self._cookies = None
def empty(status=204, headers=None): def empty(
status=204, headers: Optional[Dict[str, str]] = None
) -> HTTPResponse:
""" """
Returns an empty response to the client. Returns an empty response to the client.
@ -147,13 +269,13 @@ def empty(status=204, headers=None):
def json( def json(
body, body: Any,
status=200, status: int = 200,
headers=None, headers: Optional[Dict[str, str]] = None,
content_type="application/json", content_type: str = "application/json",
dumps=json_dumps, dumps: Callable[..., str] = json_dumps,
**kwargs, **kwargs,
): ) -> HTTPResponse:
""" """
Returns response object with body in json format. Returns response object with body in json format.
@ -171,8 +293,11 @@ def json(
def text( def text(
body, status=200, headers=None, content_type="text/plain; charset=utf-8" body: str,
): status: int = 200,
headers: Optional[Dict[str, str]] = None,
content_type: str = "text/plain; charset=utf-8",
) -> HTTPResponse:
""" """
Returns response object with body in text format. Returns response object with body in text format.
@ -192,8 +317,11 @@ def text(
def raw( def raw(
body, status=200, headers=None, content_type="application/octet-stream" body: Optional[AnyStr],
): status: int = 200,
headers: Optional[Dict[str, str]] = None,
content_type: str = "application/octet-stream",
) -> HTTPResponse:
""" """
Returns response object without encoding the body. Returns response object without encoding the body.
@ -210,7 +338,11 @@ def raw(
) )
def html(body, status=200, headers=None): def html(
body: Union[str, bytes, HTMLProtocol],
status: int = 200,
headers: Optional[Dict[str, str]] = None,
) -> HTTPResponse:
""" """
Returns response object with body in html format. Returns response object with body in html format.
@ -218,11 +350,13 @@ def html(body, status=200, headers=None):
:param status: Response code. :param status: Response code.
:param headers: Custom Headers. :param headers: Custom Headers.
""" """
if hasattr(body, "__html__"): if not isinstance(body, (str, bytes)):
body = body.__html__() if hasattr(body, "__html__"):
elif hasattr(body, "_repr_html_"): body = body.__html__()
body = body._repr_html_() elif hasattr(body, "_repr_html_"):
return HTTPResponse( body = body._repr_html_()
return HTTPResponse( # type: ignore
body, body,
status=status, status=status,
headers=headers, headers=headers,
@ -231,13 +365,13 @@ def html(body, status=200, headers=None):
async def file( async def file(
location, location: Union[str, PurePath],
status=200, status: int = 200,
mime_type=None, mime_type: Optional[str] = None,
headers=None, headers: Optional[Dict[str, str]] = None,
filename=None, filename: Optional[str] = None,
_range=None, _range: Optional[Range] = None,
): ) -> HTTPResponse:
"""Return a response object with file data. """Return a response object with file data.
:param location: Location of file on system. :param location: Location of file on system.
@ -274,15 +408,15 @@ async def file(
async def file_stream( async def file_stream(
location, location: Union[str, PurePath],
status=200, status: int = 200,
chunk_size=4096, chunk_size: int = 4096,
mime_type=None, mime_type: Optional[str] = None,
headers=None, headers: Optional[Dict[str, str]] = None,
filename=None, filename: Optional[str] = None,
chunked="deprecated", chunked="deprecated",
_range=None, _range: Optional[Range] = None,
): ) -> StreamingHTTPResponse:
"""Return a streaming response object with file data. """Return a streaming response object with file data.
:param location: Location of file on system. :param location: Location of file on system.
@ -341,10 +475,10 @@ async def file_stream(
def stream( def stream(
streaming_fn, streaming_fn: StreamingFunction,
status=200, status: int = 200,
headers=None, headers: Optional[Dict[str, str]] = None,
content_type="text/plain; charset=utf-8", content_type: str = "text/plain; charset=utf-8",
chunked="deprecated", chunked="deprecated",
): ):
"""Accepts an coroutine `streaming_fn` which can be used to """Accepts an coroutine `streaming_fn` which can be used to
@ -381,15 +515,19 @@ def stream(
def redirect( def redirect(
to, headers=None, status=302, content_type="text/html; charset=utf-8" to: str,
): headers: Optional[Dict[str, str]] = None,
"""Abort execution and cause a 302 redirect (by default). status: int = 302,
content_type: str = "text/html; charset=utf-8",
) -> HTTPResponse:
"""
Abort execution and cause a 302 redirect (by default) by setting a
Location header.
:param to: path or fully qualified URL to redirect to :param to: path or fully qualified URL to redirect to
:param headers: optional dict of headers to include in the new request :param headers: optional dict of headers to include in the new request
:param status: status code (int) of the new request, defaults to 302 :param status: status code (int) of the new request, defaults to 302
:param content_type: the content type (string) of the response :param content_type: the content type (string) of the response
:returns: the redirecting Response
""" """
headers = headers or {} headers = headers or {}

View File

@ -1,18 +1,46 @@
from functools import lru_cache from functools import lru_cache
from typing import Any, Dict, Iterable, Optional, Tuple, Union
from sanic_routing import BaseRouter from sanic_routing import BaseRouter
from sanic_routing.route import Route from sanic_routing.route import Route
from sanic.constants import HTTP_METHODS from sanic.constants import HTTP_METHODS
from sanic.handlers import RouteHandler
from sanic.request import Request from sanic.request import Request
class Router(BaseRouter): class Router(BaseRouter):
"""
The router implementation responsible for routing a :class:`Request` object
to the appropriate handler.
"""
DEFAULT_METHOD = "GET" DEFAULT_METHOD = "GET"
ALLOWED_METHODS = HTTP_METHODS ALLOWED_METHODS = HTTP_METHODS
@lru_cache @lru_cache
def get(self, request: Request): def get(
self, request: Request
) -> Tuple[
RouteHandler,
Tuple[Any, ...],
Dict[str, Any],
str,
str,
Optional[str],
bool,
]:
"""
Retrieve a `Route` object containg the details about how to handle
a response for a given request
:param request: the incoming request object
:type request: Request
:return: details needed for handling the request and returning the
correct response
:rtype: Tuple[ RouteHandler, Tuple[Any, ...], Dict[str, Any], str, str,
Optional[str], bool, ]
"""
route, handler, params = self.resolve( route, handler, params = self.resolve(
path=request.path, path=request.path,
method=request.method, method=request.method,
@ -34,16 +62,43 @@ class Router(BaseRouter):
def add( def add(
self, self,
uri, uri: str,
methods, methods: Iterable[str],
handler, handler: RouteHandler,
host=None, host: Optional[str] = None,
strict_slashes=False, strict_slashes: bool = False,
stream=False, stream: bool = False,
ignore_body=False, ignore_body: bool = False,
version=None, version: Union[str, float, int] = None,
name=None, name: Optional[str] = None,
) -> Route: ) -> Route:
"""
Add a handler to the router
:param uri: the path of the route
:type uri: str
:param methods: the types of HTTP methods that should be attached,
example: ``["GET", "POST", "OPTIONS"]``
:type methods: Iterable[str]
:param handler: the sync or async function to be executed
:type handler: RouteHandler
:param host: host that the route should be on, defaults to None
:type host: Optional[str], optional
:param strict_slashes: whether to apply strict slashes, defaults
to False
:type strict_slashes: bool, optional
:param stream: whether to stream the response, defaults to False
:type stream: bool, optional
:param ignore_body: whether the incoming request body should be read,
defaults to False
:type ignore_body: bool, optional
:param version: a version modifier for the uri, defaults to None
:type version: Union[str, float, int], optional
:param name: an identifying name of the route, defaults to None
:type name: Optional[str], optional
:return: the route object
:rtype: Route
"""
# TODO: Implement # TODO: Implement
# - host # - host
# - strict_slashes # - strict_slashes

View File

@ -1,19 +1,19 @@
from __future__ import annotations from __future__ import annotations
from typing import ( from typing import (
Optional,
TYPE_CHECKING, TYPE_CHECKING,
DefaultDict, DefaultDict,
Dict, Dict,
List, List,
NamedTuple, NamedTuple,
Optional,
Tuple, Tuple,
Dict,
Type, Type,
Union, Union,
) )
if TYPE_CHECKING: if TYPE_CHECKING:
from sanic.http import Http
from sanic.app import Sanic from sanic.app import Sanic
import asyncio import asyncio
@ -24,21 +24,22 @@ import socket
import stat import stat
import sys import sys
from sanic.http import Stage
from asyncio import CancelledError from asyncio import CancelledError
from asyncio.transports import BaseTransport
from functools import partial from functools import partial
from inspect import isawaitable from inspect import isawaitable
from ipaddress import ip_address from ipaddress import ip_address
from signal import SIG_IGN, SIGINT, SIGTERM, Signals from signal import SIG_IGN, SIGINT, SIGTERM, Signals
from signal import signal as signal_func from signal import signal as signal_func
from time import monotonic as current_time from time import monotonic as current_time
from sanic.request import Request
from sanic.compat import OS_IS_WINDOWS, ctrlc_workaround_for_windows from sanic.compat import OS_IS_WINDOWS, ctrlc_workaround_for_windows
from sanic.config import Config from sanic.config import Config
from sanic.exceptions import RequestTimeout, ServiceUnavailable from sanic.exceptions import RequestTimeout, ServiceUnavailable
from sanic.http import Http, Stage
from sanic.log import logger from sanic.log import logger
from asyncio.transports import BaseTransport from sanic.request import Request
try: try:
import uvloop # type: ignore import uvloop # type: ignore