Additonal annotations

This commit is contained in:
Adam Hopkins 2021-01-29 16:19:10 +02:00
parent 65b76f2762
commit b958cdc151
16 changed files with 394 additions and 119 deletions

View File

@ -70,9 +70,10 @@ endif
black: black:
black --config ./.black.toml sanic tests black --config ./.black.toml sanic tests
fix-import: black isort:
isort sanic tests isort sanic tests
pretty: black isort
docs-clean: docs-clean:
cd docs && make clean cd docs && make clean

View File

@ -156,7 +156,12 @@ epub_exclude_files = ["search.html"]
suppress_warnings = ["image.nonlocal_uri"] suppress_warnings = ["image.nonlocal_uri"]
autodoc_member_order = "bysource"
autodoc_typehints = "description"
autodoc_default_options = {
"member-order": "groupwise",
}
# app setup hook # app setup hook
def setup(app): def setup(app):

View File

@ -4,7 +4,7 @@ Guides
====== ======
.. toctree:: .. toctree::
:maxdepth: 3 :maxdepth: 4
sanic/api_reference sanic/api_reference

View File

@ -31,109 +31,92 @@ sanic.compat
.. automodule:: sanic.compat .. automodule:: sanic.compat
:members: :members:
sanic.config sanic.config
------------ ------------
.. automodule:: sanic.config .. automodule:: sanic.config
:members: :members:
:undoc-members:
sanic.constants
---------------
.. automodule:: sanic.constants
:members:
:undoc-members:
sanic.cookies sanic.cookies
------------- -------------
.. automodule:: sanic.cookies .. automodule:: sanic.cookies
:members: :members:
:undoc-members:
sanic.errorpages
----------------
.. automodule:: sanic.errorpages
:members:
sanic.exceptions sanic.exceptions
---------------- ----------------
.. automodule:: sanic.exceptions .. automodule:: sanic.exceptions
:members: :members:
:undoc-members:
sanic.handlers sanic.handlers
-------------- --------------
.. automodule:: sanic.handlers .. automodule:: sanic.handlers
:members: :members:
:undoc-members:
sanic.http
----------
.. automodule:: sanic.http
:members:
sanic.log sanic.log
--------- ---------
.. automodule:: sanic.log .. automodule:: sanic.log
:members: :members:
:undoc-members:
sanic.request sanic.request
------------- -------------
.. automodule:: sanic.request .. automodule:: sanic.request
:members: :members:
:undoc-members:
sanic.response sanic.response
-------------- --------------
.. automodule:: sanic.response .. automodule:: sanic.response
:members: :members:
:undoc-members:
sanic.router sanic.router
------------ ------------
.. automodule:: sanic.router .. automodule:: sanic.router
:members: :members:
:undoc-members:
sanic.server sanic.server
------------ ------------
.. automodule:: sanic.server .. automodule:: sanic.server
:members: :members:
:undoc-members:
sanic.static sanic.static
------------ ------------
.. automodule:: sanic.static .. automodule:: sanic.static
:members: :members:
:undoc-members:
sanic.views sanic.views
----------- -----------
.. automodule:: sanic.views .. automodule:: sanic.views
:members: :members:
:undoc-members:
sanic.websocket sanic.websocket
--------------- ---------------
.. automodule:: sanic.websocket .. automodule:: sanic.websocket
:members: :members:
:undoc-members:
sanic.worker sanic.worker
------------ ------------
.. automodule:: sanic.worker .. automodule:: sanic.worker
:members: :members:
:undoc-members:
Module contents
---------------
.. automodule:: sanic
:members:
:undoc-members:

View File

@ -1,6 +1,17 @@
from sanic.__version__ import __version__ from sanic.__version__ import __version__
from sanic.app import Sanic from sanic.app import Sanic
from sanic.blueprints import Blueprint from sanic.blueprints import Blueprint
from sanic.request import Request
from sanic.response import HTTPResponse, html, json, text
__all__ = ["Sanic", "Blueprint", "__version__"] __all__ = (
"__version__",
"Sanic",
"Blueprint",
"HTTPResponse",
"Request",
"html",
"json",
"text",
)

View File

