Merge branch 'master' into Improving-documentation
This commit is contained in:
@@ -1 +1 @@
|
||||
__version__ = "20.6.3"
|
||||
__version__ = "20.9.0"
|
||||
|
||||
15
sanic/app.py
15
sanic/app.py
@@ -676,9 +676,10 @@ class Sanic:
|
||||
:param strict_slashes: Instruct :class:`Sanic` to check if the request
|
||||
URLs need to terminate with a */*
|
||||
:param content_type: user defined content type for header
|
||||
:return: None
|
||||
:return: routes registered on the router
|
||||
:rtype: List[sanic.router.Route]
|
||||
"""
|
||||
static_register(
|
||||
return static_register(
|
||||
self,
|
||||
uri,
|
||||
file_or_directory,
|
||||
@@ -1452,3 +1453,13 @@ class Sanic:
|
||||
self.asgi = True
|
||||
asgi_app = await ASGIApp.create(self, scope, receive, send)
|
||||
await asgi_app()
|
||||
|
||||
# -------------------------------------------------------------------- #
|
||||
# Configuration
|
||||
# -------------------------------------------------------------------- #
|
||||
def update_config(self, config: Union[bytes, str, dict, Any]):
|
||||
"""Update app.config.
|
||||
|
||||
Please refer to config.py::Config.update_config for documentation."""
|
||||
|
||||
self.config.update_config(config)
|
||||
|
||||
@@ -143,7 +143,18 @@ class Blueprint:
|
||||
if _routes:
|
||||
routes += _routes
|
||||
|
||||
# Static Files
|
||||
for future in self.statics:
|
||||
# Prepend the blueprint URI prefix if available
|
||||
uri = url_prefix + future.uri if url_prefix else future.uri
|
||||
_routes = app.static(
|
||||
uri, future.file_or_directory, *future.args, **future.kwargs
|
||||
)
|
||||
if _routes:
|
||||
routes += _routes
|
||||
|
||||
route_names = [route.name for route in routes if route]
|
||||
|
||||
# Middleware
|
||||
for future in self.middlewares:
|
||||
if future.args or future.kwargs:
|
||||
@@ -160,14 +171,6 @@ class Blueprint:
|
||||
for future in self.exceptions:
|
||||
app.exception(*future.args, **future.kwargs)(future.handler)
|
||||
|
||||
# Static Files
|
||||
for future in self.statics:
|
||||
# Prepend the blueprint URI prefix if available
|
||||
uri = url_prefix + future.uri if url_prefix else future.uri
|
||||
app.static(
|
||||
uri, future.file_or_directory, *future.args, **future.kwargs
|
||||
)
|
||||
|
||||
# Event listeners
|
||||
for event, listeners in self.listeners.items():
|
||||
for listener in listeners:
|
||||
|
||||
147
sanic/config.py
147
sanic/config.py
@@ -1,8 +1,15 @@
|
||||
import os
|
||||
import types
|
||||
from os import environ
|
||||
from typing import Any, Union
|
||||
|
||||
from sanic.exceptions import PyFileError
|
||||
from sanic.helpers import import_string
|
||||
# NOTE(tomaszdrozdz): remove in version: 21.3
|
||||
# We replace from_envvar(), from_object(), from_pyfile() config object methods
|
||||
# with one simpler update_config() method.
|
||||
# We also replace "loading module from file code" in from_pyfile()
|
||||
# in a favour of load_module_from_file_location().
|
||||
# Please see pull request: 1903
|
||||
# and issue: 1895
|
||||
from .deprecated import from_envvar, from_object, from_pyfile # noqa
|
||||
from .utils import load_module_from_file_location, str_to_bool
|
||||
|
||||
|
||||
SANIC_PREFIX = "SANIC_"
|
||||
@@ -24,12 +31,15 @@ DEFAULT_CONFIG = {
|
||||
"WEBSOCKET_MAX_QUEUE": 32,
|
||||
"WEBSOCKET_READ_LIMIT": 2 ** 16,
|
||||
"WEBSOCKET_WRITE_LIMIT": 2 ** 16,
|
||||
"WEBSOCKET_PING_TIMEOUT": 20,
|
||||
"WEBSOCKET_PING_INTERVAL": 20,
|
||||
"GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec
|
||||
"ACCESS_LOG": True,
|
||||
"FORWARDED_SECRET": None,
|
||||
"REAL_IP_HEADER": None,
|
||||
"PROXIES_COUNT": None,
|
||||
"FORWARDED_FOR_HEADER": "X-Forwarded-For",
|
||||
"FALLBACK_ERROR_FORMAT": "html",
|
||||
}
|
||||
|
||||
|
||||
@@ -56,76 +66,23 @@ class Config(dict):
|
||||
def __setattr__(self, attr, value):
|
||||
self[attr] = value
|
||||
|
||||
def from_envvar(self, variable_name):
|
||||
"""Load a configuration from an environment variable pointing to
|
||||
a configuration file.
|
||||
|
||||
:param variable_name: name of the environment variable
|
||||
:return: bool. ``True`` if able to load config, ``False`` otherwise.
|
||||
"""
|
||||
config_file = os.environ.get(variable_name)
|
||||
if not config_file:
|
||||
raise RuntimeError(
|
||||
"The environment variable %r is not set and "
|
||||
"thus configuration could not be loaded." % variable_name
|
||||
)
|
||||
return self.from_pyfile(config_file)
|
||||
|
||||
def from_pyfile(self, filename):
|
||||
"""Update the values in the config from a Python file.
|
||||
Only the uppercase variables in that module are stored in the config.
|
||||
|
||||
:param filename: an absolute path to the config file
|
||||
"""
|
||||
module = types.ModuleType("config")
|
||||
module.__file__ = filename
|
||||
try:
|
||||
with open(filename) as config_file:
|
||||
exec( # nosec
|
||||
compile(config_file.read(), filename, "exec"),
|
||||
module.__dict__,
|
||||
)
|
||||
except IOError as e:
|
||||
e.strerror = "Unable to load configuration file (%s)" % e.strerror
|
||||
raise
|
||||
except Exception as e:
|
||||
raise PyFileError(filename) from e
|
||||
|
||||
self.from_object(module)
|
||||
return True
|
||||
|
||||
def from_object(self, obj):
|
||||
"""Update the values from the given object.
|
||||
Objects are usually either modules or classes.
|
||||
|
||||
Just the uppercase variables in that object are stored in the config.
|
||||
Example usage::
|
||||
|
||||
from yourapplication import default_config
|
||||
app.config.from_object(default_config)
|
||||
|
||||
or also:
|
||||
app.config.from_object('myproject.config.MyConfigClass')
|
||||
|
||||
You should not use this function to load the actual configuration but
|
||||
rather configuration defaults. The actual config should be loaded
|
||||
with :meth:`from_pyfile` and ideally from a location not within the
|
||||
package because the package might be installed system wide.
|
||||
|
||||
:param obj: an object holding the configuration
|
||||
"""
|
||||
if isinstance(obj, str):
|
||||
obj = import_string(obj)
|
||||
for key in dir(obj):
|
||||
if key.isupper():
|
||||
self[key] = getattr(obj, key)
|
||||
# NOTE(tomaszdrozdz): remove in version: 21.3
|
||||
# We replace from_envvar(), from_object(), from_pyfile() config object
|
||||
# methods with one simpler update_config() method.
|
||||
# We also replace "loading module from file code" in from_pyfile()
|
||||
# in a favour of load_module_from_file_location().
|
||||
# Please see pull request: 1903
|
||||
# and issue: 1895
|
||||
from_envvar = from_envvar
|
||||
from_pyfile = from_pyfile
|
||||
from_object = from_object
|
||||
|
||||
def load_environment_vars(self, prefix=SANIC_PREFIX):
|
||||
"""
|
||||
Looks for prefixed environment variables and applies
|
||||
them to the configuration if present.
|
||||
"""
|
||||
for k, v in os.environ.items():
|
||||
for k, v in environ.items():
|
||||
if k.startswith(prefix):
|
||||
_, config_key = k.split(prefix, 1)
|
||||
try:
|
||||
@@ -135,23 +92,47 @@ class Config(dict):
|
||||
self[config_key] = float(v)
|
||||
except ValueError:
|
||||
try:
|
||||
self[config_key] = strtobool(v)
|
||||
self[config_key] = str_to_bool(v)
|
||||
except ValueError:
|
||||
self[config_key] = v
|
||||
|
||||
def update_config(self, config: Union[bytes, str, dict, Any]):
|
||||
"""Update app.config.
|
||||
|
||||
def strtobool(val):
|
||||
"""
|
||||
This function was borrowed from distutils.utils. While distutils
|
||||
is part of stdlib, it feels odd to use distutils in main application code.
|
||||
Note:: only upper case settings are considered.
|
||||
|
||||
The function was modified to walk its talk and actually return bool
|
||||
and not int.
|
||||
"""
|
||||
val = val.lower()
|
||||
if val in ("y", "yes", "t", "true", "on", "1"):
|
||||
return True
|
||||
elif val in ("n", "no", "f", "false", "off", "0"):
|
||||
return False
|
||||
else:
|
||||
raise ValueError("invalid truth value %r" % (val,))
|
||||
You can upload app config by providing path to py file
|
||||
holding settings.
|
||||
|
||||
# /some/py/file
|
||||
A = 1
|
||||
B = 2
|
||||
|
||||
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
|
||||
as plain string.
|
||||
|
||||
You can upload app config by providing dict holding settings.
|
||||
|
||||
d = {"A": 1, "B": 2}
|
||||
config.update_config(d)
|
||||
|
||||
You can upload app config by providing any object holding settings,
|
||||
but in such case config.__dict__ will be used as dict holding settings.
|
||||
|
||||
class C:
|
||||
A = 1
|
||||
B = 2
|
||||
config.update_config(C)"""
|
||||
|
||||
if isinstance(config, (bytes, str)):
|
||||
config = load_module_from_file_location(location=config)
|
||||
|
||||
if not isinstance(config, dict):
|
||||
config = config.__dict__
|
||||
|
||||
config = dict(filter(lambda i: i[0].isupper(), config.items()))
|
||||
|
||||
self.update(config)
|
||||
|
||||
106
sanic/deprecated.py
Normal file
106
sanic/deprecated.py
Normal file
@@ -0,0 +1,106 @@
|
||||
# NOTE(tomaszdrozdz): remove in version: 21.3
|
||||
# We replace from_envvar(), from_object(), from_pyfile() config object methods
|
||||
# with one simpler update_config() method.
|
||||
# We also replace "loading module from file code" in from_pyfile()
|
||||
# in a favour of load_module_from_file_location().
|
||||
# Please see pull request: 1903
|
||||
# and issue: 1895
|
||||
import types
|
||||
|
||||
from os import environ
|
||||
from typing import Any
|
||||
from warnings import warn
|
||||
|
||||
from sanic.exceptions import PyFileError
|
||||
from sanic.helpers import import_string
|
||||
|
||||
|
||||
def from_envvar(self, variable_name: str) -> bool:
|
||||
"""Load a configuration from an environment variable pointing to
|
||||
a configuration file.
|
||||
|
||||
:param variable_name: name of the environment variable
|
||||
:return: bool. ``True`` if able to load config, ``False`` otherwise.
|
||||
"""
|
||||
|
||||
warn(
|
||||
"Using `from_envvar` method is deprecated and will be removed in "
|
||||
"v21.3, use `app.update_config` method instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
config_file = environ.get(variable_name)
|
||||
if not config_file:
|
||||
raise RuntimeError(
|
||||
f"The environment variable {variable_name} is not set and "
|
||||
f"thus configuration could not be loaded."
|
||||
)
|
||||
return self.from_pyfile(config_file)
|
||||
|
||||
|
||||
def from_pyfile(self, filename: str) -> bool:
|
||||
"""Update the values in the config from a Python file.
|
||||
Only the uppercase variables in that module are stored in the config.
|
||||
|
||||
:param filename: an absolute path to the config file
|
||||
"""
|
||||
|
||||
warn(
|
||||
"Using `from_pyfile` method is deprecated and will be removed in "
|
||||
"v21.3, use `app.update_config` method instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
module = types.ModuleType("config")
|
||||
module.__file__ = filename
|
||||
try:
|
||||
with open(filename) as config_file:
|
||||
exec( # nosec
|
||||
compile(config_file.read(), filename, "exec"),
|
||||
module.__dict__,
|
||||
)
|
||||
except IOError as e:
|
||||
e.strerror = "Unable to load configuration file (e.strerror)"
|
||||
raise
|
||||
except Exception as e:
|
||||
raise PyFileError(filename) from e
|
||||
|
||||
self.from_object(module)
|
||||
return True
|
||||
|
||||
|
||||
def from_object(self, obj: Any) -> None:
|
||||
"""Update the values from the given object.
|
||||
Objects are usually either modules or classes.
|
||||
|
||||
Just the uppercase variables in that object are stored in the config.
|
||||
Example usage::
|
||||
|
||||
from yourapplication import default_config
|
||||
app.config.from_object(default_config)
|
||||
|
||||
or also:
|
||||
app.config.from_object('myproject.config.MyConfigClass')
|
||||
|
||||
You should not use this function to load the actual configuration but
|
||||
rather configuration defaults. The actual config should be loaded
|
||||
with :meth:`from_pyfile` and ideally from a location not within the
|
||||
package because the package might be installed system wide.
|
||||
|
||||
:param obj: an object holding the configuration
|
||||
"""
|
||||
|
||||
warn(
|
||||
"Using `from_object` method is deprecated and will be removed in "
|
||||
"v21.3, use `app.update_config` method instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
if isinstance(obj, str):
|
||||
obj = import_string(obj)
|
||||
for key in dir(obj):
|
||||
if key.isupper():
|
||||
self[key] = getattr(obj, key)
|
||||
@@ -1,13 +1,283 @@
|
||||
import sys
|
||||
import typing as t
|
||||
|
||||
from functools import partial
|
||||
from traceback import extract_tb
|
||||
|
||||
from sanic.exceptions import SanicException
|
||||
from sanic.exceptions import InvalidUsage, SanicException
|
||||
from sanic.helpers import STATUS_CODES
|
||||
from sanic.response import html
|
||||
from sanic.request import Request
|
||||
from sanic.response import HTTPResponse, html, json, text
|
||||
|
||||
|
||||
# Here, There Be Dragons (custom HTML formatting to follow)
|
||||
try:
|
||||
from ujson import dumps
|
||||
|
||||
dumps = partial(dumps, escape_forward_slashes=False)
|
||||
except ImportError: # noqa
|
||||
from json import dumps # type: ignore
|
||||
|
||||
|
||||
FALLBACK_TEXT = (
|
||||
"The server encountered an internal error and "
|
||||
"cannot complete your request."
|
||||
)
|
||||
FALLBACK_STATUS = 500
|
||||
|
||||
|
||||
class BaseRenderer:
|
||||
def __init__(self, request, exception, debug):
|
||||
self.request = request
|
||||
self.exception = exception
|
||||
self.debug = debug
|
||||
|
||||
@property
|
||||
def headers(self):
|
||||
if isinstance(self.exception, SanicException):
|
||||
return getattr(self.exception, "headers", {})
|
||||
return {}
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
if isinstance(self.exception, SanicException):
|
||||
return getattr(self.exception, "status_code", FALLBACK_STATUS)
|
||||
return FALLBACK_STATUS
|
||||
|
||||
@property
|
||||
def text(self):
|
||||
if self.debug or isinstance(self.exception, SanicException):
|
||||
return str(self.exception)
|
||||
return FALLBACK_TEXT
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
status_text = STATUS_CODES.get(self.status, b"Error Occurred").decode()
|
||||
return f"{self.status} — {status_text}"
|
||||
|
||||
def render(self):
|
||||
output = (
|
||||
self.full
|
||||
if self.debug and not getattr(self.exception, "quiet", False)
|
||||
else self.minimal
|
||||
)
|
||||
return output()
|
||||
|
||||
def minimal(self): # noqa
|
||||
raise NotImplementedError
|
||||
|
||||
def full(self): # noqa
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class HTMLRenderer(BaseRenderer):
|
||||
TRACEBACK_STYLE = """
|
||||
html { font-family: sans-serif }
|
||||
h2 { color: #888; }
|
||||
.tb-wrapper p { margin: 0 }
|
||||
.frame-border { margin: 1rem }
|
||||
.frame-line > * { padding: 0.3rem 0.6rem }
|
||||
.frame-line { margin-bottom: 0.3rem }
|
||||
.frame-code { font-size: 16px; padding-left: 4ch }
|
||||
.tb-wrapper { border: 1px solid #eee }
|
||||
.tb-header { background: #eee; padding: 0.3rem; font-weight: bold }
|
||||
.frame-descriptor { background: #e2eafb; font-size: 14px }
|
||||
"""
|
||||
TRACEBACK_WRAPPER_HTML = (
|
||||
"<div class=tb-header>{exc_name}: {exc_value}</div>"
|
||||
"<div class=tb-wrapper>{frame_html}</div>"
|
||||
)
|
||||
TRACEBACK_BORDER = (
|
||||
"<div class=frame-border>"
|
||||
"The above exception was the direct cause of the following exception:"
|
||||
"</div>"
|
||||
)
|
||||
TRACEBACK_LINE_HTML = (
|
||||
"<div class=frame-line>"
|
||||
"<p class=frame-descriptor>"
|
||||
"File {0.filename}, line <i>{0.lineno}</i>, "
|
||||
"in <code><b>{0.name}</b></code>"
|
||||
"<p class=frame-code><code>{0.line}</code>"
|
||||
"</div>"
|
||||
)
|
||||
OUTPUT_HTML = (
|
||||
"<!DOCTYPE html><html lang=en>"
|
||||
"<meta charset=UTF-8><title>{title}</title>\n"
|
||||
"<style>{style}</style>\n"
|
||||
"<h1>{title}</h1><p>{text}\n"
|
||||
"{body}"
|
||||
)
|
||||
|
||||
def full(self):
|
||||
return html(
|
||||
self.OUTPUT_HTML.format(
|
||||
title=self.title,
|
||||
text=self.text,
|
||||
style=self.TRACEBACK_STYLE,
|
||||
body=self._generate_body(),
|
||||
),
|
||||
status=self.status,
|
||||
)
|
||||
|
||||
def minimal(self):
|
||||
return html(
|
||||
self.OUTPUT_HTML.format(
|
||||
title=self.title,
|
||||
text=self.text,
|
||||
style=self.TRACEBACK_STYLE,
|
||||
body="",
|
||||
),
|
||||
status=self.status,
|
||||
headers=self.headers,
|
||||
)
|
||||
|
||||
@property
|
||||
def text(self):
|
||||
return escape(super().text)
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
return escape(f"⚠️ {super().title}")
|
||||
|
||||
def _generate_body(self):
|
||||
_, exc_value, __ = sys.exc_info()
|
||||
exceptions = []
|
||||
while exc_value:
|
||||
exceptions.append(self._format_exc(exc_value))
|
||||
exc_value = exc_value.__cause__
|
||||
|
||||
traceback_html = self.TRACEBACK_BORDER.join(reversed(exceptions))
|
||||
appname = escape(self.request.app.name)
|
||||
name = escape(self.exception.__class__.__name__)
|
||||
value = escape(self.exception)
|
||||
path = escape(self.request.path)
|
||||
lines = [
|
||||
f"<h2>Traceback of {appname} (most recent call last):</h2>",
|
||||
f"{traceback_html}",
|
||||
"<div class=summary><p>",
|
||||
f"<b>{name}: {value}</b> while handling path <code>{path}</code>",
|
||||
"</div>",
|
||||
]
|
||||
return "\n".join(lines)
|
||||
|
||||
def _format_exc(self, exc):
|
||||
frames = extract_tb(exc.__traceback__)
|
||||
frame_html = "".join(
|
||||
self.TRACEBACK_LINE_HTML.format(frame) for frame in frames
|
||||
)
|
||||
return self.TRACEBACK_WRAPPER_HTML.format(
|
||||
exc_name=escape(exc.__class__.__name__),
|
||||
exc_value=escape(exc),
|
||||
frame_html=frame_html,
|
||||
)
|
||||
|
||||
|
||||
class TextRenderer(BaseRenderer):
|
||||
OUTPUT_TEXT = "{title}\n{bar}\n{text}\n\n{body}"
|
||||
SPACER = " "
|
||||
|
||||
def full(self):
|
||||
return text(
|
||||
self.OUTPUT_TEXT.format(
|
||||
title=self.title,
|
||||
text=self.text,
|
||||
bar=("=" * len(self.title)),
|
||||
body=self._generate_body(),
|
||||
),
|
||||
status=self.status,
|
||||
)
|
||||
|
||||
def minimal(self):
|
||||
return text(
|
||||
self.OUTPUT_TEXT.format(
|
||||
title=self.title,
|
||||
text=self.text,
|
||||
bar=("=" * len(self.title)),
|
||||
body="",
|
||||
),
|
||||
status=self.status,
|
||||
headers=self.headers,
|
||||
)
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
return f"⚠️ {super().title}"
|
||||
|
||||
def _generate_body(self):
|
||||
_, exc_value, __ = sys.exc_info()
|
||||
exceptions = []
|
||||
|
||||
# traceback_html = self.TRACEBACK_BORDER.join(reversed(exceptions))
|
||||
lines = [
|
||||
f"{self.exception.__class__.__name__}: {self.exception} while "
|
||||
f"handling path {self.request.path}",
|
||||
f"Traceback of {self.request.app.name} (most recent call last):\n",
|
||||
]
|
||||
|
||||
while exc_value:
|
||||
exceptions.append(self._format_exc(exc_value))
|
||||
exc_value = exc_value.__cause__
|
||||
|
||||
return "\n".join(lines + exceptions[::-1])
|
||||
|
||||
def _format_exc(self, exc):
|
||||
frames = "\n\n".join(
|
||||
[
|
||||
f"{self.SPACER * 2}File {frame.filename}, "
|
||||
f"line {frame.lineno}, in "
|
||||
f"{frame.name}\n{self.SPACER * 2}{frame.line}"
|
||||
for frame in extract_tb(exc.__traceback__)
|
||||
]
|
||||
)
|
||||
return f"{self.SPACER}{exc.__class__.__name__}: {exc}\n{frames}"
|
||||
|
||||
|
||||
class JSONRenderer(BaseRenderer):
|
||||
def full(self):
|
||||
output = self._generate_output(full=True)
|
||||
return json(output, status=self.status, dumps=dumps)
|
||||
|
||||
def minimal(self):
|
||||
output = self._generate_output(full=False)
|
||||
return json(output, status=self.status, dumps=dumps)
|
||||
|
||||
def _generate_output(self, *, full):
|
||||
output = {
|
||||
"description": self.title,
|
||||
"status": self.status,
|
||||
"message": self.text,
|
||||
}
|
||||
|
||||
if full:
|
||||
_, exc_value, __ = sys.exc_info()
|
||||
exceptions = []
|
||||
|
||||
while exc_value:
|
||||
exceptions.append(
|
||||
{
|
||||
"type": exc_value.__class__.__name__,
|
||||
"exception": str(exc_value),
|
||||
"frames": [
|
||||
{
|
||||
"file": frame.filename,
|
||||
"line": frame.lineno,
|
||||
"name": frame.name,
|
||||
"src": frame.line,
|
||||
}
|
||||
for frame in extract_tb(exc_value.__traceback__)
|
||||
],
|
||||
}
|
||||
)
|
||||
exc_value = exc_value.__cause__
|
||||
|
||||
output["path"] = self.request.path
|
||||
output["args"] = self.request.args
|
||||
output["exceptions"] = exceptions[::-1]
|
||||
|
||||
return output
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
return STATUS_CODES.get(self.status, b"Error Occurred").decode()
|
||||
|
||||
|
||||
def escape(text):
|
||||
@@ -15,103 +285,46 @@ def escape(text):
|
||||
return f"{text}".replace("&", "&").replace("<", "<")
|
||||
|
||||
|
||||
def exception_response(request, exception, debug):
|
||||
status = 500
|
||||
text = (
|
||||
"The server encountered an internal error "
|
||||
"and cannot complete your request."
|
||||
)
|
||||
RENDERERS_BY_CONFIG = {
|
||||
"html": HTMLRenderer,
|
||||
"json": JSONRenderer,
|
||||
"text": TextRenderer,
|
||||
}
|
||||
|
||||
headers = {}
|
||||
if isinstance(exception, SanicException):
|
||||
text = f"{exception}"
|
||||
status = getattr(exception, "status_code", status)
|
||||
headers = getattr(exception, "headers", headers)
|
||||
elif debug:
|
||||
text = f"{exception}"
|
||||
|
||||
status_text = STATUS_CODES.get(status, b"Error Occurred").decode()
|
||||
title = escape(f"{status} — {status_text}")
|
||||
text = escape(text)
|
||||
|
||||
if debug and not getattr(exception, "quiet", False):
|
||||
return html(
|
||||
f"<!DOCTYPE html><meta charset=UTF-8><title>{title}</title>"
|
||||
f"<style>{TRACEBACK_STYLE}</style>\n"
|
||||
f"<h1>⚠️ {title}</h1><p>{text}\n"
|
||||
f"{_render_traceback_html(request, exception)}",
|
||||
status=status,
|
||||
)
|
||||
|
||||
# Keeping it minimal with trailing newline for pretty curl/console output
|
||||
return html(
|
||||
f"<!DOCTYPE html><meta charset=UTF-8><title>{title}</title>"
|
||||
"<style>html { font-family: sans-serif }</style>\n"
|
||||
f"<h1>⚠️ {title}</h1><p>{text}\n",
|
||||
status=status,
|
||||
headers=headers,
|
||||
)
|
||||
RENDERERS_BY_CONTENT_TYPE = {
|
||||
"multipart/form-data": HTMLRenderer,
|
||||
"application/json": JSONRenderer,
|
||||
"text/plain": TextRenderer,
|
||||
}
|
||||
|
||||
|
||||
def _render_exception(exception):
|
||||
frames = extract_tb(exception.__traceback__)
|
||||
frame_html = "".join(TRACEBACK_LINE_HTML.format(frame) for frame in frames)
|
||||
return TRACEBACK_WRAPPER_HTML.format(
|
||||
exc_name=escape(exception.__class__.__name__),
|
||||
exc_value=escape(exception),
|
||||
frame_html=frame_html,
|
||||
)
|
||||
def exception_response(
|
||||
request: Request,
|
||||
exception: Exception,
|
||||
debug: bool,
|
||||
renderer: t.Type[t.Optional[BaseRenderer]] = None,
|
||||
) -> HTTPResponse:
|
||||
"""Render a response for the default FALLBACK exception handler"""
|
||||
|
||||
if not renderer:
|
||||
renderer = HTMLRenderer
|
||||
|
||||
def _render_traceback_html(request, exception):
|
||||
exc_type, exc_value, tb = sys.exc_info()
|
||||
exceptions = []
|
||||
while exc_value:
|
||||
exceptions.append(_render_exception(exc_value))
|
||||
exc_value = exc_value.__cause__
|
||||
if request:
|
||||
if request.app.config.FALLBACK_ERROR_FORMAT == "auto":
|
||||
try:
|
||||
renderer = JSONRenderer if request.json else HTMLRenderer
|
||||
except InvalidUsage:
|
||||
renderer = HTMLRenderer
|
||||
|
||||
traceback_html = TRACEBACK_BORDER.join(reversed(exceptions))
|
||||
appname = escape(request.app.name)
|
||||
name = escape(exception.__class__.__name__)
|
||||
value = escape(exception)
|
||||
path = escape(request.path)
|
||||
return (
|
||||
f"<h2>Traceback of {appname} (most recent call last):</h2>"
|
||||
f"{traceback_html}"
|
||||
"<div class=summary><p>"
|
||||
f"<b>{name}: {value}</b> while handling path <code>{path}</code>"
|
||||
)
|
||||
content_type, *_ = request.headers.get(
|
||||
"content-type", ""
|
||||
).split(";")
|
||||
renderer = RENDERERS_BY_CONTENT_TYPE.get(
|
||||
content_type, renderer
|
||||
)
|
||||
else:
|
||||
render_format = request.app.config.FALLBACK_ERROR_FORMAT
|
||||
renderer = RENDERERS_BY_CONFIG.get(render_format, renderer)
|
||||
|
||||
|
||||
TRACEBACK_STYLE = """
|
||||
html { font-family: sans-serif }
|
||||
h2 { color: #888; }
|
||||
.tb-wrapper p { margin: 0 }
|
||||
.frame-border { margin: 1rem }
|
||||
.frame-line > * { padding: 0.3rem 0.6rem }
|
||||
.frame-line { margin-bottom: 0.3rem }
|
||||
.frame-code { font-size: 16px; padding-left: 4ch }
|
||||
.tb-wrapper { border: 1px solid #eee }
|
||||
.tb-header { background: #eee; padding: 0.3rem; font-weight: bold }
|
||||
.frame-descriptor { background: #e2eafb; font-size: 14px }
|
||||
"""
|
||||
|
||||
TRACEBACK_WRAPPER_HTML = (
|
||||
"<div class=tb-header>{exc_name}: {exc_value}</div>"
|
||||
"<div class=tb-wrapper>{frame_html}</div>"
|
||||
)
|
||||
|
||||
TRACEBACK_BORDER = (
|
||||
"<div class=frame-border>"
|
||||
"The above exception was the direct cause of the following exception:"
|
||||
"</div>"
|
||||
)
|
||||
|
||||
TRACEBACK_LINE_HTML = (
|
||||
"<div class=frame-line>"
|
||||
"<p class=frame-descriptor>"
|
||||
"File {0.filename}, line <i>{0.lineno}</i>, "
|
||||
"in <code><b>{0.name}</b></code>"
|
||||
"<p class=frame-code><code>{0.line}</code>"
|
||||
"</div>"
|
||||
)
|
||||
renderer = t.cast(t.Type[BaseRenderer], renderer)
|
||||
return renderer(request, exception, debug).render()
|
||||
|
||||
@@ -169,6 +169,10 @@ class Unauthorized(SanicException):
|
||||
}
|
||||
|
||||
|
||||
class LoadFileException(SanicException):
|
||||
pass
|
||||
|
||||
|
||||
def abort(status_code, message=None):
|
||||
"""
|
||||
Raise an exception based on SanicException. Returns the HTTP response
|
||||
|
||||
@@ -42,7 +42,7 @@ class BaseHTTPResponse:
|
||||
body=b"",
|
||||
):
|
||||
""".. deprecated:: 20.3:
|
||||
This function is not public API and will be removed."""
|
||||
This function is not public API and will be removed."""
|
||||
|
||||
# self.headers get priority over content_type
|
||||
if self.content_type and "Content-Type" not in self.headers:
|
||||
@@ -249,7 +249,10 @@ def raw(
|
||||
:param content_type: the content type (string) of the response.
|
||||
"""
|
||||
return HTTPResponse(
|
||||
body=body, status=status, headers=headers, content_type=content_type,
|
||||
body=body,
|
||||
status=status,
|
||||
headers=headers,
|
||||
content_type=content_type,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -452,7 +452,7 @@ class Router:
|
||||
return route_handler, [], kwargs, route.uri, route.name
|
||||
|
||||
def is_stream_handler(self, request):
|
||||
""" Handler for request is stream or not.
|
||||
"""Handler for request is stream or not.
|
||||
:param request: Request object
|
||||
:return: bool
|
||||
"""
|
||||
|
||||
@@ -14,11 +14,13 @@ from ipaddress import ip_address
|
||||
from signal import SIG_IGN, SIGINT, SIGTERM, Signals
|
||||
from signal import signal as signal_func
|
||||
from time import time
|
||||
from typing import Dict, Type, Union
|
||||
|
||||
from httptools import HttpRequestParser # type: ignore
|
||||
from httptools.parser.errors import HttpParserError # type: ignore
|
||||
|
||||
from sanic.compat import Header, ctrlc_workaround_for_windows
|
||||
from sanic.config import Config
|
||||
from sanic.exceptions import (
|
||||
HeaderExpectationFailed,
|
||||
InvalidUsage,
|
||||
@@ -416,12 +418,13 @@ class HttpProtocol(asyncio.Protocol):
|
||||
async def stream_append(self):
|
||||
while self._body_chunks:
|
||||
body = self._body_chunks.popleft()
|
||||
if self.request.stream.is_full():
|
||||
self.transport.pause_reading()
|
||||
await self.request.stream.put(body)
|
||||
self.transport.resume_reading()
|
||||
else:
|
||||
await self.request.stream.put(body)
|
||||
if self.request:
|
||||
if self.request.stream.is_full():
|
||||
self.transport.pause_reading()
|
||||
await self.request.stream.put(body)
|
||||
self.transport.resume_reading()
|
||||
else:
|
||||
await self.request.stream.put(body)
|
||||
|
||||
def on_message_complete(self):
|
||||
# Entire request (headers and whole body) is received.
|
||||
@@ -844,6 +847,7 @@ def serve(
|
||||
app.asgi = False
|
||||
|
||||
connections = connections if connections is not None else set()
|
||||
protocol_kwargs = _build_protocol_kwargs(protocol, app.config)
|
||||
server = partial(
|
||||
protocol,
|
||||
loop=loop,
|
||||
@@ -852,6 +856,7 @@ def serve(
|
||||
app=app,
|
||||
state=state,
|
||||
unix=unix,
|
||||
**protocol_kwargs,
|
||||
)
|
||||
asyncio_server_kwargs = (
|
||||
asyncio_server_kwargs if asyncio_server_kwargs else {}
|
||||
@@ -948,6 +953,21 @@ def serve(
|
||||
remove_unix_socket(unix)
|
||||
|
||||
|
||||
def _build_protocol_kwargs(
|
||||
protocol: Type[HttpProtocol], config: Config
|
||||
) -> Dict[str, Union[int, float]]:
|
||||
if hasattr(protocol, "websocket_handshake"):
|
||||
return {
|
||||
"websocket_max_size": config.WEBSOCKET_MAX_SIZE,
|
||||
"websocket_max_queue": config.WEBSOCKET_MAX_QUEUE,
|
||||
"websocket_read_limit": config.WEBSOCKET_READ_LIMIT,
|
||||
"websocket_write_limit": config.WEBSOCKET_WRITE_LIMIT,
|
||||
"websocket_ping_timeout": config.WEBSOCKET_PING_TIMEOUT,
|
||||
"websocket_ping_interval": config.WEBSOCKET_PING_INTERVAL,
|
||||
}
|
||||
return {}
|
||||
|
||||
|
||||
def bind_socket(host: str, port: int, *, backlog=100) -> socket.socket:
|
||||
"""Create TCP server socket.
|
||||
:param host: IPv4, IPv6 or hostname may be specified
|
||||
|
||||
@@ -134,6 +134,8 @@ def register(
|
||||
threshold size to switch to file_stream()
|
||||
:param name: user defined name used for url_for
|
||||
:param content_type: user defined content type for header
|
||||
:return: registered static routes
|
||||
:rtype: List[sanic.router.Route]
|
||||
"""
|
||||
# If we're not trying to match a file directly,
|
||||
# serve from the folder
|
||||
@@ -155,10 +157,11 @@ def register(
|
||||
)
|
||||
)
|
||||
|
||||
app.route(
|
||||
_routes, _ = app.route(
|
||||
uri,
|
||||
methods=["GET", "HEAD"],
|
||||
name=name,
|
||||
host=host,
|
||||
strict_slashes=strict_slashes,
|
||||
)(_handler)
|
||||
return _routes
|
||||
|
||||
@@ -11,6 +11,8 @@ from sanic.response import text
|
||||
|
||||
|
||||
ASGI_HOST = "mockserver"
|
||||
ASGI_PORT = 1234
|
||||
ASGI_BASE_URL = f"http://{ASGI_HOST}:{ASGI_PORT}"
|
||||
HOST = "127.0.0.1"
|
||||
PORT = None
|
||||
|
||||
@@ -103,7 +105,9 @@ class SanicTestClient:
|
||||
|
||||
if self.port:
|
||||
server_kwargs = dict(
|
||||
host=host or self.host, port=self.port, **server_kwargs,
|
||||
host=host or self.host,
|
||||
port=self.port,
|
||||
**server_kwargs,
|
||||
)
|
||||
host, port = host or self.host, self.port
|
||||
else:
|
||||
@@ -193,24 +197,19 @@ async def app_call_with_return(self, scope, receive, send):
|
||||
return await asgi_app()
|
||||
|
||||
|
||||
class SanicASGIDispatch(httpx.ASGIDispatch):
|
||||
pass
|
||||
|
||||
|
||||
class SanicASGITestClient(httpx.AsyncClient):
|
||||
def __init__(
|
||||
self,
|
||||
app,
|
||||
base_url: str = f"http://{ASGI_HOST}",
|
||||
base_url: str = ASGI_BASE_URL,
|
||||
suppress_exceptions: bool = False,
|
||||
) -> None:
|
||||
app.__class__.__call__ = app_call_with_return
|
||||
app.asgi = True
|
||||
|
||||
self.app = app
|
||||
|
||||
dispatch = SanicASGIDispatch(app=app, client=(ASGI_HOST, PORT or 0))
|
||||
super().__init__(dispatch=dispatch, base_url=base_url)
|
||||
transport = httpx.ASGITransport(app=app, client=(ASGI_HOST, ASGI_PORT))
|
||||
super().__init__(transport=transport, base_url=base_url)
|
||||
|
||||
self.last_request = None
|
||||
|
||||
|
||||
99
sanic/utils.py
Normal file
99
sanic/utils.py
Normal file
@@ -0,0 +1,99 @@
|
||||
from importlib.util import module_from_spec, spec_from_file_location
|
||||
from os import environ as os_environ
|
||||
from re import findall as re_findall
|
||||
from typing import Union
|
||||
|
||||
from .exceptions import LoadFileException
|
||||
|
||||
|
||||
def str_to_bool(val: str) -> bool:
|
||||
"""Takes string and tries to turn it into bool as human would do.
|
||||
|
||||
If val is in case insensitive (
|
||||
"y", "yes", "yep", "yup", "t",
|
||||
"true", "on", "enable", "enabled", "1"
|
||||
) returns True.
|
||||
If val is in case insensitive (
|
||||
"n", "no", "f", "false", "off", "disable", "disabled", "0"
|
||||
) returns False.
|
||||
Else Raise ValueError."""
|
||||
|
||||
val = val.lower()
|
||||
if val in {
|
||||
"y",
|
||||
"yes",
|
||||
"yep",
|
||||
"yup",
|
||||
"t",
|
||||
"true",
|
||||
"on",
|
||||
"enable",
|
||||
"enabled",
|
||||
"1",
|
||||
}:
|
||||
return True
|
||||
elif val in {"n", "no", "f", "false", "off", "disable", "disabled", "0"}:
|
||||
return False
|
||||
else:
|
||||
raise ValueError(f"Invalid truth value {val}")
|
||||
|
||||
|
||||
def load_module_from_file_location(
|
||||
location: Union[bytes, str], encoding: str = "utf8", *args, **kwargs
|
||||
):
|
||||
"""Returns loaded module provided as a file path.
|
||||
|
||||
:param args:
|
||||
Coresponds to importlib.util.spec_from_file_location location
|
||||
parameters,but with this differences:
|
||||
- It has to be of a string or bytes type.
|
||||
- You can also use here environment variables
|
||||
in format ${some_env_var}.
|
||||
Mark that $some_env_var will not be resolved as environment variable.
|
||||
:encoding:
|
||||
If location parameter is of a bytes type, then use this encoding
|
||||
to decode it into string.
|
||||
:param args:
|
||||
Coresponds to the rest of importlib.util.spec_from_file_location
|
||||
parameters.
|
||||
:param kwargs:
|
||||
Coresponds to the rest of importlib.util.spec_from_file_location
|
||||
parameters.
|
||||
|
||||
For example You can:
|
||||
|
||||
some_module = load_module_from_file_location(
|
||||
"some_module_name",
|
||||
"/some/path/${some_env_var}"
|
||||
)
|
||||
"""
|
||||
|
||||
# 1) Parse location.
|
||||
if isinstance(location, bytes):
|
||||
location = location.decode(encoding)
|
||||
|
||||
# A) Check if location contains any environment variables
|
||||
# in format ${some_env_var}.
|
||||
env_vars_in_location = set(re_findall(r"\${(.+?)}", location))
|
||||
|
||||
# B) Check these variables exists in environment.
|
||||
not_defined_env_vars = env_vars_in_location.difference(os_environ.keys())
|
||||
if not_defined_env_vars:
|
||||
raise LoadFileException(
|
||||
"The following environment variables are not set: "
|
||||
f"{', '.join(not_defined_env_vars)}"
|
||||
)
|
||||
|
||||
# C) Substitute them in location.
|
||||
for env_var in env_vars_in_location:
|
||||
location = location.replace("${" + env_var + "}", os_environ[env_var])
|
||||
|
||||
# 2) Load and return module.
|
||||
name = location.split("/")[-1].split(".")[
|
||||
0
|
||||
] # get just the file name without path and .py extension
|
||||
_mod_spec = spec_from_file_location(name, location, *args, **kwargs)
|
||||
module = module_from_spec(_mod_spec)
|
||||
_mod_spec.loader.exec_module(module) # type: ignore
|
||||
|
||||
return module
|
||||
@@ -35,6 +35,8 @@ class WebSocketProtocol(HttpProtocol):
|
||||
websocket_max_queue=None,
|
||||
websocket_read_limit=2 ** 16,
|
||||
websocket_write_limit=2 ** 16,
|
||||
websocket_ping_interval=20,
|
||||
websocket_ping_timeout=20,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -45,6 +47,8 @@ class WebSocketProtocol(HttpProtocol):
|
||||
self.websocket_max_queue = websocket_max_queue
|
||||
self.websocket_read_limit = websocket_read_limit
|
||||
self.websocket_write_limit = websocket_write_limit
|
||||
self.websocket_ping_interval = websocket_ping_interval
|
||||
self.websocket_ping_timeout = websocket_ping_timeout
|
||||
|
||||
# timeouts make no sense for websocket routes
|
||||
def request_timeout_callback(self):
|
||||
@@ -119,6 +123,8 @@ class WebSocketProtocol(HttpProtocol):
|
||||
max_queue=self.websocket_max_queue,
|
||||
read_limit=self.websocket_read_limit,
|
||||
write_limit=self.websocket_write_limit,
|
||||
ping_interval=self.websocket_ping_interval,
|
||||
ping_timeout=self.websocket_ping_timeout,
|
||||
)
|
||||
# Following two lines are required for websockets 8.x
|
||||
self.websocket.is_client = False
|
||||
|
||||
@@ -174,7 +174,7 @@ class GunicornWorker(base.Worker):
|
||||
|
||||
@staticmethod
|
||||
def _create_ssl_context(cfg):
|
||||
""" Creates SSLContext instance for usage in asyncio.create_server.
|
||||
"""Creates SSLContext instance for usage in asyncio.create_server.
|
||||
See ssl.SSLSocket.__init__ for more details.
|
||||
"""
|
||||
ctx = ssl.SSLContext(cfg.ssl_version)
|
||||
|
||||
Reference in New Issue
Block a user