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 --config ./.black.toml sanic tests
fix-import: black
isort:
isort sanic tests
pretty: black isort
docs-clean:
cd docs && make clean

View File

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

View File

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

View File

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

View File

@ -1,6 +1,17 @@
from sanic.__version__ import __version__
from sanic.app import Sanic
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):
"""
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():
if k.startswith(prefix):
@ -86,7 +95,9 @@ class Config(dict):
"""
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
holding settings.
@ -102,7 +113,7 @@ class Config(dict):
config.update_config("${some}/py/file")
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.
You can upload app config by providing dict holding settings.
@ -122,6 +133,8 @@ class Config(dict):
B = 2
config.update_config(C)
`See user guide <https://sanicframework.org/guide/deployment/configuration.html>`__
"""
if isinstance(config, (bytes, str, Path)):

View File

@ -2,6 +2,7 @@ import re
import string
from datetime import datetime
from typing import Dict
DEFAULT_MAX_AGE = 0
@ -41,16 +42,17 @@ _is_legal_key = re.compile("[%s]+" % re.escape(_LegalChars)).fullmatch
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
MultiHeader class to provide a unique key that encodes to Set-Cookie.
"""
def __init__(self, headers):
super().__init__()
self.headers = headers
self.cookie_headers = {}
self.header_key = "Set-Cookie"
self.headers: Dict[str, str] = headers
self.cookie_headers: Dict[str, str] = {}
self.header_key: str = "Set-Cookie"
def __setitem__(self, key, value):
# 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 typing as t
@ -26,6 +40,10 @@ FALLBACK_STATUS = 500
class BaseRenderer:
"""
Base class that all renderers must inherit from.
"""
def __init__(self, request, exception, debug):
self.request = request
self.exception = exception
@ -54,7 +72,13 @@ class BaseRenderer:
status_text = STATUS_CODES.get(self.status, b"Error Occurred").decode()
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 = (
self.full
if self.debug and not getattr(self.exception, "quiet", False)
@ -62,14 +86,28 @@ class BaseRenderer:
)
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
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
class HTMLRenderer(BaseRenderer):
"""
Render an exception as HTML.
The default fallback type.
"""
TRACEBACK_STYLE = """
html { font-family: sans-serif }
h2 { color: #888; }
@ -172,6 +210,10 @@ class HTMLRenderer(BaseRenderer):
class TextRenderer(BaseRenderer):
"""
Render an exception as plain text.
"""
OUTPUT_TEXT = "{title}\n{bar}\n{text}\n\n{body}"
SPACER = " "
@ -231,6 +273,10 @@ class TextRenderer(BaseRenderer):
class JSONRenderer(BaseRenderer):
"""
Render an exception as JSON.
"""
def full(self):
output = self._generate_output(full=True)
return json(output, status=self.status, dumps=dumps)
@ -280,7 +326,9 @@ class JSONRenderer(BaseRenderer):
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;")
@ -303,7 +351,9 @@ def exception_response(
debug: bool,
renderer: t.Type[t.Optional[BaseRenderer]] = None,
) -> HTTPResponse:
"""Render a response for the default FALLBACK exception handler"""
"""
Render a response for the default FALLBACK exception handler.
"""
if not renderer:
renderer = HTMLRenderer

View File

@ -1,3 +1,5 @@
from typing import Optional, Union
from sanic.helpers import STATUS_CODES
@ -33,16 +35,28 @@ class SanicException(Exception):
@add_status_code(404)
class NotFound(SanicException):
"""
**Status**: 404 Not Found
"""
pass
@add_status_code(400)
class InvalidUsage(SanicException):
"""
**Status**: 400 Bad Request
"""
pass
@add_status_code(405)
class MethodNotSupported(SanicException):
"""
**Status**: 405 Method Not Allowed
"""
def __init__(self, message, method, allowed_methods):
super().__init__(message)
self.headers = {"Allow": ", ".join(allowed_methods)}
@ -50,22 +64,38 @@ class MethodNotSupported(SanicException):
@add_status_code(500)
class ServerError(SanicException):
"""
**Status**: 500 Internal Server Error
"""
pass
@add_status_code(503)
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
class URLBuildError(ServerError):
"""
**Status**: 500 Internal Server Error
"""
pass
class FileNotFound(NotFound):
"""
**Status**: 404 Not Found
"""
def __init__(self, message, path, relative_url):
super().__init__(message)
self.path = path
@ -87,15 +117,27 @@ class RequestTimeout(SanicException):
@add_status_code(413)
class PayloadTooLarge(SanicException):
"""
**Status**: 413 Payload Too Large
"""
pass
class HeaderNotFound(InvalidUsage):
"""
**Status**: 400 Bad Request
"""
pass
@add_status_code(416)
class ContentRangeError(SanicException):
"""
**Status**: 416 Range Not Satisfiable
"""
def __init__(self, message, content_range):
super().__init__(message)
self.headers = {"Content-Range": f"bytes */{content_range.total}"}
@ -103,15 +145,27 @@ class ContentRangeError(SanicException):
@add_status_code(417)
class HeaderExpectationFailed(SanicException):
"""
**Status**: 417 Expectation Failed
"""
pass
@add_status_code(403)
class Forbidden(SanicException):
"""
**Status**: 403 Forbidden
"""
pass
class InvalidRangeType(ContentRangeError):
"""
**Status**: 416 Range Not Satisfiable
"""
pass
@ -123,7 +177,7 @@ class PyFileError(Exception):
@add_status_code(401)
class Unauthorized(SanicException):
"""
Unauthorized exception (401 HTTP status code).
**Status**: 401 Unauthorized
:param message: Message describing the exception.
:param status_code: HTTP Status code.
@ -173,7 +227,7 @@ class LoadFileException(SanicException):
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
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
"""
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
message = message.decode("utf8")
message = msg.decode("utf8")
sanic_exception = _sanic_exceptions.get(status_code, SanicException)
raise sanic_exception(message=message, status_code=status_code)

View File

@ -2,9 +2,10 @@
from importlib import import_module
from inspect import ismodule
from typing import Dict
STATUS_CODES = {
STATUS_CODES: Dict[int, bytes] = {
100: b"Continue",
101: b"Switching Protocols",
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 enum import Enum
@ -15,6 +25,17 @@ from sanic.log import access_logger, logger
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
REQUEST = 1 # Request headers being received
HANDLER = 3 # Headers done, handler running
@ -26,6 +47,24 @@ HTTP_CONTINUE = b"HTTP/1.1 100 Continue\r\n\r\n"
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__ = [
"_send",
"_receive_more",
@ -53,16 +92,16 @@ class Http:
self._receive_more = protocol.receive_more
self.recv_buffer = protocol.recv_buffer
self.protocol = protocol
self.expecting_continue = False
self.stage = Stage.IDLE
self.expecting_continue: bool = False
self.stage: Stage = Stage.IDLE
self.request_body = None
self.request_bytes = None
self.request_bytes_left = None
self.request_max_size = protocol.request_max_size
self.keep_alive = True
self.head_only = None
self.request = None
self.response = None
self.request: Request = None
self.response: BaseHTTPResponse = None
self.exception = None
self.url = None
self.upgrade_websocket = False
@ -72,7 +111,9 @@ class Http:
return self.stage in (Stage.HANDLER, Stage.RESPONSE)
async def http1(self):
"""HTTP 1.1 connection handler"""
"""
HTTP 1.1 connection handler
"""
while True: # As long as connection stays keep-alive
try:
# Receive and handle a request
@ -125,7 +166,9 @@ class Http:
await self._receive_more()
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)
# Receive until full header is in buffer
buf = self.recv_buffer
@ -214,7 +257,9 @@ class Http:
self.request, request.stream = request, self
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
# Compatibility with simple response body
@ -257,7 +302,7 @@ class Http:
else:
# Length not known, use chunked encoding
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
if self.head_only:
@ -283,14 +328,20 @@ class Http:
await self._send(ret)
self.stage = Stage.IDLE if end_stream else Stage.RESPONSE
def head_response_ignored(self, data, end_stream):
"""HEAD response: body data silently ignored."""
def head_response_ignored(self, data: bytes, end_stream: bool) -> None:
"""
HEAD response: body data silently ignored.
"""
if end_stream:
self.response_func = None
self.stage = Stage.IDLE
async def http1_response_chunked(self, data, end_stream):
"""Format a part of response body in chunked encoding."""
async def http1_response_chunked(
self, data: bytes, end_stream: bool
) -> None:
"""
Format a part of response body in chunked encoding.
"""
# Chunked encoding
size = len(data)
if end_stream:
@ -304,8 +355,12 @@ class Http:
elif size:
await self._send(b"%x\r\n%b\r\n" % (size, data))
async def http1_response_normal(self, data: bytes, end_stream: bool):
"""Format / keep track of non-chunked response."""
async def http1_response_normal(
self, data: bytes, end_stream: bool
) -> None:
"""
Format / keep track of non-chunked response.
"""
bytes_left = self.response_bytes_left - len(data)
if bytes_left <= 0:
if bytes_left < 0:
@ -321,7 +376,10 @@ class Http:
await self._send(data)
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
if self.stage is not Stage.HANDLER:
self.keep_alive = False
@ -339,10 +397,13 @@ class Http:
await app.handle_exception(self.request, exception)
def create_empty_request(self):
"""Current error handling code needs a request object that won't exist
def create_empty_request(self) -> None:
"""
Current error handling code needs a request object that won't exist
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
self.request = self.protocol.request_class(
url_bytes=self.url.encode() if self.url else b"*",
@ -354,17 +415,10 @@ class Http:
)
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
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
extra = {
@ -382,15 +436,20 @@ class Http:
# Request methods
async def __aiter__(self):
"""Async iterate over request body."""
"""
Async iterate over request body.
"""
while self.request_body:
data = await self.read()
if data:
yield data
async def read(self):
"""Read some bytes of request body."""
async def read(self) -> Optional[bytes]:
"""
Read some bytes of request body.
"""
# Send a 100-continue if needed
if self.expecting_continue:
self.expecting_continue = False
@ -440,7 +499,7 @@ class Http:
# End of request body?
if not self.request_bytes_left:
self.request_body = None
return
return None
# At this point we are good to read/return up to request_bytes_left
if not buf:
@ -457,12 +516,14 @@ class Http:
# Response methods
def respond(self, response):
"""Initiate new streaming response.
def respond(self, response: BaseHTTPResponse) -> BaseHTTPResponse:
"""
Initiate new streaming response.
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
given."""
given.
"""
if self.stage is not Stage.HANDLER:
self.stage = Stage.FAILED
raise RuntimeError("Response already started")

View File

@ -54,5 +54,16 @@ LOGGING_CONFIG_DEFAULTS = dict(
logger = logging.getLogger("sanic.root")
"""
General Sanic logger
"""
error_logger = logging.getLogger("sanic.error")
"""
Logger used by Sanic for error logging
"""
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 uuid
from collections import defaultdict, namedtuple
from collections import defaultdict
from http.cookies import SimpleCookie
from types import SimpleNamespace
from urllib.parse import parse_qs, parse_qsl, unquote, urlunparse
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.headers import (
parse_content_header,
@ -47,7 +63,9 @@ class RequestParameters(dict):
class Request:
"""Properties of an HTTP request such as URL, headers, etc."""
"""
Properties of an HTTP request such as URL, headers, etc.
"""
__slots__ = (
"__weakref__",
@ -80,7 +98,15 @@ class Request:
"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
# TODO: Content-Encoding detection
self._parsed_url = parse_url(url_bytes)
@ -94,18 +120,22 @@ class Request:
# Init but do not inhale
self.body = b""
self.conn_info = None
self.conn_info: Optional[ConnInfo] = None
self.ctx = SimpleNamespace()
self.name = None
self.name: Optional[str] = None
self.parsed_forwarded = None
self.parsed_json = None
self.parsed_form = None
self.parsed_files = None
self.parsed_args = defaultdict(RequestParameters)
self.parsed_not_grouped_args = defaultdict(list)
self.parsed_args: DefaultDict[
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.request_middleware_started = False
self._cookies = None
self._cookies: Dict[str, str] = {}
self.stream = None
self.endpoint = None
@ -350,11 +380,11 @@ class Request:
query_args = property(get_query_args)
@property
def cookies(self):
def cookies(self) -> Dict[str, str]:
if self._cookies is None:
cookie = self.headers.get("Cookie")
if cookie is not None:
cookies = SimpleCookie()
cookies: SimpleCookie = SimpleCookie()
cookies.load(cookie)
self._cookies = {
name: cookie.value for name, cookie in cookies.items()
@ -364,27 +394,35 @@ class Request:
return self._cookies
@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)
@property
def match_info(self):
"""return matched info after resolving route"""
"""
:return: matched info after resolving route
"""
return self.app.router.get(self)[2]
# Transport properties (obtained from local interface only)
@property
def ip(self):
def ip(self) -> str:
"""
:return: peer ip of the socket
:rtype: str
"""
return self.conn_info.client if self.conn_info else ""
@property
def port(self):
def port(self) -> int:
"""
:return: peer port of the socket
:rtype: int
"""
return self.conn_info.client_port if self.conn_info else 0
@ -477,39 +515,49 @@ class Request:
@property
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 ""
@property
def server_port(self) -> int:
"""
The port the client connected to, by forwarded `port` or
`request.host`.
The port the client connected to, by forwarded ``port`` or
``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]
return port or (80 if self.scheme in ("http", "ws") else 443)
@property
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
@property
def query_string(self):
def query_string(self) -> str:
"""
Representation of the requested query
"""
if self._parsed_url.query:
return self._parsed_url.query.decode("utf-8")
else:
return ""
@property
def url(self):
def url(self) -> str:
"""
The URL
"""
return urlunparse(
(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
`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):
"""Parse a request body and returns fields and files
"""
Parse a request body and returns fields and files
:param body: bytes request body
:param boundary: bytes multipart boundary

View File

@ -1,12 +1,14 @@
from functools import partial
from mimetypes import guess_type
from os import path
from typing import Optional
from urllib.parse import quote_plus
from warnings import warn
from sanic.compat import Header, open_async
from sanic.cookies import CookieJar
from sanic.helpers import has_message_body, remove_entity_headers
from sanic.http import Http
try:
@ -21,7 +23,10 @@ except ImportError:
class BaseHTTPResponse:
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):
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 multiprocessing
import os
@ -6,6 +24,7 @@ import socket
import stat
import sys
from sanic.http import Stage
from asyncio import CancelledError
from functools import partial
from inspect import isawaitable
@ -13,15 +32,13 @@ from ipaddress import ip_address
from signal import SIG_IGN, SIGINT, SIGTERM, Signals
from signal import signal as signal_func
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.config import Config
from sanic.exceptions import RequestTimeout, ServiceUnavailable
from sanic.http import Http, Stage
from sanic.log import logger
from sanic.request import Request
from asyncio.transports import BaseTransport
try:
import uvloop # type: ignore
@ -49,7 +66,7 @@ class ConnInfo:
"ssl",
)
def __init__(self, transport, unix=None):
def __init__(self, transport: BaseTransport, unix=None):
self.ssl = bool(transport.get_extra_info("sslcontext"))
self.server = self.client = ""
self.server_port = self.client_port = 0
@ -126,7 +143,7 @@ class HttpProtocol(asyncio.Protocol):
asyncio.set_event_loop(loop)
self.loop = loop
deprecated_loop = self.loop if sys.version_info < (3, 7) else None
self.app = app
self.app: Sanic = app
self.url = None
self.transport = None
self.conn_info = None

View File

@ -1,5 +1,6 @@
import inspect
import os
from pathlib import Path
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"],
)
def test_static_file_bytes(app, static_file_directory, file_name):
bsep = os.path.sep.encode('utf-8')
file_path = static_file_directory.encode('utf-8') + bsep + file_name
bsep = os.path.sep.encode("utf-8")
file_path = static_file_directory.encode("utf-8") + bsep + file_name
app.static("/testing.file", file_path)
request, response = app.test_client.get("/testing.file")
assert response.status == 200