@ -66,7 +66,16 @@ class Config(dict):
def load_environment_vars(self, prefix=SANIC_PREFIX): def load_environment_vars(self, prefix=SANIC_PREFIX):
""" """
Looks for prefixed environment variables and applies Looks for prefixed environment variables and applies
them to the configuration if present. them to the configuration if present. This is called automatically when
Sanic starts up to load environment variables into config.
It will automatically hyrdate the following types:
- ``int``
- ``float``
- ``bool``
Anything else will be imported as a ``str``.
""" """
for k, v in environ.items(): for k, v in environ.items():
if k.startswith(prefix): if k.startswith(prefix):
@ -86,7 +95,9 @@ class Config(dict):
""" """
Update app.config. Update app.config.
..note:: only upper case settings are considered. .. note::
Only upper case settings are considered
You can upload app config by providing path to py file You can upload app config by providing path to py file
holding settings. holding settings.
@ -102,7 +113,7 @@ class Config(dict):
config.update_config("${some}/py/file") config.update_config("${some}/py/file")
Yes you can put environment variable here, but they must be provided Yes you can put environment variable here, but they must be provided
in format: ${some_env_var}, and mark that $some_env_var is treated in format: ``${some_env_var}``, and mark that ``$some_env_var`` is treated
as plain string. as plain string.
You can upload app config by providing dict holding settings. You can upload app config by providing dict holding settings.
@ -122,6 +133,8 @@ class Config(dict):
B = 2 B = 2
config.update_config(C) config.update_config(C)
`See user guide <https://sanicframework.org/guide/deployment/configuration.html>`__
""" """
if isinstance(config, (bytes, str, Path)): if isinstance(config, (bytes, str, Path)):

View File

@ -2,6 +2,7 @@ import re
import string import string
from datetime import datetime from datetime import datetime
from typing import Dict
DEFAULT_MAX_AGE = 0 DEFAULT_MAX_AGE = 0
@ -41,16 +42,17 @@ _is_legal_key = re.compile("[%s]+" % re.escape(_LegalChars)).fullmatch
class CookieJar(dict): class CookieJar(dict):
"""CookieJar dynamically writes headers as cookies are added and removed """
CookieJar dynamically writes headers as cookies are added and removed
It gets around the limitation of one header per name by using the It gets around the limitation of one header per name by using the
MultiHeader class to provide a unique key that encodes to Set-Cookie. MultiHeader class to provide a unique key that encodes to Set-Cookie.
""" """
def __init__(self, headers): def __init__(self, headers):
super().__init__() super().__init__()
self.headers = headers self.headers: Dict[str, str] = headers
self.cookie_headers = {} self.cookie_headers: Dict[str, str] = {}
self.header_key = "Set-Cookie" self.header_key: str = "Set-Cookie"
def __setitem__(self, key, value): def __setitem__(self, key, value):
# If this cookie doesn't exist, add it to the header keys # If this cookie doesn't exist, add it to the header keys

View File

