Merge branch 'master' into 20.12LTS

This commit is contained in:
Adam Hopkins 2020-12-28 23:21:46 +02:00 committed by GitHub
commit b66fb6f9e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 120 additions and 39 deletions

View File

@ -17,11 +17,11 @@ environment:
PYTHON_VERSION: "3.8.x"
PYTHON_ARCH: "64"
- TOXENV: py39-no-ext
PYTHON: "C:\\Python39-x64\\python"
PYTHONPATH: "C:\\Python39-x64"
PYTHON_VERSION: "3.9.x"
PYTHON_ARCH: "64"
# - TOXENV: py39-no-ext
# PYTHON: "C:\\Python39-x64\\python"
# PYTHONPATH: "C:\\Python39-x64"
# PYTHON_VERSION: "3.9.x"
# PYTHON_ARCH: "64"
init: SET "PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%"

View File

@ -60,3 +60,26 @@ Open the address `http://0.0.0.0:8000 <http://0.0.0.0:8000>`_ in your web browse
the message *Hello world!*.
You now have a working Sanic server!
5. Application registry
-----------------------
When you instantiate a Sanic instance, that can be retrieved at a later time from the Sanic app registry. This can be useful, for example, if you need to access your Sanic instance from a location where it is not otherwise accessible.
.. code-block:: python
# ./path/to/server.py
from sanic import Sanic
app = Sanic("my_awesome_server")
# ./path/to/somewhere_else.py
from sanic import Sanic
app = Sanic.get_app("my_awesome_server")
If you call ``Sanic.get_app("non-existing")`` on an app that does not exist, it will raise ``SanicException`` by default. You can, instead, force the method to return a new instance of ``Sanic`` with that name:
.. code-block:: python
app = Sanic.get_app("my_awesome_server", force_create=True)

View File

@ -133,7 +133,7 @@ which allows the handler function to work with any of the HTTP methods in the li
async def get_handler(request):
return text('GET request - {}'.format(request.args))
There is also an optional `host` argument (which can be a list or a string). This restricts a route to the host or hosts provided. If there is a also a route with no host, it will be the default.
There is also an optional `host` argument (which can be a list or a string). This restricts a route to the host or hosts provided. If there is also a route with no host, it will be the default.
.. code-block:: python

View File