@ -1,3 +1,17 @@
"""
Sanic `provides a pattern <https://sanicframework.org/guide/best-practices/exceptions.html#using-sanic-exceptions>`_
for providing a response when an exception occurs. However, if you do no handle
an exception, it will provide a fallback. There are three fallback types:
- HTML - *default*
- Text
- JSON
Setting ``app.config.FALLBACK_ERROR_FORMAT = "auto"`` will enable a switch that
will attempt to provide an appropriate response format based upon the
request type.
"""
import sys import sys
import typing as t import typing as t
@ -26,6 +40,10 @@ FALLBACK_STATUS = 500
class BaseRenderer: class BaseRenderer:
"""
Base class that all renderers must inherit from.
"""
def __init__(self, request, exception, debug): def __init__(self, request, exception, debug):
self.request = request self.request = request
self.exception = exception self.exception = exception
@ -54,7 +72,13 @@ class BaseRenderer:
status_text = STATUS_CODES.get(self.status, b"Error Occurred").decode() status_text = STATUS_CODES.get(self.status, b"Error Occurred").decode()
return f"{self.status}{status_text}" return f"{self.status}{status_text}"
def render(self): def render(self) -> str:
"""
Outputs the exception as a ``str`` for response.
:return: The formatted exception
:rtype: str
"""
output = ( output = (
self.full self.full
if self.debug and not getattr(self.exception, "quiet", False) if self.debug and not getattr(self.exception, "quiet", False)
@ -62,14 +86,28 @@ class BaseRenderer:
) )
return output() return output()
def minimal(self): # noqa def minimal(self) -> str: # noqa
"""
Provide a formatted message that is meant to not show any sensitive
data or details.
"""
raise NotImplementedError raise NotImplementedError
def full(self): # noqa def full(self) -> str: # noqa
"""
Provide a formatted message that has all details and is mean to be used
primarily for debugging and non-production environments.
"""
raise NotImplementedError raise NotImplementedError
class HTMLRenderer(BaseRenderer): class HTMLRenderer(BaseRenderer):
"""
Render an exception as HTML.
The default fallback type.
"""
TRACEBACK_STYLE = """ TRACEBACK_STYLE = """
html { font-family: sans-serif } html { font-family: sans-serif }
h2 { color: #888; } h2 { color: #888; }
@ -172,6 +210,10 @@ class HTMLRenderer(BaseRenderer):
class TextRenderer(BaseRenderer): class TextRenderer(BaseRenderer):
"""
Render an exception as plain text.
"""
OUTPUT_TEXT = "{title}\n{bar}\n{text}\n\n{body}" OUTPUT_TEXT = "{title}\n{bar}\n{text}\n\n{body}"
SPACER = " " SPACER = " "
@ -231,6 +273,10 @@ class TextRenderer(BaseRenderer):
class JSONRenderer(BaseRenderer): class JSONRenderer(BaseRenderer):
"""
Render an exception as JSON.
"""
def full(self): def full(self):
output = self._generate_output(full=True) output = self._generate_output(full=True)
return json(output, status=self.status, dumps=dumps) return json(output, status=self.status, dumps=dumps)
@ -280,7 +326,9 @@ class JSONRenderer(BaseRenderer):
def escape(text): def escape(text):
"""Minimal HTML escaping, not for attribute values (unlike html.escape).""" """
Minimal HTML escaping, not for attribute values (unlike html.escape).
"""
return f"{text}".replace("&", "&amp;").replace("<", "&lt;") return f"{text}".replace("&", "&amp;").replace("<", "&lt;")
@ -303,7 +351,9 @@ def exception_response(
debug: bool, debug: bool,
renderer: t.Type[t.Optional[BaseRenderer]] = None, renderer: t.Type[t.Optional[BaseRenderer]] = None,
) -> HTTPResponse: ) -> HTTPResponse:
"""Render a response for the default FALLBACK exception handler""" """
Render a response for the default FALLBACK exception handler.
"""
if not renderer: if not renderer:
renderer = HTMLRenderer renderer = HTMLRenderer

View File

@ -1,3 +1,5 @@
from typing import Optional, Union
from sanic.helpers import STATUS_CODES from sanic.helpers import STATUS_CODES
@ -33,16 +35,28 @@ class SanicException(Exception):
@add_status_code(404) @add_status_code(404)
class NotFound(SanicException): class NotFound(SanicException):
"""
**Status**: 404 Not Found
"""
pass pass
@add_status_code(400) @add_status_code(400)
class InvalidUsage(SanicException): class InvalidUsage(SanicException):
"""
**Status**: 400 Bad Request
"""
pass pass
@add_status_code(405) @add_status_code(405)
class MethodNotSupported(SanicException): class MethodNotSupported(SanicException):
"""
**Status**: 405 Method Not Allowed
"""
def __init__(self, message, method, allowed_methods): def __init__(self, message, method, allowed_methods):
super().__init__(message) super().__init__(message)
self.headers = {"Allow": ", ".join(allowed_methods)} self.headers = {"Allow": ", ".join(allowed_methods)}
@ -50,22 +64,38 @@ class MethodNotSupported(SanicException):
@add_status_code(500) @add_status_code(500)
class ServerError(SanicException): class ServerError(SanicException):
"""
**Status**: 500 Internal Server Error
"""
pass pass
@add_status_code(503) @add_status_code(503)
class ServiceUnavailable(SanicException): class ServiceUnavailable(SanicException):
"""The server is currently unavailable (because it is overloaded or """
down for maintenance). Generally, this is a temporary state.""" **Status**: 503 Service Unavailable
The server is currently unavailable (because it is overloaded or
down for maintenance). Generally, this is a temporary state.
"""
pass pass
class URLBuildError(ServerError): class URLBuildError(ServerError):
"""
**Status**: 500 Internal Server Error
"""
pass pass
class FileNotFound(NotFound): class FileNotFound(NotFound):
"""
**Status**: 404 Not Found
"""
def __init__(self, message, path, relative_url): def __init__(self, message, path, relative_url):
super().__init__(message) super().__init__(message)
self.path = path self.path = path
@ -87,15 +117,27 @@ class RequestTimeout(SanicException):
@add_status_code(413) @add_status_code(413)
class PayloadTooLarge(SanicException): class PayloadTooLarge(SanicException):
"""
**Status**: 413 Payload Too Large
"""
pass pass
class HeaderNotFound(InvalidUsage): class HeaderNotFound(InvalidUsage):
"""
**Status**: 400 Bad Request
"""
pass pass
@add_status_code(416) @add_status_code(416)
class ContentRangeError(SanicException): class ContentRangeError(SanicException):
"""
**Status**: 416 Range Not Satisfiable
"""
def __init__(self, message, content_range): def __init__(self, message, content_range):
super().__init__(message) super().__init__(message)
self.headers = {"Content-Range": f"bytes */{content_range.total}"} self.headers = {"Content-Range": f"bytes */{content_range.total}"}
@ -103,15 +145,27 @@ class ContentRangeError(SanicException):
@add_status_code(417) @add_status_code(417)
class HeaderExpectationFailed(SanicException): class HeaderExpectationFailed(SanicException):
"""
**Status**: 417 Expectation Failed
"""
pass pass
@add_status_code(403) @add_status_code(403)
class Forbidden(SanicException): class Forbidden(SanicException):
"""
**Status**: 403 Forbidden
"""
pass pass
class InvalidRangeType(ContentRangeError): class InvalidRangeType(ContentRangeError):
"""
**Status**: 416 Range Not Satisfiable
"""
pass pass
@ -123,7 +177,7 @@ class PyFileError(Exception):
@add_status_code(401) @add_status_code(401)
class Unauthorized(SanicException): class Unauthorized(SanicException):
""" """
Unauthorized exception (401 HTTP status code). **Status**: 401 Unauthorized
:param message: Message describing the exception. :param message: Message describing the exception.
:param status_code: HTTP Status code. :param status_code: HTTP Status code.
@ -173,7 +227,7 @@ class LoadFileException(SanicException):
pass pass
def abort(status_code, message=None): def abort(status_code: int, message: Optional[Union[str, bytes]] = None):
""" """
Raise an exception based on SanicException. Returns the HTTP response Raise an exception based on SanicException. Returns the HTTP response
message appropriate for the given status code, unless provided. message appropriate for the given status code, unless provided.
@ -184,8 +238,8 @@ def abort(status_code, message=None):
:param message: The HTTP response body. Defaults to the messages in :param message: The HTTP response body. Defaults to the messages in
""" """
if message is None: if message is None:
message = STATUS_CODES.get(status_code) msg: bytes = STATUS_CODES[status_code]
# These are stored as bytes in the STATUS_CODES dict # These are stored as bytes in the STATUS_CODES dict
message = message.decode("utf8") message = msg.decode("utf8")
sanic_exception = _sanic_exceptions.get(status_code, SanicException) sanic_exception = _sanic_exceptions.get(status_code, SanicException)
raise sanic_exception(message=message, status_code=status_code) raise sanic_exception(message=message, status_code=status_code)

View File