@ -58,10 +58,6 @@ More information about
the available arguments to `httpx` can be found
[in the documentation for `httpx <https://www.encode.io/httpx/>`_.
Additionally, Sanic has an asynchronous testing client. The difference is that the async client will not stand up an
instance of your application, but will instead reach inside it using ASGI. All listeners and middleware are still
executed.
.. code-block:: python
@pytest.mark.asyncio
async def test_index_returns_200():

View File

@ -2,12 +2,11 @@ import logging
import logging.config
import os
import re
import warnings
from asyncio import CancelledError, Protocol, ensure_future, get_event_loop
from collections import defaultdict, deque
from functools import partial
from inspect import getmodulename, isawaitable, signature, stack
from inspect import isawaitable, signature
from socket import socket
from ssl import Purpose, SSLContext, create_default_context
from traceback import format_exc
@ -38,6 +37,9 @@ from sanic.websocket import ConnectionClosed, WebSocketProtocol
class Sanic:
_app_registry: Dict[str, "Sanic"] = {}
test_mode = False
def __init__(
self,
name=None,
@ -52,15 +54,10 @@ class Sanic:
# Get name from previous stack frame
if name is None:
warnings.warn(
"Sanic(name=None) is deprecated and None value support "
"for `name` will be removed in the next release. "
raise SanicException(
"Sanic instance cannot be unnamed. "
"Please use Sanic(name='your_application_name') instead.",
DeprecationWarning,
stacklevel=2,
)
frame_records = stack()[1]
name = getmodulename(frame_records[1])
# logging
if configure_logging:
@ -90,7 +87,8 @@ class Sanic:
self.named_response_middleware = {}
# Register alternative method names
self.go_fast = self.run
self.test_mode = False
self.__class__.register_app(self)
@property
def loop(self):
@ -1401,9 +1399,36 @@ class Sanic:
# -------------------------------------------------------------------- #
# 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)
# -------------------------------------------------------------------- #
# Class methods
# -------------------------------------------------------------------- #
@classmethod
def register_app(cls, app: "Sanic") -> None:
"""Register a Sanic instance"""
if not isinstance(app, cls):
raise SanicException("Registered app must be an instance of Sanic")
name = app.name
if name in cls._app_registry and not cls.test_mode:
raise SanicException(f'Sanic app name "{name}" already in use.')
cls._app_registry[name] = app
@classmethod
def get_app(cls, name: str, *, force_create: bool = False) -> "Sanic":
"""Retrieve an instantiated Sanic instance"""
try:
return cls._app_registry[name]
except KeyError:
if force_create:
return cls(name)
raise SanicException(f'Sanic app name "{name}" not found.')

View File

@ -13,6 +13,7 @@ from sanic.exceptions import (
InvalidUsage,
)
from sanic.handlers import ContentRangeHandler
from sanic.log import error_logger
from sanic.response import HTTPResponse, file, file_stream
@ -40,6 +41,10 @@ async def _static_request_handler(
# match filenames which got encoded (filenames with spaces etc)
file_path = path.abspath(unquote(file_path))
if not file_path.startswith(path.abspath(unquote(root_path))):
error_logger.exception(
f"File not found: path={file_or_directory}, "
f"relative_url={file_uri}"
)
raise FileNotFound(
"File not found", path=file_or_directory, relative_url=file_uri
)
@ -94,6 +99,10 @@ async def _static_request_handler(
except ContentRangeError:
raise
except Exception:
error_logger.exception(
f"File not found: path={file_or_directory}, "
f"relative_url={file_uri}"
)
raise FileNotFound(
"File not found", path=file_or_directory, relative_url=file_uri
)

View File

@ -5,6 +5,7 @@ import codecs
import os
import re
import sys
from distutils.util import strtobool
from setuptools import setup
@ -24,6 +25,7 @@ class PyTest(TestCommand):
def run_tests(self):
import shlex
import pytest
errno = pytest.main(shlex.split(self.pytest_args))
@ -38,7 +40,9 @@ def open_local(paths, mode="r", encoding="utf8"):
with open_local(["sanic", "__version__.py"], encoding="latin1") as fp:
try:
version = re.findall(r"^__version__ = \"([^']+)\"\r?$", fp.read(), re.M)[0]
version = re.findall(
r"^__version__ = \"([^']+)\"\r?$", fp.read(), re.M
)[0]
except IndexError:
raise RuntimeError("Unable to determine version.")
@ -72,7 +76,9 @@ setup_kwargs = {
"entry_points": {"console_scripts": ["sanic = sanic.__main__:main"]},
}
env_dependency = '; sys_platform != "win32" ' 'and implementation_name == "cpython"'
env_dependency = (
'; sys_platform != "win32" ' 'and implementation_name == "cpython"'
)
ujson = "ujson>=1.35" + env_dependency
uvloop = "uvloop>=0.5.3" + env_dependency
@ -89,9 +95,9 @@ requirements = [
tests_require = [
"pytest==5.2.1",
"multidict>=5.0,<6.0",
"gunicorn",
"gunicorn==20.0.4",
"pytest-cov",
"httpcore==0.3.0",
"httpcore==0.11.*",
"beautifulsoup4",
uvloop,
ujson,

View File

@ -11,6 +11,7 @@ from sanic.router import RouteExists, Router
random.seed("Pack my box with five dozen liquor jugs.")
Sanic.test_mode = True
if sys.platform in ["win32", "cygwin"]:
collect_ignore = ["test_worker.py"]

View File

@ -258,7 +258,7 @@ def test_handle_request_with_nested_sanic_exception(app, monkeypatch, caplog):
def test_app_name_required():
with pytest.deprecated_call():
with pytest.raises(SanicException):
Sanic()
@ -274,14 +274,35 @@ def test_app_has_test_mode_sync():
assert response.status == 200
# @pytest.mark.asyncio
# async def test_app_has_test_mode_async():
# app = Sanic("test")
def test_app_registry():
instance = Sanic("test")
assert Sanic._app_registry["test"] is instance
# @app.get("/")
# async def handler(request):
# assert request.app.test_mode
# return text("test")
# _, response = await app.asgi_client.get("/")
# assert response.status == 200
def test_app_registry_wrong_type():
with pytest.raises(SanicException):
Sanic.register_app(1)
def test_app_registry_name_reuse():
Sanic("test")
Sanic.test_mode = False
with pytest.raises(SanicException):
Sanic("test")
Sanic.test_mode = True
def test_app_registry_retrieval():
instance = Sanic("test")
assert Sanic.get_app("test") is instance
def test_get_app_does_not_exist():
with pytest.raises(SanicException):
Sanic.get_app("does-not-exist")
def test_get_app_does_not_exist_force_create():
assert isinstance(
Sanic.get_app("does-not-exist", force_create=True), Sanic
)

10
tox.ini
View File

@ -7,18 +7,18 @@ setenv =
{py36,py37,py38,py39,pyNightly}-no-ext: SANIC_NO_UJSON=1
{py36,py37,py38,py39,pyNightly}-no-ext: SANIC_NO_UVLOOP=1
deps =
coverage
coverage==5.3
pytest==5.2.1
pytest-cov
pytest-sanic
pytest-sugar
pytest-benchmark
pytest-dependency
httpcore==0.3.0
httpcore==0.11.*
httpx==0.15.4
chardet<=2.3.0
chardet==3.*
beautifulsoup4
gunicorn
gunicorn==20.0.4
uvicorn
websockets>=8.1,<9.0
commands =
@ -76,7 +76,7 @@ deps =
recommonmark>=0.5.0
docutils
pygments
gunicorn
gunicorn==20.0.4
commands =
make docs-test