@ -2,9 +2,10 @@
from importlib import import_module from importlib import import_module
from inspect import ismodule from inspect import ismodule
from typing import Dict
STATUS_CODES = { STATUS_CODES: Dict[int, bytes] = {
100: b"Continue", 100: b"Continue",
101: b"Switching Protocols", 101: b"Switching Protocols",
102: b"Processing", 102: b"Processing",

View File

@ -1,3 +1,13 @@
from __future__ import annotations
from typing import (
Optional,
TYPE_CHECKING,
)
if TYPE_CHECKING:
from sanic.request import Request
from sanic.response import BaseHTTPResponse
from asyncio import CancelledError, sleep from asyncio import CancelledError, sleep
from enum import Enum from enum import Enum
@ -15,6 +25,17 @@ from sanic.log import access_logger, logger
class Stage(Enum): class Stage(Enum):
"""
Enum for representing the stage of the request/response cycle
| ``IDLE`` Waiting for request
| ``REQUEST`` Request headers being received
| ``HANDLER`` Headers done, handler running
| ``RESPONSE`` Response headers sent, body in progress
| ``FAILED`` Unrecoverable state (error while sending response)
|
"""
IDLE = 0 # Waiting for request IDLE = 0 # Waiting for request
REQUEST = 1 # Request headers being received REQUEST = 1 # Request headers being received
HANDLER = 3 # Headers done, handler running HANDLER = 3 # Headers done, handler running
@ -26,6 +47,24 @@ HTTP_CONTINUE = b"HTTP/1.1 100 Continue\r\n\r\n"
class Http: class Http:
"""
Internal helper for managing the HTTP request/response cycle
:raises ServerError:
:raises PayloadTooLarge:
:raises Exception:
:raises InvalidUsage:
:raises HeaderExpectationFailed:
:raises RuntimeError:
:raises ServerError:
:raises ServerError:
:raises InvalidUsage:
:raises InvalidUsage:
:raises InvalidUsage:
:raises PayloadTooLarge:
:raises RuntimeError:
"""
__slots__ = [ __slots__ = [
"_send", "_send",
"_receive_more", "_receive_more",
@ -53,16 +92,16 @@ class Http:
self._receive_more = protocol.receive_more self._receive_more = protocol.receive_more
self.recv_buffer = protocol.recv_buffer self.recv_buffer = protocol.recv_buffer
self.protocol = protocol self.protocol = protocol
self.expecting_continue = False self.expecting_continue: bool = False
self.stage = Stage.IDLE self.stage: Stage = Stage.IDLE
self.request_body = None self.request_body = None
self.request_bytes = None self.request_bytes = None
self.request_bytes_left = None self.request_bytes_left = None
self.request_max_size = protocol.request_max_size self.request_max_size = protocol.request_max_size
self.keep_alive = True self.keep_alive = True
self.head_only = None self.head_only = None
self.request = None self.request: Request = None
self.response = None self.response: BaseHTTPResponse = None
self.exception = None self.exception = None
self.url = None self.url = None
self.upgrade_websocket = False self.upgrade_websocket = False
@ -72,7 +111,9 @@ class Http:
return self.stage in (Stage.HANDLER, Stage.RESPONSE) return self.stage in (Stage.HANDLER, Stage.RESPONSE)
async def http1(self): async def http1(self):
"""HTTP 1.1 connection handler""" """
HTTP 1.1 connection handler
"""
while True: # As long as connection stays keep-alive while True: # As long as connection stays keep-alive
try: try:
# Receive and handle a request # Receive and handle a request
@ -125,7 +166,9 @@ class Http:
await self._receive_more() await self._receive_more()
async def http1_request_header(self): async def http1_request_header(self):
"""Receive and parse request header into self.request.""" """
Receive and parse request header into self.request.
"""
HEADER_MAX_SIZE = min(8192, self.request_max_size) HEADER_MAX_SIZE = min(8192, self.request_max_size)
# Receive until full header is in buffer # Receive until full header is in buffer
buf = self.recv_buffer buf = self.recv_buffer
@ -214,7 +257,9 @@ class Http:
self.request, request.stream = request, self self.request, request.stream = request, self
self.protocol.state["requests_count"] += 1 self.protocol.state["requests_count"] += 1
async def http1_response_header(self, data, end_stream): async def http1_response_header(
self, data: bytes, end_stream: bool
) -> None:
res = self.response res = self.response
# Compatibility with simple response body # Compatibility with simple response body
@ -257,7 +302,7 @@ class Http:
else: else:
# Length not known, use chunked encoding # Length not known, use chunked encoding
headers["transfer-encoding"] = "chunked" headers["transfer-encoding"] = "chunked"
data = b"%x\r\n%b\r\n" % (size, data) if size else None data = b"%x\r\n%b\r\n" % (size, data) if size else b""
self.response_func = self.http1_response_chunked self.response_func = self.http1_response_chunked
if self.head_only: if self.head_only:
@ -283,14 +328,20 @@ class Http:
await self._send(ret) await self._send(ret)
self.stage = Stage.IDLE if end_stream else Stage.RESPONSE self.stage = Stage.IDLE if end_stream else Stage.RESPONSE
def head_response_ignored(self, data, end_stream): def head_response_ignored(self, data: bytes, end_stream: bool) -> None:
"""HEAD response: body data silently ignored.""" """
HEAD response: body data silently ignored.
"""
if end_stream: if end_stream:
self.response_func = None self.response_func = None
self.stage = Stage.IDLE self.stage = Stage.IDLE
async def http1_response_chunked(self, data, end_stream): async def http1_response_chunked(
"""Format a part of response body in chunked encoding.""" self, data: bytes, end_stream: bool
) -> None:
"""
Format a part of response body in chunked encoding.
"""
# Chunked encoding # Chunked encoding
size = len(data) size = len(data)
if end_stream: if end_stream:
@ -304,8 +355,12 @@ class Http:
elif size: elif size:
await self._send(b"%x\r\n%b\r\n" % (size, data)) await self._send(b"%x\r\n%b\r\n" % (size, data))
async def http1_response_normal(self, data: bytes, end_stream: bool): async def http1_response_normal(
"""Format / keep track of non-chunked response.""" self, data: bytes, end_stream: bool
) -> None:
"""
Format / keep track of non-chunked response.
"""
bytes_left = self.response_bytes_left - len(data) bytes_left = self.response_bytes_left - len(data)
if bytes_left <= 0: if bytes_left <= 0:
if bytes_left < 0: if bytes_left < 0:
@ -321,7 +376,10 @@ class Http:
await self._send(data) await self._send(data)
self.response_bytes_left = bytes_left self.response_bytes_left = bytes_left
async def error_response(self, exception): async def error_response(self, exception: Exception) -> None:
"""
Handle response when exception encountered
"""
# Disconnect after an error if in any other state than handler # Disconnect after an error if in any other state than handler
if self.stage is not Stage.HANDLER: if self.stage is not Stage.HANDLER:
self.keep_alive = False self.keep_alive = False
@ -339,10 +397,13 @@ class Http:
await app.handle_exception(self.request, exception) await app.handle_exception(self.request, exception)
def create_empty_request(self): def create_empty_request(self) -> None:
"""Current error handling code needs a request object that won't exist """
Current error handling code needs a request object that won't exist
if an error occurred during before a request was received. Create a if an error occurred during before a request was received. Create a
bogus response for error handling use.""" bogus response for error handling use.
"""
# FIXME: Avoid this by refactoring error handling and response code # FIXME: Avoid this by refactoring error handling and response code
self.request = self.protocol.request_class( self.request = self.protocol.request_class(
url_bytes=self.url.encode() if self.url else b"*", url_bytes=self.url.encode() if self.url else b"*",
@ -354,17 +415,10 @@ class Http:
) )
self.request.stream = self self.request.stream = self
def log_response(self): def log_response(self) -> None:
""" """
Helper method provided to enable the logging of responses in case if Helper method provided to enable the logging of responses in case if
the :attr:`HttpProtocol.access_log` is enabled. the :attr:`HttpProtocol.access_log` is enabled.
:param response: Response generated for the current request
:type response: :class:`sanic.response.HTTPResponse` or
:class:`sanic.response.StreamingHTTPResponse`
:return: None
""" """
req, res = self.request, self.response req, res = self.request, self.response
extra = { extra = {
@ -382,15 +436,20 @@ class Http:
# Request methods # Request methods
async def __aiter__(self): async def __aiter__(self):
"""Async iterate over request body.""" """
Async iterate over request body.
"""
while self.request_body: while self.request_body:
data = await self.read() data = await self.read()
if data: if data:
yield data yield data
async def read(self): async def read(self) -> Optional[bytes]:
"""Read some bytes of request body.""" """
Read some bytes of request body.
"""
# Send a 100-continue if needed # Send a 100-continue if needed
if self.expecting_continue: if self.expecting_continue:
self.expecting_continue = False self.expecting_continue = False
@ -440,7 +499,7 @@ class Http:
# End of request body? # End of request body?
if not self.request_bytes_left: if not self.request_bytes_left:
self.request_body = None self.request_body = None
return return None
# At this point we are good to read/return up to request_bytes_left # At this point we are good to read/return up to request_bytes_left
if not buf: if not buf:
@ -457,12 +516,14 @@ class Http:
# Response methods # Response methods
def respond(self, response): def respond(self, response: BaseHTTPResponse) -> BaseHTTPResponse:
"""Initiate new streaming response. """
Initiate new streaming response.
Nothing is sent until the first send() call on the returned object, and Nothing is sent until the first send() call on the returned object, and
calling this function multiple times will just alter the response to be calling this function multiple times will just alter the response to be
given.""" given.
"""
if self.stage is not Stage.HANDLER: if self.stage is not Stage.HANDLER:
self.stage = Stage.FAILED self.stage = Stage.FAILED
raise RuntimeError("Response already started") raise RuntimeError("Response already started")

View File

@ -54,5 +54,16 @@ LOGGING_CONFIG_DEFAULTS = dict(
logger = logging.getLogger("sanic.root") logger = logging.getLogger("sanic.root")
"""
General Sanic logger
"""
error_logger = logging.getLogger("sanic.error") error_logger = logging.getLogger("sanic.error")
"""
Logger used by Sanic for error logging
"""
access_logger = logging.getLogger("sanic.access") access_logger = logging.getLogger("sanic.access")
"""
Logger used by Sanic for access logging
"""

View File

@ -1,14 +1,30 @@
from __future__ import annotations
from typing import (
Optional,
TYPE_CHECKING,
DefaultDict,
Dict,
List,
NamedTuple,
Tuple,
)
if TYPE_CHECKING:
from sanic.server import ConnInfo
from sanic.app import Sanic
from asyncio.transports import BaseTransport
import email.utils import email.utils
import uuid import uuid
from collections import defaultdict, namedtuple from collections import defaultdict
from http.cookies import SimpleCookie from http.cookies import SimpleCookie
from types import SimpleNamespace from types import SimpleNamespace
from urllib.parse import parse_qs, parse_qsl, unquote, urlunparse from urllib.parse import parse_qs, parse_qsl, unquote, urlunparse
from httptools import parse_url # type: ignore from httptools import parse_url # type: ignore
from sanic.compat import CancelledErrors from sanic.compat import CancelledErrors, Header
from sanic.exceptions import InvalidUsage from sanic.exceptions import InvalidUsage
from sanic.headers import ( from sanic.headers import (
parse_content_header, parse_content_header,
@ -47,7 +63,9 @@ class RequestParameters(dict):
class Request: class Request:
"""Properties of an HTTP request such as URL, headers, etc.""" """
Properties of an HTTP request such as URL, headers, etc.
"""
__slots__ = ( __slots__ = (
"__weakref__", "__weakref__",
@ -80,7 +98,15 @@ class Request:
"version", "version",
) )
def __init__(self, url_bytes, headers, version, method, transport, app): def __init__(
self,
url_bytes: bytes,
headers: Header,
version: str,
method: str,
transport: BaseTransport,
app: Sanic,
):
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)
@ -94,18 +120,22 @@ class Request:
# Init but do not inhale # Init but do not inhale
self.body = b"" self.body = b""
self.conn_info = None self.conn_info: Optional[ConnInfo] = None
self.ctx = SimpleNamespace() self.ctx = SimpleNamespace()
self.name = None self.name: Optional[str] = None
self.parsed_forwarded = None self.parsed_forwarded = 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
self.parsed_args = defaultdict(RequestParameters) self.parsed_args: DefaultDict[
self.parsed_not_grouped_args = defaultdict(list) Tuple[bool, bool, str, str], RequestParameters
] = defaultdict(RequestParameters)
self.parsed_not_grouped_args: DefaultDict[
Tuple[bool, bool, str, str], List[Tuple[str, str]]
] = defaultdict(list)
self.uri_template = None self.uri_template = None
self.request_middleware_started = False self.request_middleware_started = False
self._cookies = None self._cookies: Dict[str, str] = {}
self.stream = None self.stream = None
self.endpoint = None self.endpoint = None
@ -350,11 +380,11 @@ class Request:
query_args = property(get_query_args) query_args = property(get_query_args)
@property @property
def cookies(self): def cookies(self) -> 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:
cookies = SimpleCookie() cookies: SimpleCookie = SimpleCookie()
cookies.load(cookie) cookies.load(cookie)
self._cookies = { self._cookies = {
name: cookie.value for name, cookie in cookies.items() name: cookie.value for name, cookie in cookies.items()
@ -364,27 +394,35 @@ class Request:
return self._cookies return self._cookies
@property @property
def content_type(self): def content_type(self) -> str:
"""
:return: Content-Type header form the request
:rtype: str
"""
return self.headers.get("Content-Type", DEFAULT_HTTP_CONTENT_TYPE) return self.headers.get("Content-Type", DEFAULT_HTTP_CONTENT_TYPE)
@property @property
def match_info(self): def match_info(self):
"""return matched info after resolving route""" """
:return: matched info after resolving route
"""
return self.app.router.get(self)[2] return self.app.router.get(self)[2]
# Transport properties (obtained from local interface only) # Transport properties (obtained from local interface only)
@property @property
def ip(self): def ip(self) -> str:
""" """
:return: peer ip of the socket :return: peer ip of the socket
:rtype: str
""" """
return self.conn_info.client if self.conn_info else "" return self.conn_info.client if self.conn_info else ""
@property @property
def port(self): def port(self) -> int:
""" """
:return: peer port of the socket :return: peer port of the socket
:rtype: int
""" """
return self.conn_info.client_port if self.conn_info else 0 return self.conn_info.client_port if self.conn_info else 0
@ -477,39 +515,49 @@ class Request:
@property @property
def server_name(self) -> str: def server_name(self) -> str:
"""The hostname the client connected to, by `request.host`.""" """
The hostname the client connected to, by ``request.host``.
"""
return parse_host(self.host)[0] or "" return parse_host(self.host)[0] or ""
@property @property
def server_port(self) -> int: def server_port(self) -> int:
""" """
The port the client connected to, by forwarded `port` or The port the client connected to, by forwarded ``port`` or
`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``.
""" """
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 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.""" """
Full path of current URL. Uses proxied or local path.
"""
return self.forwarded.get("path") or self.path return self.forwarded.get("path") or self.path
@property @property
def query_string(self): def query_string(self) -> str:
"""
Representation of the requested query
"""
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")
else: else:
return "" return ""
@property @property
def url(self): def url(self) -> str:
"""
The URL
"""
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)
) )
def url_for(self, view_name, **kwargs): def url_for(self, view_name: str, **kwargs) -> str:
""" """
Same as :func:`sanic.Sanic.url_for`, but automatically determine Same as :func:`sanic.Sanic.url_for`, but automatically determine
`scheme` and `netloc` base on the request. Since this method is aiming `scheme` and `netloc` base on the request. Since this method is aiming
@ -542,11 +590,23 @@ class Request:
) )
File = namedtuple("File", ["type", "body", "name"]) class File(NamedTuple):
"""
Model for defining a file
:param type: The mimetype, defaults to text/plain
:param body: Bytes of the file
:param name: The filename
"""
type: str
body: bytes
name: str
def parse_multipart_form(body, boundary): def parse_multipart_form(body, boundary):
"""Parse a request body and returns fields and files """
Parse a request body and returns fields and files
:param body: bytes request body :param body: bytes request body
:param boundary: bytes multipart boundary :param boundary: bytes multipart boundary

View File

@ -1,12 +1,14 @@
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 urllib.parse import quote_plus from urllib.parse import quote_plus
from warnings import warn from warnings import warn
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
from sanic.http import Http
try: try:
@ -21,7 +23,10 @@ except ImportError:
class BaseHTTPResponse: class BaseHTTPResponse:
def __init__(self): def __init__(self):
self.asgi = False self.asgi: bool = False
self.body: Optional[bytes] = None
self.stream: Http = None
self.status: int = None
def _encode_body(self, data): def _encode_body(self, data):
if data is None: if data is None:

View File

@ -1,3 +1,21 @@
from __future__ import annotations
from typing import (
Optional,
TYPE_CHECKING,
DefaultDict,
Dict,
List,
NamedTuple,
Tuple,
Dict,
Type,
Union,
)
if TYPE_CHECKING:
from sanic.http import Http
from sanic.app import Sanic
import asyncio import asyncio
import multiprocessing import multiprocessing
import os import os
@ -6,6 +24,7 @@ import socket
import stat import stat
import sys import sys
from sanic.http import Stage
from asyncio import CancelledError from asyncio import CancelledError
from functools import partial from functools import partial
from inspect import isawaitable from inspect import isawaitable
@ -13,15 +32,13 @@ 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 typing import Dict, Type, Union 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 sanic.request import Request from asyncio.transports import BaseTransport
try: try:
import uvloop # type: ignore import uvloop # type: ignore
@ -49,7 +66,7 @@ class ConnInfo:
"ssl", "ssl",
) )
def __init__(self, transport, unix=None): def __init__(self, transport: BaseTransport, unix=None):
self.ssl = bool(transport.get_extra_info("sslcontext")) self.ssl = bool(transport.get_extra_info("sslcontext"))
self.server = self.client = "" self.server = self.client = ""
self.server_port = self.client_port = 0 self.server_port = self.client_port = 0
@ -126,7 +143,7 @@ class HttpProtocol(asyncio.Protocol):
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
self.loop = loop self.loop = loop
deprecated_loop = self.loop if sys.version_info < (3, 7) else None deprecated_loop = self.loop if sys.version_info < (3, 7) else None
self.app = app self.app: Sanic = app
self.url = None self.url = None
self.transport = None self.transport = None
self.conn_info = None self.conn_info = None

View File

@ -1,5 +1,6 @@
import inspect import inspect
import os import os
from pathlib import Path from pathlib import Path
from time import gmtime, strftime from time import gmtime, strftime
@ -93,8 +94,8 @@ def test_static_file_pathlib(app, static_file_directory, file_name):
[b"test.file", b"decode me.txt", b"python.png"], [b"test.file", b"decode me.txt", b"python.png"],
) )
def test_static_file_bytes(app, static_file_directory, file_name): def test_static_file_bytes(app, static_file_directory, file_name):
bsep = os.path.sep.encode('utf-8') bsep = os.path.sep.encode("utf-8")
file_path = static_file_directory.encode('utf-8') + bsep + file_name file_path = static_file_directory.encode("utf-8") + bsep + file_name
app.static("/testing.file", file_path) app.static("/testing.file", file_path)
request, response = app.test_client.get("/testing.file") request, response = app.test_client.get("/testing.file")
assert response.status == 200 assert response.status == 200