Compare commits

..

30 Commits

Author SHA1 Message Date
Adam Hopkins
00218aa9f2 22.3 Internal version bumps (#2419) 2022-03-31 14:30:30 +03:00
Adam Hopkins
874718db94 Bump version and 22.3 changelog (#2418) 2022-03-30 15:09:45 +03:00
Javier Marcet
bb4474897f Fix "DeprecationWarning: There is no current event loop" (#2390)
Co-authored-by: Adam Hopkins <adam@amhopkins.com>
2022-03-30 09:40:30 +03:00
Adam Hopkins
0cb342aef4 Better exception for bad URL parse (#2415) 2022-03-25 00:22:12 +02:00
Ashley Sommer
030987480c Add config option to skip Touchup step, for debugging purposes (#2361)
Co-authored-by: Adam Hopkins <adam@amhopkins.com>
2022-03-24 13:52:05 +02:00
Robert Schütz
f6fdc80b40 allow multidict version 6 (#2396)
Co-authored-by: Adam Hopkins <adam@amhopkins.com>
2022-03-24 00:38:45 +02:00
Jonathan Vargas
361c242473 remove error_logger on websockets (#2373)
Co-authored-by: Adam Hopkins <adam@amhopkins.com>
2022-03-23 16:25:19 +02:00
André Ericson
32962d1e1c Fixing typing for ListenerMixin.listener (#2376)
Co-authored-by: Adam Hopkins <adam@amhopkins.com>
2022-03-23 15:34:33 +02:00
Adam Hopkins
6e0a6871b5 Upgrade tests for sanic-routing changes (#2405) 2022-03-23 13:43:36 +02:00
Adam Hopkins
0030425c8c Conditionally inject CLI arguments into factory (#2402) 2022-03-23 12:00:41 +02:00
Adam Hopkins
c9dbc8ed26 Remove loop as required listener arg (#2414) 2022-03-23 11:02:39 +02:00
Callum
44b108b564 Changes to CLI (#2401)
Co-authored-by: Callum Fleming <howzitcal@zohomail.com>
Co-authored-by: Adam Hopkins <adam@amhopkins.com>
2022-03-23 10:30:41 +02:00
Adam Hopkins
2a8e91052f Add two new events on the reloader process (#2413) 2022-03-22 23:29:39 +02:00
Bluenix
0c9df02e66 Add a docstring to Request.respond() (#2409)
Co-authored-by: Ryu juheon <saidbysolo@gmail.com>
Co-authored-by: Adam Hopkins <adam@amhopkins.com>
2022-03-14 13:10:49 +02:00
Arie Bovenberg
7523e87937 remove overlapping slots from app.Sanic, fix broken slots inherit of HTTPResponse (#2387) 2022-02-24 17:45:23 +02:00
Bluenix
d4fb44e986 Document middleware on_request and on_response (#2398) 2022-02-13 21:08:08 +02:00
Ryu juheon
68b654d981 fix(tasks): newly assigned `None` in registry (#2381) 2022-02-08 08:33:09 +02:00
Adam Hopkins
88bc6d8966 Upgrade black and isort changes (#2397) 2022-02-02 10:41:55 +02:00
Adam Hopkins
ac388d644b Downgrade warnings to backwater debug messages (#2382) 2022-01-19 14:26:45 +02:00
Ryu juheon
bb517ddcca fix: deprecation warning in `asyncio.wait` (#2383) 2022-01-19 08:09:17 +02:00
Adam Hopkins
b8d991420b Sanic multi-application server (#2347) 2022-01-16 09:03:04 +02:00
Adam Hopkins
4a416e177a Updates to CLI help messaging (#2372) 2022-01-14 00:54:51 +02:00
Adam Hopkins
8dfa49b648 22.3 Deprecations and changes (#2362) 2022-01-12 16:28:43 +02:00
Adam Hopkins
8b0eaa097c Change back to codecov (#2363) 2022-01-09 12:22:09 +02:00
Sergey Rybakov
101151b419 Add credentials property to Request objects (#2357) 2022-01-06 19:14:52 +02:00
Adam Hopkins
4669036f45 Mergeback of 21.12.1 (#2358)
Co-authored-by: Néstor Pérez <25409753+prryplatypus@users.noreply.github.com>
Co-authored-by: Ryu juheon <saidbysolo@gmail.com>
2022-01-06 12:40:52 +02:00
raphaelauv
9bf9067c99 [FIX] README ASGI link (#2350) 2022-01-04 06:39:59 +02:00
Adam Hopkins
a7bc8b56ba Set dev version 2021-12-26 22:19:39 +02:00
Zhiwei
371985d129 deprecation warning for not catch lowercase env var (#2344) 2021-12-26 21:47:15 +02:00
Adam Hopkins
3eae00898d Set setuptools version for RTD 2021-12-26 14:25:09 +02:00
72 changed files with 2557 additions and 1467 deletions

View File

@@ -1,28 +0,0 @@
exclude_patterns:
- "sanic/__main__.py"
- "sanic/application/logo.py"
- "sanic/application/motd.py"
- "sanic/reloader_helpers.py"
- "sanic/simple.py"
- "sanic/utils.py"
- ".github/"
- "changelogs/"
- "docker/"
- "docs/"
- "examples/"
- "scripts/"
- "tests/"
checks:
argument-count:
enabled: false
file-lines:
config:
threshold: 1000
method-count:
config:
threshold: 40
complex-logic:
enabled: false
method-complexity:
config:
threshold: 10

View File

@@ -3,13 +3,12 @@ branch = True
source = sanic source = sanic
omit = omit =
site-packages site-packages
sanic/application/logo.py
sanic/application/motd.py
sanic/cli
sanic/__main__.py sanic/__main__.py
sanic/compat.py
sanic/reloader_helpers.py sanic/reloader_helpers.py
sanic/simple.py sanic/simple.py
sanic/utils.py sanic/utils.py
sanic/cli
[html] [html]
directory = coverage directory = coverage
@@ -21,3 +20,12 @@ exclude_lines =
noqa noqa
NOQA NOQA
pragma: no cover pragma: no cover
omit =
site-packages
sanic/__main__.py
sanic/compat.py
sanic/reloader_helpers.py
sanic/simple.py
sanic/utils.py
sanic/cli
skip_empty = True

View File

@@ -7,10 +7,11 @@ on:
tags: tags:
- "!*" # Do not execute on tags - "!*" # Do not execute on tags
pull_request: pull_request:
types: [opened, synchronize, reopened, ready_for_review] branches:
- main
- "*LTS"
jobs: jobs:
test: test:
if: github.event.pull_request.draft == false
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
@@ -20,7 +21,6 @@ jobs:
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-python@v1 - uses: actions/setup-python@v1
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
@@ -29,9 +29,10 @@ jobs:
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install tox pip install tox
- uses: paambaati/codeclimate-action@v2.5.3 - name: Run coverage
if: always() run: tox -e coverage
env: continue-on-error: true
CC_TEST_REPORTER_ID: ${{ secrets.CODECLIMATE }} - uses: codecov/codecov-action@v2
with: with:
coverageCommand: tox -e coverage files: ./coverage.xml
fail_ci_if_error: false

View File

@@ -140,6 +140,7 @@ To maintain the code consistency, Sanic uses following tools.
#. `isort <https://github.com/timothycrosley/isort>`_ #. `isort <https://github.com/timothycrosley/isort>`_
#. `black <https://github.com/python/black>`_ #. `black <https://github.com/python/black>`_
#. `flake8 <https://github.com/PyCQA/flake8>`_ #. `flake8 <https://github.com/PyCQA/flake8>`_
#. `slotscheck <https://github.com/ariebovenberg/slotscheck>`_
isort isort
***** *****
@@ -167,7 +168,13 @@ flake8
#. pycodestyle #. pycodestyle
#. Ned Batchelder's McCabe script #. Ned Batchelder's McCabe script
``isort``\ , ``black`` and ``flake8`` checks are performed during ``tox`` lint checks. slotscheck
**********
``slotscheck`` ensures that there are no problems with ``__slots__``
(e.g. overlaps, or missing slots in base classes).
``isort``\ , ``black``\ , ``flake8`` and ``slotscheck`` checks are performed during ``tox`` lint checks.
The **easiest** way to make your code conform is to run the following before committing. The **easiest** way to make your code conform is to run the following before committing.

View File

@@ -66,7 +66,7 @@ Sanic | Build fast. Run fast.
Sanic is a **Python 3.7+** web server and web framework that's written to go fast. It allows the usage of the ``async/await`` syntax added in Python 3.5, which makes your code non-blocking and speedy. Sanic is a **Python 3.7+** web server and web framework that's written to go fast. It allows the usage of the ``async/await`` syntax added in Python 3.5, which makes your code non-blocking and speedy.
Sanic is also ASGI compliant, so you can deploy it with an `alternative ASGI webserver <https://sanic.readthedocs.io/en/latest/sanic/deploying.html#running-via-asgi>`_. Sanic is also ASGI compliant, so you can deploy it with an `alternative ASGI webserver <https://sanicframework.org/en/guide/deployment/running.html#asgi>`_.
`Source code on GitHub <https://github.com/sanic-org/sanic/>`_ | `Help and discussion board <https://community.sanicframework.org/>`_ | `User Guide <https://sanicframework.org>`_ | `Chat on Discord <https://discord.gg/FARQzAEMAA>`_ `Source code on GitHub <https://github.com/sanic-org/sanic/>`_ | `Help and discussion board <https://community.sanicframework.org/>`_ | `User Guide <https://sanicframework.org>`_ | `Chat on Discord <https://discord.gg/FARQzAEMAA>`_

View File

@@ -4,7 +4,6 @@ coverage:
default: default:
target: auto target: auto
threshold: 0.75 threshold: 0.75
informational: true
project: project:
default: default:
target: auto target: auto

View File

@@ -1,6 +1,7 @@
📜 Changelog 📜 Changelog
============ ============
.. mdinclude:: ./releases/22/22.3.md
.. mdinclude:: ./releases/21/21.12.md .. mdinclude:: ./releases/21/21.12.md
.. mdinclude:: ./releases/21/21.9.md .. mdinclude:: ./releases/21/21.9.md
.. include:: ../../CHANGELOG.rst .. include:: ../../CHANGELOG.rst

View File

@@ -0,0 +1,52 @@
## Version 22.3.0
### Features
- [#2347](https://github.com/sanic-org/sanic/pull/2347) API for multi-application server
- 🚨 *BREAKING CHANGE*: The old `sanic.worker.GunicornWorker` has been **removed**. To run Sanic with `gunicorn`, you should use it thru `uvicorn` [as described in their docs](https://www.uvicorn.org/#running-with-gunicorn).
- 🧁 *SIDE EFFECT*: Named background tasks are now supported, even in Python 3.7
- [#2357](https://github.com/sanic-org/sanic/pull/2357) Parse `Authorization` header as `Request.credentials`
- [#2361](https://github.com/sanic-org/sanic/pull/2361) Add config option to skip `Touchup` step in application startup
- [#2372](https://github.com/sanic-org/sanic/pull/2372) Updates to CLI help messaging
- [#2382](https://github.com/sanic-org/sanic/pull/2382) Downgrade warnings to backwater debug messages
- [#2396](https://github.com/sanic-org/sanic/pull/2396) Allow for `multidict` v0.6
- [#2401](https://github.com/sanic-org/sanic/pull/2401) Upgrade CLI catching for alternative application run types
- [#2402](https://github.com/sanic-org/sanic/pull/2402) Conditionally inject CLI arguments into factory
- [#2413](https://github.com/sanic-org/sanic/pull/2413) Add new start and stop event listeners to reloader process
- [#2414](https://github.com/sanic-org/sanic/pull/2414) Remove loop as required listener arg
- [#2415](https://github.com/sanic-org/sanic/pull/2415) Better exception for bad URL parsing
- [sanic-routing#47](https://github.com/sanic-org/sanic-routing/pull/47) Add a new extention parameter type: `<file:ext>`, `<file:ext=jpg>`, `<file:ext=jpg|png|gif|svg>`, `<file=int:ext>`, `<file=int:ext=jpg|png|gif|svg>`, `<file=float:ext=tar.gz>`
- 👶 *BETA FEATURE*: This feature will not work with `path` type matching, and is being released as a beta feature only.
- [sanic-routing#57](https://github.com/sanic-org/sanic-routing/pull/57) Change `register_pattern` to accept a `str` or `Pattern`
- [sanic-routing#58](https://github.com/sanic-org/sanic-routing/pull/58) Default matching on non-empty strings only, and new `strorempty` pattern type
- 🚨 *BREAKING CHANGE*: Previously a route with a dynamic string parameter (`/<foo>` or `/<foo:str>`) would match on any string, including empty strings. It will now **only** match a non-empty string. To retain the old behavior, you should use the new parameter type: `/<foo:strorempty>`.
### Bugfixes
- [#2373](https://github.com/sanic-org/sanic/pull/2373) Remove `error_logger` on websockets
- [#2381](https://github.com/sanic-org/sanic/pull/2381) Fix newly assigned `None` in task registry
- [sanic-routing#52](https://github.com/sanic-org/sanic-routing/pull/52) Add type casting to regex route matching
- [sanic-routing#60](https://github.com/sanic-org/sanic-routing/pull/60) Add requirements check on regex routes (this resolves, for example, multiple static directories with differing `host` values)
### Deprecations and Removals
- [#2362](https://github.com/sanic-org/sanic/pull/2362) 22.3 Deprecations and changes
1. `debug=True` and `--debug` do _NOT_ automatically run `auto_reload`
2. Default error render is with plain text (browsers still get HTML by default because `auto` looks at headers)
3. `config` is required for `ErrorHandler.finalize`
4. `ErrorHandler.lookup` requires two positional args
5. Unused websocket protocol args removed
- [#2344](https://github.com/sanic-org/sanic/pull/2344) Deprecate loading of lowercase environment variables
### Developer infrastructure
- [#2363](https://github.com/sanic-org/sanic/pull/2363) Revert code coverage back to Codecov
- [#2405](https://github.com/sanic-org/sanic/pull/2405) Upgrade tests for `sanic-routing` changes
- [sanic-testing#35](https://github.com/sanic-org/sanic-testing/pull/35) Allow for httpx v0.22
### Improved Documentation
- [#2350](https://github.com/sanic-org/sanic/pull/2350) Fix link in README for ASGI
- [#2398](https://github.com/sanic-org/sanic/pull/2398) Document middleware on_request and on_response
- [#2409](https://github.com/sanic-org/sanic/pull/2409) Add missing documentation for `Request.respond`
### Miscellaneous
- [#2376](https://github.com/sanic-org/sanic/pull/2376) Fix typing for `ListenerMixin.listener`
- [#2383](https://github.com/sanic-org/sanic/pull/2383) Clear deprecation warning in `asyncio.wait`
- [#2387](https://github.com/sanic-org/sanic/pull/2387) Cleanup `__slots__` implementations
- [#2390](https://github.com/sanic-org/sanic/pull/2390) Clear deprecation warning in `asyncio.get_event_loop`

View File

@@ -4,6 +4,7 @@ from sanic import Sanic, response
app = Sanic("DelayedResponseApp", strict_slashes=True) app = Sanic("DelayedResponseApp", strict_slashes=True)
app.config.AUTO_EXTEND = False
@app.get("/") @app.get("/")
@@ -11,7 +12,7 @@ async def handler(request):
return response.redirect("/sleep/3") return response.redirect("/sleep/3")
@app.get("/sleep/<t:number>") @app.get("/sleep/<t:float>")
async def handler2(request, t=0.3): async def handler2(request, t=0.3):
await sleep(t) await sleep(t)
return response.text(f"Slept {t:.1f} seconds.\n") return response.text(f"Slept {t:.1f} seconds.\n")

View File

@@ -1,3 +1,3 @@
[build-system] [build-system]
requires = ["setuptools", "wheel"] requires = ["setuptools<60.0", "wheel"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"

View File

@@ -6,4 +6,4 @@ python:
path: . path: .
extra_requirements: extra_requirements:
- docs - docs
system_packages: true system_packages: true

View File

@@ -1 +1 @@
__version__ = "21.12.2" __version__ = "22.3.0"

View File

@@ -3,28 +3,23 @@ from __future__ import annotations
import asyncio import asyncio
import logging import logging
import logging.config import logging.config
import os
import platform
import re import re
import sys import sys
from asyncio import ( from asyncio import (
AbstractEventLoop, AbstractEventLoop,
CancelledError, CancelledError,
Protocol,
Task, Task,
ensure_future, ensure_future,
get_event_loop, get_running_loop,
wait_for, wait_for,
) )
from asyncio.futures import Future from asyncio.futures import Future
from collections import defaultdict, deque from collections import defaultdict, deque
from contextlib import suppress
from functools import partial from functools import partial
from importlib import import_module
from inspect import isawaitable from inspect import isawaitable
from pathlib import Path
from socket import socket from socket import socket
from ssl import SSLContext
from traceback import format_exc from traceback import format_exc
from types import SimpleNamespace from types import SimpleNamespace
from typing import ( from typing import (
@@ -54,11 +49,8 @@ from sanic_routing.exceptions import ( # type: ignore
) )
from sanic_routing.route import Route # type: ignore from sanic_routing.route import Route # type: ignore
from sanic import reloader_helpers
from sanic.application.ext import setup_ext from sanic.application.ext import setup_ext
from sanic.application.logo import get_logo from sanic.application.state import ApplicationState, Mode, ServerStage
from sanic.application.motd import MOTD
from sanic.application.state import ApplicationState, Mode
from sanic.asgi import ASGIApp from sanic.asgi import ASGIApp
from sanic.base.root import BaseSanic from sanic.base.root import BaseSanic
from sanic.blueprint_group import BlueprintGroup from sanic.blueprint_group import BlueprintGroup
@@ -72,16 +64,15 @@ from sanic.exceptions import (
URLBuildError, URLBuildError,
) )
from sanic.handlers import ErrorHandler from sanic.handlers import ErrorHandler
from sanic.helpers import _default
from sanic.http import Stage from sanic.http import Stage
from sanic.log import ( from sanic.log import (
LOGGING_CONFIG_DEFAULTS, LOGGING_CONFIG_DEFAULTS,
Colors,
deprecation, deprecation,
error_logger, error_logger,
logger, logger,
) )
from sanic.mixins.listeners import ListenerEvent from sanic.mixins.listeners import ListenerEvent
from sanic.mixins.runner import RunnerMixin
from sanic.models.futures import ( from sanic.models.futures import (
FutureException, FutureException,
FutureListener, FutureListener,
@@ -96,13 +87,8 @@ from sanic.models.handler_types import Sanic as SanicVar
from sanic.request import Request from sanic.request import Request
from sanic.response import BaseHTTPResponse, HTTPResponse, ResponseStream from sanic.response import BaseHTTPResponse, HTTPResponse, ResponseStream
from sanic.router import Router from sanic.router import Router
from sanic.server import AsyncioServer, HttpProtocol
from sanic.server import Signal as ServerSignal
from sanic.server import serve, serve_multiple, serve_single, try_use_uvloop
from sanic.server.protocols.websocket_protocol import WebSocketProtocol
from sanic.server.websockets.impl import ConnectionClosed from sanic.server.websockets.impl import ConnectionClosed
from sanic.signals import Signal, SignalRouter from sanic.signals import Signal, SignalRouter
from sanic.tls import process_to_context
from sanic.touchup import TouchUp, TouchUpMeta from sanic.touchup import TouchUp, TouchUpMeta
@@ -114,15 +100,13 @@ if TYPE_CHECKING: # no cov
Extend = TypeVar("Extend") # type: ignore Extend = TypeVar("Extend") # type: ignore
if OS_IS_WINDOWS: if OS_IS_WINDOWS: # no cov
enable_windows_color_support() enable_windows_color_support()
filterwarnings("once", category=DeprecationWarning) filterwarnings("once", category=DeprecationWarning)
SANIC_PACKAGES = ("sanic-routing", "sanic-testing", "sanic-ext")
class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
class Sanic(BaseSanic, metaclass=TouchUpMeta):
""" """
The main application instance The main application instance
""" """
@@ -157,7 +141,6 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
"error_handler", "error_handler",
"go_fast", "go_fast",
"listeners", "listeners",
"name",
"named_request_middleware", "named_request_middleware",
"named_response_middleware", "named_response_middleware",
"request_class", "request_class",
@@ -221,7 +204,6 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
self.blueprints: Dict[str, Blueprint] = {} self.blueprints: Dict[str, Blueprint] = {}
self.configure_logging: bool = configure_logging self.configure_logging: bool = configure_logging
self.ctx: Any = ctx or SimpleNamespace() self.ctx: Any = ctx or SimpleNamespace()
self.debug = False
self.error_handler: ErrorHandler = error_handler or ErrorHandler() self.error_handler: ErrorHandler = error_handler or ErrorHandler()
self.listeners: Dict[str, List[ListenerType[Any]]] = defaultdict(list) self.listeners: Dict[str, List[ListenerType[Any]]] = defaultdict(list)
self.named_request_middleware: Dict[str, Deque[MiddlewareType]] = {} self.named_request_middleware: Dict[str, Deque[MiddlewareType]] = {}
@@ -265,12 +247,12 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
Only supported when using the `app.run` method. Only supported when using the `app.run` method.
""" """
if not self.is_running and self.asgi is False: if self.state.stage is ServerStage.STOPPED and self.asgi is False:
raise SanicException( raise SanicException(
"Loop can only be retrieved after the app has started " "Loop can only be retrieved after the app has started "
"running. Not supported with `create_server` function" "running. Not supported with `create_server` function"
) )
return get_event_loop() return get_running_loop()
# -------------------------------------------------------------------- # # -------------------------------------------------------------------- #
# Registration # Registration
@@ -1052,283 +1034,6 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
# Execution # Execution
# -------------------------------------------------------------------- # # -------------------------------------------------------------------- #
def make_coffee(self, *args, **kwargs):
self.state.coffee = True
self.run(*args, **kwargs)
def run(
self,
host: Optional[str] = None,
port: Optional[int] = None,
*,
debug: bool = False,
auto_reload: Optional[bool] = None,
ssl: Union[None, SSLContext, dict, str, list, tuple] = None,
sock: Optional[socket] = None,
workers: int = 1,
protocol: Optional[Type[Protocol]] = None,
backlog: int = 100,
register_sys_signals: bool = True,
access_log: Optional[bool] = None,
unix: Optional[str] = None,
loop: AbstractEventLoop = None,
reload_dir: Optional[Union[List[str], str]] = None,
noisy_exceptions: Optional[bool] = None,
motd: bool = True,
fast: bool = False,
verbosity: int = 0,
motd_display: Optional[Dict[str, str]] = None,
) -> None:
"""
Run the HTTP Server and listen until keyboard interrupt or term
signal. On termination, drain connections before closing.
:param host: Address to host on
:type host: str
:param port: Port to host on
:type port: int
:param debug: Enables debug output (slows server)
:type debug: bool
:param auto_reload: Reload app whenever its source code is changed.
Enabled by default in debug mode.
:type auto_relaod: bool
:param ssl: SSLContext, or location of certificate and key
for SSL encryption of worker(s)
:type ssl: str, dict, SSLContext or list
:param sock: Socket for the server to accept connections from
:type sock: socket
:param workers: Number of processes received before it is respected
:type workers: int
:param protocol: Subclass of asyncio Protocol class
:type protocol: type[Protocol]
:param backlog: a number of unaccepted connections that the system
will allow before refusing new connections
:type backlog: int
:param register_sys_signals: Register SIG* events
:type register_sys_signals: bool
:param access_log: Enables writing access logs (slows server)
:type access_log: bool
:param unix: Unix socket to listen on instead of TCP port
:type unix: str
:param noisy_exceptions: Log exceptions that are normally considered
to be quiet/silent
:type noisy_exceptions: bool
:return: Nothing
"""
self.state.verbosity = verbosity
if fast and workers != 1:
raise RuntimeError("You cannot use both fast=True and workers=X")
if motd_display:
self.config.MOTD_DISPLAY.update(motd_display)
if reload_dir:
if isinstance(reload_dir, str):
reload_dir = [reload_dir]
for directory in reload_dir:
direc = Path(directory)
if not direc.is_dir():
logger.warning(
f"Directory {directory} could not be located"
)
self.state.reload_dirs.add(Path(directory))
if loop is not None:
raise TypeError(
"loop is not a valid argument. To use an existing loop, "
"change to create_server().\nSee more: "
"https://sanic.readthedocs.io/en/latest/sanic/deploying.html"
"#asynchronous-support"
)
if auto_reload or auto_reload is None and debug:
auto_reload = True
if os.environ.get("SANIC_SERVER_RUNNING") != "true":
return reloader_helpers.watchdog(1.0, self)
if sock is None:
host, port = host or "127.0.0.1", port or 8000
if protocol is None:
protocol = (
WebSocketProtocol if self.websocket_enabled else HttpProtocol
)
# Set explicitly passed configuration values
for attribute, value in {
"ACCESS_LOG": access_log,
"AUTO_RELOAD": auto_reload,
"MOTD": motd,
"NOISY_EXCEPTIONS": noisy_exceptions,
}.items():
if value is not None:
setattr(self.config, attribute, value)
if fast:
self.state.fast = True
try:
workers = len(os.sched_getaffinity(0))
except AttributeError:
workers = os.cpu_count() or 1
server_settings = self._helper(
host=host,
port=port,
debug=debug,
ssl=ssl,
sock=sock,
unix=unix,
workers=workers,
protocol=protocol,
backlog=backlog,
register_sys_signals=register_sys_signals,
)
if self.config.USE_UVLOOP is True or (
self.config.USE_UVLOOP is _default and not OS_IS_WINDOWS
):
try_use_uvloop()
try:
self.is_running = True
self.is_stopping = False
if workers > 1 and os.name != "posix":
logger.warn(
f"Multiprocessing is currently not supported on {os.name},"
" using workers=1 instead"
)
workers = 1
if workers == 1:
serve_single(server_settings)
else:
serve_multiple(server_settings, workers)
except BaseException:
error_logger.exception(
"Experienced exception while trying to serve"
)
raise
finally:
self.is_running = False
logger.info("Server Stopped")
def stop(self):
"""
This kills the Sanic
"""
if not self.is_stopping:
self.shutdown_tasks(timeout=0)
self.is_stopping = True
get_event_loop().stop()
async def create_server(
self,
host: Optional[str] = None,
port: Optional[int] = None,
*,
debug: bool = False,
ssl: Union[None, SSLContext, dict, str, list, tuple] = None,
sock: Optional[socket] = None,
protocol: Type[Protocol] = None,
backlog: int = 100,
access_log: Optional[bool] = None,
unix: Optional[str] = None,
return_asyncio_server: bool = False,
asyncio_server_kwargs: Dict[str, Any] = None,
noisy_exceptions: Optional[bool] = None,
) -> Optional[AsyncioServer]:
"""
Asynchronous version of :func:`run`.
This method will take care of the operations necessary to invoke
the *before_start* events via :func:`trigger_events` method invocation
before starting the *sanic* app in Async mode.
.. note::
This does not support multiprocessing and is not the preferred
way to run a :class:`Sanic` application.
:param host: Address to host on
:type host: str
:param port: Port to host on
:type port: int
:param debug: Enables debug output (slows server)
:type debug: bool
:param ssl: SSLContext, or location of certificate and key
for SSL encryption of worker(s)
:type ssl: SSLContext or dict
:param sock: Socket for the server to accept connections from
:type sock: socket
:param protocol: Subclass of asyncio Protocol class
:type protocol: type[Protocol]
:param backlog: a number of unaccepted connections that the system
will allow before refusing new connections
:type backlog: int
:param access_log: Enables writing access logs (slows server)
:type access_log: bool
:param return_asyncio_server: flag that defines whether there's a need
to return asyncio.Server or
start it serving right away
:type return_asyncio_server: bool
:param asyncio_server_kwargs: key-value arguments for
asyncio/uvloop create_server method
:type asyncio_server_kwargs: dict
:param noisy_exceptions: Log exceptions that are normally considered
to be quiet/silent
:type noisy_exceptions: bool
:return: AsyncioServer if return_asyncio_server is true, else Nothing
"""
if sock is None:
host, port = host or "127.0.0.1", port or 8000
if protocol is None:
protocol = (
WebSocketProtocol if self.websocket_enabled else HttpProtocol
)
# Set explicitly passed configuration values
for attribute, value in {
"ACCESS_LOG": access_log,
"NOISY_EXCEPTIONS": noisy_exceptions,
}.items():
if value is not None:
setattr(self.config, attribute, value)
server_settings = self._helper(
host=host,
port=port,
debug=debug,
ssl=ssl,
sock=sock,
unix=unix,
loop=get_event_loop(),
protocol=protocol,
backlog=backlog,
run_async=return_asyncio_server,
)
if self.config.USE_UVLOOP is not _default:
error_logger.warning(
"You are trying to change the uvloop configuration, but "
"this is only effective when using the run(...) method. "
"When using the create_server(...) method Sanic will use "
"the already existing loop."
)
main_start = server_settings.pop("main_start", None)
main_stop = server_settings.pop("main_stop", None)
if main_start or main_stop:
logger.warning(
"Listener events for the main process are not available "
"with create_server()"
)
return await serve(
asyncio_server_kwargs=asyncio_server_kwargs, **server_settings
)
async def _run_request_middleware( async def _run_request_middleware(
self, request, request_name=None self, request, request_name=None
): # no cov ): # no cov
@@ -1412,100 +1117,6 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
break break
return response return response
def _helper(
self,
host: Optional[str] = None,
port: Optional[int] = None,
debug: bool = False,
ssl: Union[None, SSLContext, dict, str, list, tuple] = None,
sock: Optional[socket] = None,
unix: Optional[str] = None,
workers: int = 1,
loop: AbstractEventLoop = None,
protocol: Type[Protocol] = HttpProtocol,
backlog: int = 100,
register_sys_signals: bool = True,
run_async: bool = False,
):
"""Helper function used by `run` and `create_server`."""
if self.config.PROXIES_COUNT and self.config.PROXIES_COUNT < 0:
raise ValueError(
"PROXIES_COUNT cannot be negative. "
"https://sanic.readthedocs.io/en/latest/sanic/config.html"
"#proxy-configuration"
)
ssl = process_to_context(ssl)
self.debug = debug
self.state.host = host
self.state.port = port
self.state.workers = workers
self.state.ssl = ssl
self.state.unix = unix
self.state.sock = sock
server_settings = {
"protocol": protocol,
"host": host,
"port": port,
"sock": sock,
"unix": unix,
"ssl": ssl,
"app": self,
"signal": ServerSignal(),
"loop": loop,
"register_sys_signals": register_sys_signals,
"backlog": backlog,
}
self.motd(self.serve_location)
if sys.stdout.isatty() and not self.state.is_debug:
error_logger.warning(
f"{Colors.YELLOW}Sanic is running in PRODUCTION mode. "
"Consider using '--debug' or '--dev' while actively "
f"developing your application.{Colors.END}"
)
# Register start/stop events
for event_name, settings_name, reverse in (
("main_process_start", "main_start", False),
("main_process_stop", "main_stop", True),
):
listeners = self.listeners[event_name].copy()
if reverse:
listeners.reverse()
# Prepend sanic to the arguments when listeners are triggered
listeners = [partial(listener, self) for listener in listeners]
server_settings[settings_name] = listeners # type: ignore
if run_async:
server_settings["run_async"] = True
return server_settings
@property
def serve_location(self) -> str:
serve_location = ""
proto = "http"
if self.state.ssl is not None:
proto = "https"
if self.state.unix:
serve_location = f"{self.state.unix} {proto}://..."
elif self.state.sock:
serve_location = f"{self.state.sock.getsockname()} {proto}://..."
elif self.state.host and self.state.port:
# colon(:) is legal for a host only in an ipv6 address
display_host = (
f"[{self.state.host}]"
if ":" in self.state.host
else self.state.host
)
serve_location = f"{proto}://{display_host}:{self.state.port}"
return serve_location
def _build_endpoint_name(self, *parts): def _build_endpoint_name(self, *parts):
parts = [self.name, *parts] parts = [self.name, *parts]
return ".".join(parts) return ".".join(parts)
@@ -1519,7 +1130,10 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
async def _listener( async def _listener(
app: Sanic, loop: AbstractEventLoop, listener: ListenerType app: Sanic, loop: AbstractEventLoop, listener: ListenerType
): ):
maybe_coro = listener(app, loop) try:
maybe_coro = listener(app) # type: ignore
except TypeError:
maybe_coro = listener(app, loop) # type: ignore
if maybe_coro and isawaitable(maybe_coro): if maybe_coro and isawaitable(maybe_coro):
await maybe_coro await maybe_coro
@@ -1554,13 +1168,14 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
) -> Task: ) -> Task:
if not isinstance(task, Future): if not isinstance(task, Future):
prepped = cls._prep_task(task, app, loop) prepped = cls._prep_task(task, app, loop)
if sys.version_info < (3, 8): if sys.version_info < (3, 8): # no cov
task = loop.create_task(prepped)
if name: if name:
error_logger.warning( error_logger.warning(
"Cannot set a name for a task when using Python 3.7. " "Cannot set a name for a task when using Python 3.7. "
"Your task will be created without a name." "Your task will be created without a name."
) )
task = loop.create_task(prepped) task.get_name = lambda: name
else: else:
task = loop.create_task(prepped, name=name) task = loop.create_task(prepped, name=name)
@@ -1598,12 +1213,6 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
:param task: future, couroutine or awaitable :param task: future, couroutine or awaitable
""" """
if name and sys.version_info == (3, 7):
name = None
error_logger.warning(
"Cannot set a name for a task when using Python 3.7. Your "
"task will be created without a name."
)
try: try:
loop = self.loop # Will raise SanicError if loop is not started loop = self.loop # Will raise SanicError if loop is not started
return self._loop_add_task( return self._loop_add_task(
@@ -1626,12 +1235,6 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
def get_task( def get_task(
self, name: str, *, raise_exception: bool = True self, name: str, *, raise_exception: bool = True
) -> Optional[Task]: ) -> Optional[Task]:
if sys.version_info < (3, 8):
error_logger.warning(
"This feature (get_task) is only supported on using "
"Python 3.8+."
)
return
try: try:
return self._task_registry[name] return self._task_registry[name]
except KeyError: except KeyError:
@@ -1648,19 +1251,13 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
*, *,
raise_exception: bool = True, raise_exception: bool = True,
) -> None: ) -> None:
if sys.version_info < (3, 8):
error_logger.warning(
"This feature (cancel_task) is only supported on using "
"Python 3.8+."
)
return
task = self.get_task(name, raise_exception=raise_exception) task = self.get_task(name, raise_exception=raise_exception)
if task and not task.cancelled(): if task and not task.cancelled():
args: Tuple[str, ...] = () args: Tuple[str, ...] = ()
if msg: if msg:
if sys.version_info >= (3, 9): if sys.version_info >= (3, 9):
args = (msg,) args = (msg,)
else: else: # no cov
raise RuntimeError( raise RuntimeError(
"Cancelling a task with a message is only supported " "Cancelling a task with a message is only supported "
"on Python 3.9+." "on Python 3.9+."
@@ -1672,16 +1269,9 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
... ...
def purge_tasks(self): def purge_tasks(self):
if sys.version_info < (3, 8): for key, task in self._task_registry.items():
error_logger.warning(
"This feature (purge_tasks) is only supported on using "
"Python 3.8+."
)
return
for task in self.tasks:
if task.done() or task.cancelled(): if task.done() or task.cancelled():
name = task.get_name() self._task_registry[key] = None
self._task_registry[name] = None
self._task_registry = { self._task_registry = {
k: v for k, v in self._task_registry.items() if v is not None k: v for k, v in self._task_registry.items() if v is not None
@@ -1690,31 +1280,22 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
def shutdown_tasks( def shutdown_tasks(
self, timeout: Optional[float] = None, increment: float = 0.1 self, timeout: Optional[float] = None, increment: float = 0.1
): ):
if sys.version_info < (3, 8):
error_logger.warning(
"This feature (shutdown_tasks) is only supported on using "
"Python 3.8+."
)
return
for task in self.tasks: for task in self.tasks:
task.cancel() if task.get_name() != "RunServer":
task.cancel()
if timeout is None: if timeout is None:
timeout = self.config.GRACEFUL_SHUTDOWN_TIMEOUT timeout = self.config.GRACEFUL_SHUTDOWN_TIMEOUT
while len(self._task_registry) and timeout: while len(self._task_registry) and timeout:
self.loop.run_until_complete(asyncio.sleep(increment)) with suppress(RuntimeError):
running_loop = get_running_loop()
running_loop.run_until_complete(asyncio.sleep(increment))
self.purge_tasks() self.purge_tasks()
timeout -= increment timeout -= increment
@property @property
def tasks(self): def tasks(self):
if sys.version_info < (3, 8):
error_logger.warning(
"This feature (tasks) is only supported on using "
"Python 3.8+."
)
return
return iter(self._task_registry.values()) return iter(self._task_registry.values())
# -------------------------------------------------------------------- # # -------------------------------------------------------------------- #
@@ -1764,6 +1345,13 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
@debug.setter @debug.setter
def debug(self, value: bool): def debug(self, value: bool):
deprecation(
"Setting the value of a Sanic application's debug value directly "
"is deprecated and will be removed in v22.9. Please set it using "
"the CLI, app.run, app.prepare, or directly set "
"app.state.mode to Mode.DEBUG.",
22.9,
)
mode = Mode.DEBUG if value else Mode.PRODUCTION mode = Mode.DEBUG if value else Mode.PRODUCTION
self.state.mode = mode self.state.mode = mode
@@ -1781,80 +1369,60 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
@property @property
def is_running(self): def is_running(self):
deprecation(
"Use of the is_running property is no longer used by Sanic "
"internally. The property is now deprecated and will be removed "
"in version 22.9. You may continue to set the property for your "
"own needs until that time. If you would like to check whether "
"the application is operational, please use app.state.stage. More "
"information is available at ___.",
22.9,
)
return self.state.is_running return self.state.is_running
@is_running.setter @is_running.setter
def is_running(self, value: bool): def is_running(self, value: bool):
deprecation(
"Use of the is_running property is no longer used by Sanic "
"internally. The property is now deprecated and will be removed "
"in version 22.9. You may continue to set the property for your "
"own needs until that time. If you would like to check whether "
"the application is operational, please use app.state.stage. More "
"information is available at ___.",
22.9,
)
self.state.is_running = value self.state.is_running = value
@property @property
def is_stopping(self): def is_stopping(self):
deprecation(
"Use of the is_stopping property is no longer used by Sanic "
"internally. The property is now deprecated and will be removed "
"in version 22.9. You may continue to set the property for your "
"own needs until that time. If you would like to check whether "
"the application is operational, please use app.state.stage. More "
"information is available at ___.",
22.9,
)
return self.state.is_stopping return self.state.is_stopping
@is_stopping.setter @is_stopping.setter
def is_stopping(self, value: bool): def is_stopping(self, value: bool):
deprecation(
"Use of the is_stopping property is no longer used by Sanic "
"internally. The property is now deprecated and will be removed "
"in version 22.9. You may continue to set the property for your "
"own needs until that time. If you would like to check whether "
"the application is operational, please use app.state.stage. More "
"information is available at ___.",
22.9,
)
self.state.is_stopping = value self.state.is_stopping = value
@property @property
def reload_dirs(self): def reload_dirs(self):
return self.state.reload_dirs return self.state.reload_dirs
def motd(self, serve_location):
if self.config.MOTD:
mode = [f"{self.state.mode},"]
if self.state.fast:
mode.append("goin' fast")
if self.state.asgi:
mode.append("ASGI")
else:
if self.state.workers == 1:
mode.append("single worker")
else:
mode.append(f"w/ {self.state.workers} workers")
display = {
"mode": " ".join(mode),
"server": self.state.server,
"python": platform.python_version(),
"platform": platform.platform(),
}
extra = {}
if self.config.AUTO_RELOAD:
reload_display = "enabled"
if self.state.reload_dirs:
reload_display += ", ".join(
[
"",
*(
str(path.absolute())
for path in self.state.reload_dirs
),
]
)
display["auto-reload"] = reload_display
packages = []
for package_name in SANIC_PACKAGES:
module_name = package_name.replace("-", "_")
try:
module = import_module(module_name)
packages.append(f"{package_name}=={module.__version__}")
except ImportError:
...
if packages:
display["packages"] = ", ".join(packages)
if self.config.MOTD_DISPLAY:
extra.update(self.config.MOTD_DISPLAY)
logo = (
get_logo(coffee=self.state.coffee)
if self.config.LOGO == "" or self.config.LOGO is True
else self.config.LOGO
)
MOTD.output(logo, serve_location, display, extra)
@property @property
def ext(self) -> Extend: def ext(self) -> Extend:
if not hasattr(self, "_ext"): if not hasattr(self, "_ext"):
@@ -1942,7 +1510,8 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
if not Sanic.test_mode: if not Sanic.test_mode:
raise e raise e
def signalize(self): def signalize(self, allow_fail_builtin=True):
self.signal_router.allow_fail_builtin = allow_fail_builtin
try: try:
self.signal_router.finalize() self.signal_router.finalize()
except FinalizationError as e: except FinalizationError as e:
@@ -1952,14 +1521,16 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
async def _startup(self): async def _startup(self):
self._future_registry.clear() self._future_registry.clear()
# Startup Sanic Extensions
if not hasattr(self, "_ext"): if not hasattr(self, "_ext"):
setup_ext(self) setup_ext(self)
if hasattr(self, "_ext"): if hasattr(self, "_ext"):
self.ext._display() self.ext._display()
if self.state.is_debug:
self.config.TOUCHUP = False
# Setup routers # Setup routers
self.signalize() self.signalize(self.config.TOUCHUP)
self.finalize() self.finalize()
# TODO: Replace in v22.6 to check against apps in app registry # TODO: Replace in v22.6 to check against apps in app registry
@@ -1975,8 +1546,12 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
self.__class__._uvloop_setting = self.config.USE_UVLOOP self.__class__._uvloop_setting = self.config.USE_UVLOOP
# Startup time optimizations # Startup time optimizations
ErrorHandler.finalize(self.error_handler, config=self.config) if self.state.primary:
TouchUp.run(self) # TODO:
# - Raise warning if secondary apps have error handler config
ErrorHandler.finalize(self.error_handler, config=self.config)
if self.config.TOUCHUP:
TouchUp.run(self)
self.state.is_started = True self.state.is_started = True

View File

@@ -3,16 +3,17 @@ from __future__ import annotations
import logging import logging
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import Enum, auto from enum import Enum, IntEnum, auto
from pathlib import Path from pathlib import Path
from socket import socket from socket import socket
from ssl import SSLContext from ssl import SSLContext
from typing import TYPE_CHECKING, Any, Optional, Set, Union from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Union
from sanic.log import logger from sanic.log import logger
from sanic.server.async_server import AsyncioServer
if TYPE_CHECKING: if TYPE_CHECKING: # no cov
from sanic import Sanic from sanic import Sanic
@@ -32,6 +33,19 @@ class Mode(StrEnum):
DEBUG = auto() DEBUG = auto()
class ServerStage(IntEnum):
STOPPED = auto()
PARTIAL = auto()
SERVING = auto()
@dataclass
class ApplicationServerInfo:
settings: Dict[str, Any]
stage: ServerStage = field(default=ServerStage.STOPPED)
server: Optional[AsyncioServer] = field(default=None)
@dataclass @dataclass
class ApplicationState: class ApplicationState:
app: Sanic app: Sanic
@@ -45,12 +59,15 @@ class ApplicationState:
unix: Optional[str] = field(default=None) unix: Optional[str] = field(default=None)
mode: Mode = field(default=Mode.PRODUCTION) mode: Mode = field(default=Mode.PRODUCTION)
reload_dirs: Set[Path] = field(default_factory=set) reload_dirs: Set[Path] = field(default_factory=set)
auto_reload: bool = field(default=False)
server: Server = field(default=Server.SANIC) server: Server = field(default=Server.SANIC)
is_running: bool = field(default=False) is_running: bool = field(default=False)
is_started: bool = field(default=False) is_started: bool = field(default=False)
is_stopping: bool = field(default=False) is_stopping: bool = field(default=False)
verbosity: int = field(default=0) verbosity: int = field(default=0)
workers: int = field(default=0) workers: int = field(default=0)
primary: bool = field(default=True)
server_info: List[ApplicationServerInfo] = field(default_factory=list)
# This property relates to the ApplicationState instance and should # This property relates to the ApplicationState instance and should
# not be changed except in the __post_init__ method # not be changed except in the __post_init__ method
@@ -77,3 +94,17 @@ class ApplicationState:
@property @property
def is_debug(self): def is_debug(self):
return self.mode is Mode.DEBUG return self.mode is Mode.DEBUG
@property
def stage(self) -> ServerStage:
if not self.server_info:
return ServerStage.STOPPED
if all(info.stage is ServerStage.SERVING for info in self.server_info):
return ServerStage.SERVING
elif any(
info.stage is ServerStage.SERVING for info in self.server_info
):
return ServerStage.PARTIAL
return ServerStage.STOPPED

View File

@@ -1,14 +1,15 @@
from __future__ import annotations
import warnings import warnings
from typing import Optional from typing import TYPE_CHECKING, Optional
from urllib.parse import quote from urllib.parse import quote
import sanic.app # noqa
from sanic.compat import Header from sanic.compat import Header
from sanic.exceptions import ServerError from sanic.exceptions import ServerError
from sanic.helpers import _default from sanic.helpers import _default
from sanic.http import Stage from sanic.http import Stage
from sanic.log import logger
from sanic.models.asgi import ASGIReceive, ASGIScope, ASGISend, MockTransport from sanic.models.asgi import ASGIReceive, ASGIScope, ASGISend, MockTransport
from sanic.request import Request from sanic.request import Request
from sanic.response import BaseHTTPResponse from sanic.response import BaseHTTPResponse
@@ -16,30 +17,35 @@ from sanic.server import ConnInfo
from sanic.server.websockets.connection import WebSocketConnection from sanic.server.websockets.connection import WebSocketConnection
if TYPE_CHECKING: # no cov
from sanic import Sanic
class Lifespan: class Lifespan:
def __init__(self, asgi_app: "ASGIApp") -> None: def __init__(self, asgi_app: ASGIApp) -> None:
self.asgi_app = asgi_app self.asgi_app = asgi_app
if ( if self.asgi_app.sanic_app.state.verbosity > 0:
"server.init.before" if (
in self.asgi_app.sanic_app.signal_router.name_index "server.init.before"
): in self.asgi_app.sanic_app.signal_router.name_index
warnings.warn( ):
'You have set a listener for "before_server_start" ' logger.debug(
"in ASGI mode. " 'You have set a listener for "before_server_start" '
"It will be executed as early as possible, but not before " "in ASGI mode. "
"the ASGI server is started." "It will be executed as early as possible, but not before "
) "the ASGI server is started."
if ( )
"server.shutdown.after" if (
in self.asgi_app.sanic_app.signal_router.name_index "server.shutdown.after"
): in self.asgi_app.sanic_app.signal_router.name_index
warnings.warn( ):
'You have set a listener for "after_server_stop" ' logger.debug(
"in ASGI mode. " 'You have set a listener for "after_server_stop" '
"It will be executed as late as possible, but not after " "in ASGI mode. "
"the ASGI server is stopped." "It will be executed as late as possible, but not after "
) "the ASGI server is stopped."
)
async def startup(self) -> None: async def startup(self) -> None:
""" """
@@ -88,7 +94,7 @@ class Lifespan:
class ASGIApp: class ASGIApp:
sanic_app: "sanic.app.Sanic" sanic_app: Sanic
request: Request request: Request
transport: MockTransport transport: MockTransport
lifespan: Lifespan lifespan: Lifespan

View File

@@ -68,6 +68,13 @@ Or, a path to a directory to run as a simple HTTP server:
legacy_version = len(sys.argv) == 2 and sys.argv[-1] == "-v" legacy_version = len(sys.argv) == 2 and sys.argv[-1] == "-v"
parse_args = ["--version"] if legacy_version else None parse_args = ["--version"] if legacy_version else None
if not parse_args:
parsed, unknown = self.parser.parse_known_args()
if unknown and parsed.factory:
for arg in unknown:
if arg.startswith("--"):
self.parser.add_argument(arg.split("=")[0])
self.args = self.parser.parse_args(args=parse_args) self.args = self.parser.parse_args(args=parse_args)
self._precheck() self._precheck()
@@ -79,13 +86,6 @@ Or, a path to a directory to run as a simple HTTP server:
error_logger.exception("Failed to run app") error_logger.exception("Failed to run app")
def _precheck(self): def _precheck(self):
if self.args.debug and self.main_process:
error_logger.warning(
"Starting in v22.3, --debug will no "
"longer automatically run the auto-reloader.\n Switch to "
"--dev to continue using that functionality."
)
# # Custom TLS mismatch handling for better diagnostics # # Custom TLS mismatch handling for better diagnostics
if self.main_process and ( if self.main_process and (
# one of cert/key missing # one of cert/key missing
@@ -120,6 +120,14 @@ Or, a path to a directory to run as a simple HTTP server:
delimiter = ":" if ":" in self.args.module else "." delimiter = ":" if ":" in self.args.module else "."
module_name, app_name = self.args.module.rsplit(delimiter, 1) module_name, app_name = self.args.module.rsplit(delimiter, 1)
if module_name == "" and os.path.isdir(self.args.module):
raise ValueError(
"App not found.\n"
" Please use --simple if you are passing a "
"directory to sanic.\n"
f" eg. sanic {self.args.module} --simple"
)
if app_name.endswith("()"): if app_name.endswith("()"):
self.args.factory = True self.args.factory = True
app_name = app_name[:-2] app_name = app_name[:-2]
@@ -127,14 +135,26 @@ Or, a path to a directory to run as a simple HTTP server:
module = import_module(module_name) module = import_module(module_name)
app = getattr(module, app_name, None) app = getattr(module, app_name, None)
if self.args.factory: if self.args.factory:
app = app() try:
app = app(self.args)
except TypeError:
app = app()
app_type_name = type(app).__name__ app_type_name = type(app).__name__
if not isinstance(app, Sanic): if not isinstance(app, Sanic):
if callable(app):
solution = f"sanic {self.args.module} --factory"
raise ValueError(
"Module is not a Sanic app, it is a"
f"{app_type_name}\n"
" If this callable returns a"
f"Sanic instance try: \n{solution}"
)
raise ValueError( raise ValueError(
f"Module is not a Sanic app, it is a {app_type_name}\n" f"Module is not a Sanic app, it is a {app_type_name}\n"
f" Perhaps you meant {self.args.module}.app?" f" Perhaps you meant {self.args.module}:app?"
) )
except ImportError as e: except ImportError as e:
if module_name.startswith(e.name): if module_name.startswith(e.name):
@@ -174,16 +194,11 @@ Or, a path to a directory to run as a simple HTTP server:
"workers": self.args.workers, "workers": self.args.workers,
} }
if self.args.auto_reload: for maybe_arg in ("auto_reload", "dev"):
kwargs["auto_reload"] = True if getattr(self.args, maybe_arg, False):
kwargs[maybe_arg] = True
if self.args.path: if self.args.path:
if self.args.auto_reload or self.args.debug: kwargs["auto_reload"] = True
kwargs["reload_dir"] = self.args.path kwargs["reload_dir"] = self.args.path
else:
error_logger.warning(
"Ignoring '--reload-dir' since auto reloading was not "
"enabled. If you would like to watch directories for "
"changes, consider using --debug or --auto-reload."
)
return kwargs return kwargs

View File

@@ -180,18 +180,10 @@ class DevelopmentGroup(Group):
"--debug", "--debug",
dest="debug", dest="debug",
action="store_true", action="store_true",
help="Run the server in debug mode",
)
self.container.add_argument(
"-d",
"--dev",
dest="debug",
action="store_true",
help=( help=(
"Currently is an alias for --debug. But starting in v22.3, \n" "Run the server in DEBUG mode. It includes DEBUG logging,\n"
"--debug will no longer automatically trigger auto_restart. \n" "additional context on exceptions, and other settings\n"
"However, --dev will continue, effectively making it the \n" "not-safe for PRODUCTION, but helpful for debugging problems."
"same as debug + auto_reload."
), ),
) )
self.container.add_argument( self.container.add_argument(
@@ -212,6 +204,13 @@ class DevelopmentGroup(Group):
action="append", action="append",
help="Extra directories to watch and reload on changes", help="Extra directories to watch and reload on changes",
) )
self.container.add_argument(
"-d",
"--dev",
dest="dev",
action="store_true",
help=("debug + auto reload."),
)
class OutputGroup(Group): class OutputGroup(Group):

View File

@@ -38,8 +38,9 @@ DEFAULT_CONFIG = {
"REQUEST_MAX_SIZE": 100000000, # 100 megabytes "REQUEST_MAX_SIZE": 100000000, # 100 megabytes
"REQUEST_TIMEOUT": 60, # 60 seconds "REQUEST_TIMEOUT": 60, # 60 seconds
"RESPONSE_TIMEOUT": 60, # 60 seconds "RESPONSE_TIMEOUT": 60, # 60 seconds
"TOUCHUP": True,
"USE_UVLOOP": _default, "USE_UVLOOP": _default,
"WEBSOCKET_MAX_SIZE": 2 ** 20, # 1 megabyte "WEBSOCKET_MAX_SIZE": 2**20, # 1 megabyte
"WEBSOCKET_PING_INTERVAL": 20, "WEBSOCKET_PING_INTERVAL": 20,
"WEBSOCKET_PING_TIMEOUT": 20, "WEBSOCKET_PING_TIMEOUT": 20,
} }
@@ -81,6 +82,7 @@ class Config(dict, metaclass=DescriptorMeta):
REQUEST_TIMEOUT: int REQUEST_TIMEOUT: int
RESPONSE_TIMEOUT: int RESPONSE_TIMEOUT: int
SERVER_NAME: str SERVER_NAME: str
TOUCHUP: bool
USE_UVLOOP: Union[Default, bool] USE_UVLOOP: Union[Default, bool]
WEBSOCKET_MAX_SIZE: int WEBSOCKET_MAX_SIZE: int
WEBSOCKET_PING_INTERVAL: int WEBSOCKET_PING_INTERVAL: int
@@ -226,9 +228,12 @@ class Config(dict, metaclass=DescriptorMeta):
`See user guide re: config `See user guide re: config
<https://sanicframework.org/guide/deployment/configuration.html>`__ <https://sanicframework.org/guide/deployment/configuration.html>`__
""" """
lower_case_var_found = False
for key, value in environ.items(): for key, value in environ.items():
if not key.startswith(prefix): if not key.startswith(prefix):
continue continue
if not key.isupper():
lower_case_var_found = True
_, config_key = key.split(prefix, 1) _, config_key = key.split(prefix, 1)
@@ -238,6 +243,12 @@ class Config(dict, metaclass=DescriptorMeta):
break break
except ValueError: except ValueError:
pass pass
if lower_case_var_found:
deprecation(
"Lowercase environment variables will not be "
"loaded into Sanic config beginning in v22.9.",
22.9,
)
def update_config(self, config: Union[bytes, str, dict, Any]): def update_config(self, config: Union[bytes, str, dict, Any]):
""" """

View File

@@ -51,6 +51,10 @@ class InvalidUsage(SanicException):
quiet = True quiet = True
class BadURL(InvalidUsage):
...
class MethodNotSupported(SanicException): class MethodNotSupported(SanicException):
""" """
**Status**: 405 Method Not Allowed **Status**: 405 Method Not Allowed

View File

@@ -1,13 +1,12 @@
from __future__ import annotations from __future__ import annotations
from inspect import signature
from typing import Dict, List, Optional, Tuple, Type, Union from typing import Dict, List, Optional, Tuple, Type, Union
from sanic.config import Config from sanic.config import Config
from sanic.errorpages import ( from sanic.errorpages import (
DEFAULT_FORMAT, DEFAULT_FORMAT,
BaseRenderer, BaseRenderer,
HTMLRenderer, TextRenderer,
exception_response, exception_response,
) )
from sanic.exceptions import ( from sanic.exceptions import (
@@ -35,13 +34,11 @@ class ErrorHandler:
""" """
# Beginning in v22.3, the base renderer will be TextRenderer
def __init__( def __init__(
self, self,
fallback: Union[str, Default] = _default, fallback: Union[str, Default] = _default,
base: Type[BaseRenderer] = HTMLRenderer, base: Type[BaseRenderer] = TextRenderer,
): ):
self.handlers: List[Tuple[Type[BaseException], RouteHandler]] = []
self.cached_handlers: Dict[ self.cached_handlers: Dict[
Tuple[Type[BaseException], Optional[str]], Optional[RouteHandler] Tuple[Type[BaseException], Optional[str]], Optional[RouteHandler]
] = {} ] = {}
@@ -53,14 +50,14 @@ class ErrorHandler:
self._warn_fallback_deprecation() self._warn_fallback_deprecation()
@property @property
def fallback(self): def fallback(self): # no cov
# This is for backwards compat and can be removed in v22.6 # This is for backwards compat and can be removed in v22.6
if self._fallback is _default: if self._fallback is _default:
return DEFAULT_FORMAT return DEFAULT_FORMAT
return self._fallback return self._fallback
@fallback.setter @fallback.setter
def fallback(self, value: str): def fallback(self, value: str): # no cov
self._warn_fallback_deprecation() self._warn_fallback_deprecation()
if not isinstance(value, str): if not isinstance(value, str):
raise SanicException( raise SanicException(
@@ -95,8 +92,8 @@ class ErrorHandler:
def finalize( def finalize(
cls, cls,
error_handler: ErrorHandler, error_handler: ErrorHandler,
config: Config,
fallback: Optional[str] = None, fallback: Optional[str] = None,
config: Optional[Config] = None,
): ):
if fallback: if fallback:
deprecation( deprecation(
@@ -107,14 +104,10 @@ class ErrorHandler:
22.6, 22.6,
) )
if config is None: if not fallback:
deprecation( fallback = config.FALLBACK_ERROR_FORMAT
"Starting in v22.3, config will be a required argument "
"for ErrorHandler.finalize().",
22.3,
)
if fallback and fallback != DEFAULT_FORMAT: if fallback != DEFAULT_FORMAT:
if error_handler._fallback is not _default: if error_handler._fallback is not _default:
error_logger.warning( error_logger.warning(
f"Setting the fallback value to {fallback}. This changes " f"Setting the fallback value to {fallback}. This changes "
@@ -128,27 +121,9 @@ class ErrorHandler:
f"Error handler is non-conforming: {type(error_handler)}" f"Error handler is non-conforming: {type(error_handler)}"
) )
sig = signature(error_handler.lookup)
if len(sig.parameters) == 1:
deprecation(
"You are using a deprecated error handler. The lookup "
"method should accept two positional parameters: "
"(exception, route_name: Optional[str]). "
"Until you upgrade your ErrorHandler.lookup, Blueprint "
"specific exceptions will not work properly. Beginning "
"in v22.3, the legacy style lookup method will not "
"work at all.",
22.3,
)
legacy_lookup = error_handler._legacy_lookup
error_handler._lookup = legacy_lookup # type: ignore
def _full_lookup(self, exception, route_name: Optional[str] = None): def _full_lookup(self, exception, route_name: Optional[str] = None):
return self.lookup(exception, route_name) return self.lookup(exception, route_name)
def _legacy_lookup(self, exception, route_name: Optional[str] = None):
return self.lookup(exception)
def add(self, exception, handler, route_names: Optional[List[str]] = None): def add(self, exception, handler, route_names: Optional[List[str]] = None):
""" """
Add a new exception handler to an already existing handler object. Add a new exception handler to an already existing handler object.
@@ -162,9 +137,6 @@ class ErrorHandler:
:return: None :return: None
""" """
# self.handlers is deprecated and will be removed in version 22.3
self.handlers.append((exception, handler))
if route_names: if route_names:
for route in route_names: for route in route_names:
self.cached_handlers[(exception, route)] = handler self.cached_handlers[(exception, route)] = handler
@@ -236,7 +208,7 @@ class ErrorHandler:
except Exception: except Exception:
try: try:
url = repr(request.url) url = repr(request.url)
except AttributeError: except AttributeError: # no cov
url = "unknown" url = "unknown"
response_message = ( response_message = (
"Exception raised in exception handler " '"%s" for uri: %s' "Exception raised in exception handler " '"%s" for uri: %s'
@@ -281,7 +253,7 @@ class ErrorHandler:
if quiet is False or noisy is True: if quiet is False or noisy is True:
try: try:
url = repr(request.url) url = repr(request.url)
except AttributeError: except AttributeError: # no cov
url = "unknown" url = "unknown"
error_logger.exception( error_logger.exception(

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
import re import re
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Union
from urllib.parse import unquote from urllib.parse import unquote
from sanic.exceptions import InvalidHeader from sanic.exceptions import InvalidHeader
@@ -18,7 +18,7 @@ Options = Dict[str, Union[int, str]] # key=value fields in various headers
OptionsIterable = Iterable[Tuple[str, str]] # May contain duplicate keys OptionsIterable = Iterable[Tuple[str, str]] # May contain duplicate keys
_token, _quoted = r"([\w!#$%&'*+\-.^_`|~]+)", r'"([^"]*)"' _token, _quoted = r"([\w!#$%&'*+\-.^_`|~]+)", r'"([^"]*)"'
_param = re.compile(fr";\s*{_token}=(?:{_token}|{_quoted})", re.ASCII) _param = re.compile(rf";\s*{_token}=(?:{_token}|{_quoted})", re.ASCII)
_firefox_quote_escape = re.compile(r'\\"(?!; |\s*$)') _firefox_quote_escape = re.compile(r'\\"(?!; |\s*$)')
_ipv6 = "(?:[0-9A-Fa-f]{0,4}:){2,7}[0-9A-Fa-f]{0,4}" _ipv6 = "(?:[0-9A-Fa-f]{0,4}:){2,7}[0-9A-Fa-f]{0,4}"
_ipv6_re = re.compile(_ipv6) _ipv6_re = re.compile(_ipv6)
@@ -394,3 +394,17 @@ def parse_accept(accept: str) -> AcceptContainer:
return AcceptContainer( return AcceptContainer(
sorted(accept_list, key=_sort_accept_value, reverse=True) sorted(accept_list, key=_sort_accept_value, reverse=True)
) )
def parse_credentials(
header: Optional[str],
prefixes: Union[List, Tuple, Set] = None,
) -> Tuple[Optional[str], Optional[str]]:
"""Parses any header with the aim to retrieve any credentials from it."""
if not prefixes or not isinstance(prefixes, (list, tuple, set)):
prefixes = ("Basic", "Bearer", "Token")
if header is not None:
for prefix in prefixes:
if prefix in header:
return prefix, header.partition(prefix)[-1].strip()
return None, header

View File

@@ -6,7 +6,7 @@ from typing import Any, Dict
from warnings import warn from warnings import warn
LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict( LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict( # no cov
version=1, version=1,
disable_existing_loggers=False, disable_existing_loggers=False,
loggers={ loggers={
@@ -57,7 +57,7 @@ LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict(
) )
class Colors(str, Enum): class Colors(str, Enum): # no cov
END = "\033[0m" END = "\033[0m"
BLUE = "\033[01;34m" BLUE = "\033[01;34m"
GREEN = "\033[01;32m" GREEN = "\033[01;32m"
@@ -65,23 +65,23 @@ class Colors(str, Enum):
RED = "\033[01;31m" RED = "\033[01;31m"
logger = logging.getLogger("sanic.root") logger = logging.getLogger("sanic.root") # no cov
""" """
General Sanic logger General Sanic logger
""" """
error_logger = logging.getLogger("sanic.error") error_logger = logging.getLogger("sanic.error") # no cov
""" """
Logger used by Sanic for error logging Logger used by Sanic for error logging
""" """
access_logger = logging.getLogger("sanic.access") access_logger = logging.getLogger("sanic.access") # no cov
""" """
Logger used by Sanic for access logging Logger used by Sanic for access logging
""" """
def deprecation(message: str, version: float): def deprecation(message: str, version: float): # no cov
version_info = f"[DEPRECATION v{version}] " version_info = f"[DEPRECATION v{version}] "
if sys.stdout.isatty(): if sys.stdout.isatty():
version_info = f"{Colors.RED}{version_info}" version_info = f"{Colors.RED}{version_info}"

View File

@@ -1,8 +1,9 @@
from enum import Enum, auto from enum import Enum, auto
from functools import partial from functools import partial
from typing import List, Optional, Union from typing import Callable, List, Optional, Union, overload
from sanic.base.meta import SanicMeta from sanic.base.meta import SanicMeta
from sanic.exceptions import InvalidUsage
from sanic.models.futures import FutureListener from sanic.models.futures import FutureListener
from sanic.models.handler_types import ListenerType, Sanic from sanic.models.handler_types import ListenerType, Sanic
@@ -17,6 +18,8 @@ class ListenerEvent(str, Enum):
AFTER_SERVER_STOP = "server.shutdown.after" AFTER_SERVER_STOP = "server.shutdown.after"
MAIN_PROCESS_START = auto() MAIN_PROCESS_START = auto()
MAIN_PROCESS_STOP = auto() MAIN_PROCESS_STOP = auto()
RELOAD_PROCESS_START = auto()
RELOAD_PROCESS_STOP = auto()
class ListenerMixin(metaclass=SanicMeta): class ListenerMixin(metaclass=SanicMeta):
@@ -26,12 +29,33 @@ class ListenerMixin(metaclass=SanicMeta):
def _apply_listener(self, listener: FutureListener): def _apply_listener(self, listener: FutureListener):
raise NotImplementedError # noqa raise NotImplementedError # noqa
@overload
def listener(
self,
listener_or_event: ListenerType[Sanic],
event_or_none: str,
apply: bool = ...,
) -> ListenerType[Sanic]:
...
@overload
def listener(
self,
listener_or_event: str,
event_or_none: None = ...,
apply: bool = ...,
) -> Callable[[ListenerType[Sanic]], ListenerType[Sanic]]:
...
def listener( def listener(
self, self,
listener_or_event: Union[ListenerType[Sanic], str], listener_or_event: Union[ListenerType[Sanic], str],
event_or_none: Optional[str] = None, event_or_none: Optional[str] = None,
apply: bool = True, apply: bool = True,
) -> ListenerType[Sanic]: ) -> Union[
ListenerType[Sanic],
Callable[[ListenerType[Sanic]], ListenerType[Sanic]],
]:
""" """
Create a listener from a decorated function. Create a listener from a decorated function.
@@ -49,7 +73,9 @@ class ListenerMixin(metaclass=SanicMeta):
:param event: event to listen to :param event: event to listen to
""" """
def register_listener(listener, event): def register_listener(
listener: ListenerType[Sanic], event: str
) -> ListenerType[Sanic]:
nonlocal apply nonlocal apply
future_listener = FutureListener(listener, event) future_listener = FutureListener(listener, event)
@@ -59,6 +85,10 @@ class ListenerMixin(metaclass=SanicMeta):
return listener return listener
if callable(listener_or_event): if callable(listener_or_event):
if event_or_none is None:
raise InvalidUsage(
"Invalid event registration: Missing event name."
)
return register_listener(listener_or_event, event_or_none) return register_listener(listener_or_event, event_or_none)
else: else:
return partial(register_listener, event=listener_or_event) return partial(register_listener, event=listener_or_event)
@@ -73,6 +103,16 @@ class ListenerMixin(metaclass=SanicMeta):
) -> ListenerType[Sanic]: ) -> ListenerType[Sanic]:
return self.listener(listener, "main_process_stop") return self.listener(listener, "main_process_stop")
def reload_process_start(
self, listener: ListenerType[Sanic]
) -> ListenerType[Sanic]:
return self.listener(listener, "reload_process_start")
def reload_process_stop(
self, listener: ListenerType[Sanic]
) -> ListenerType[Sanic]:
return self.listener(listener, "reload_process_stop")
def before_server_start( def before_server_start(
self, listener: ListenerType[Sanic] self, listener: ListenerType[Sanic]
) -> ListenerType[Sanic]: ) -> ListenerType[Sanic]:

View File

@@ -16,9 +16,9 @@ class MiddlewareMixin(metaclass=SanicMeta):
self, middleware_or_request, attach_to="request", apply=True self, middleware_or_request, attach_to="request", apply=True
): ):
""" """
Decorate and register middleware to be called before a request. Decorate and register middleware to be called before a request
Can either be called as *@app.middleware* or is handled or after a response is created. Can either be called as
*@app.middleware('request')* *@app.middleware* or *@app.middleware('request')*.
`See user guide re: middleware `See user guide re: middleware
<https://sanicframework.org/guide/basics/middleware.html>`__ <https://sanicframework.org/guide/basics/middleware.html>`__
@@ -47,12 +47,25 @@ class MiddlewareMixin(metaclass=SanicMeta):
) )
def on_request(self, middleware=None): def on_request(self, middleware=None):
"""Register a middleware to be called before a request is handled.
This is the same as *@app.middleware('request')*.
:param: middleware: A callable that takes in request.
"""
if callable(middleware): if callable(middleware):
return self.middleware(middleware, "request") return self.middleware(middleware, "request")
else: else:
return partial(self.middleware, attach_to="request") return partial(self.middleware, attach_to="request")
def on_response(self, middleware=None): def on_response(self, middleware=None):
"""Register a middleware to be called after a response is created.
This is the same as *@app.middleware('response')*.
:param: middleware:
A callable that takes in a request and its response.
"""
if callable(middleware): if callable(middleware):
return self.middleware(middleware, "response") return self.middleware(middleware, "response")
else: else:

View File

@@ -4,7 +4,8 @@ from functools import partial, wraps
from inspect import getsource, signature from inspect import getsource, signature
from mimetypes import guess_type from mimetypes import guess_type
from os import path from os import path
from pathlib import Path, PurePath from pathlib import PurePath
from re import sub
from textwrap import dedent from textwrap import dedent
from time import gmtime, strftime from time import gmtime, strftime
from typing import Any, Callable, Iterable, List, Optional, Set, Tuple, Union from typing import Any, Callable, Iterable, List, Optional, Set, Tuple, Union
@@ -16,7 +17,12 @@ from sanic.base.meta import SanicMeta
from sanic.compat import stat_async from sanic.compat import stat_async
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE, HTTP_METHODS from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE, HTTP_METHODS
from sanic.errorpages import RESPONSE_MAPPING from sanic.errorpages import RESPONSE_MAPPING
from sanic.exceptions import ContentRangeError, FileNotFound, HeaderNotFound from sanic.exceptions import (
ContentRangeError,
FileNotFound,
HeaderNotFound,
InvalidUsage,
)
from sanic.handlers import ContentRangeHandler from sanic.handlers import ContentRangeHandler
from sanic.log import deprecation, error_logger from sanic.log import deprecation, error_logger
from sanic.models.futures import FutureRoute, FutureStatic from sanic.models.futures import FutureRoute, FutureStatic
@@ -769,40 +775,32 @@ class RouteMixin(metaclass=SanicMeta):
content_type=None, content_type=None,
__file_uri__=None, __file_uri__=None,
): ):
# Using this to determine if the URL is trying to break out of the path
# served. os.path.realpath seems to be very slow
if __file_uri__ and "../" in __file_uri__:
raise InvalidUsage("Invalid URL")
# Merge served directory and requested file if provided # Merge served directory and requested file if provided
file_path_raw = Path(unquote(file_or_directory)) # Strip all / that in the beginning of the URL to help prevent python
root_path = file_path = file_path_raw.resolve() # from herping a derp and treating the uri as an absolute path
not_found = FileNotFound( root_path = file_path = file_or_directory
"File not found",
path=file_or_directory,
relative_url=__file_uri__,
)
if __file_uri__: if __file_uri__:
# Strip all / that in the beginning of the URL to help prevent file_path = path.join(
# python from herping a derp and treating the uri as an file_or_directory, sub("^[/]*", "", __file_uri__)
# absolute path )
unquoted_file_uri = unquote(__file_uri__).lstrip("/")
file_path_raw = Path(file_or_directory, unquoted_file_uri)
file_path = file_path_raw.resolve()
if (
file_path < root_path and not file_path_raw.is_symlink()
) or ".." in file_path_raw.parts:
error_logger.exception(
f"File not found: path={file_or_directory}, "
f"relative_url={__file_uri__}"
)
raise not_found
try: # URL decode the path sent by the browser otherwise we won't be able to
file_path.relative_to(root_path) # match filenames which got encoded (filenames with spaces etc)
except ValueError: file_path = path.abspath(unquote(file_path))
if not file_path_raw.is_symlink(): if not file_path.startswith(path.abspath(unquote(root_path))):
error_logger.exception( error_logger.exception(
f"File not found: path={file_or_directory}, " f"File not found: path={file_or_directory}, "
f"relative_url={__file_uri__}" f"relative_url={__file_uri__}"
) )
raise not_found raise FileNotFound(
"File not found",
path=file_or_directory,
relative_url=__file_uri__,
)
try: try:
headers = {} headers = {}
# Check if the client has been sent this file before # Check if the client has been sent this file before
@@ -870,7 +868,11 @@ class RouteMixin(metaclass=SanicMeta):
except ContentRangeError: except ContentRangeError:
raise raise
except FileNotFoundError: except FileNotFoundError:
raise not_found raise FileNotFound(
"File not found",
path=file_or_directory,
relative_url=__file_uri__,
)
except Exception: except Exception:
error_logger.exception( error_logger.exception(
f"Exception in static request handler: " f"Exception in static request handler: "

703
sanic/mixins/runner.py Normal file
View File

@@ -0,0 +1,703 @@
from __future__ import annotations
import os
import platform
import sys
from asyncio import (
AbstractEventLoop,
CancelledError,
Protocol,
all_tasks,
get_event_loop,
get_running_loop,
new_event_loop,
)
from contextlib import suppress
from functools import partial
from importlib import import_module
from pathlib import Path
from socket import socket
from ssl import SSLContext
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Type, Union
from sanic import reloader_helpers
from sanic.application.logo import get_logo
from sanic.application.motd import MOTD
from sanic.application.state import ApplicationServerInfo, Mode, ServerStage
from sanic.base.meta import SanicMeta
from sanic.compat import OS_IS_WINDOWS
from sanic.helpers import _default
from sanic.log import Colors, error_logger, logger
from sanic.models.handler_types import ListenerType
from sanic.server import Signal as ServerSignal
from sanic.server import try_use_uvloop
from sanic.server.async_server import AsyncioServer
from sanic.server.events import trigger_events
from sanic.server.protocols.http_protocol import HttpProtocol
from sanic.server.protocols.websocket_protocol import WebSocketProtocol
from sanic.server.runners import serve, serve_multiple, serve_single
from sanic.tls import process_to_context
if TYPE_CHECKING: # no cov
from sanic import Sanic
from sanic.application.state import ApplicationState
from sanic.config import Config
SANIC_PACKAGES = ("sanic-routing", "sanic-testing", "sanic-ext")
class RunnerMixin(metaclass=SanicMeta):
_app_registry: Dict[str, Sanic]
config: Config
listeners: Dict[str, List[ListenerType[Any]]]
state: ApplicationState
websocket_enabled: bool
def make_coffee(self, *args, **kwargs):
self.state.coffee = True
self.run(*args, **kwargs)
def run(
self,
host: Optional[str] = None,
port: Optional[int] = None,
*,
dev: bool = False,
debug: bool = False,
auto_reload: Optional[bool] = None,
ssl: Union[None, SSLContext, dict, str, list, tuple] = None,
sock: Optional[socket] = None,
workers: int = 1,
protocol: Optional[Type[Protocol]] = None,
backlog: int = 100,
register_sys_signals: bool = True,
access_log: Optional[bool] = None,
unix: Optional[str] = None,
loop: AbstractEventLoop = None,
reload_dir: Optional[Union[List[str], str]] = None,
noisy_exceptions: Optional[bool] = None,
motd: bool = True,
fast: bool = False,
verbosity: int = 0,
motd_display: Optional[Dict[str, str]] = None,
) -> None:
"""
Run the HTTP Server and listen until keyboard interrupt or term
signal. On termination, drain connections before closing.
:param host: Address to host on
:type host: str
:param port: Port to host on
:type port: int
:param debug: Enables debug output (slows server)
:type debug: bool
:param auto_reload: Reload app whenever its source code is changed.
Enabled by default in debug mode.
:type auto_relaod: bool
:param ssl: SSLContext, or location of certificate and key
for SSL encryption of worker(s)
:type ssl: str, dict, SSLContext or list
:param sock: Socket for the server to accept connections from
:type sock: socket
:param workers: Number of processes received before it is respected
:type workers: int
:param protocol: Subclass of asyncio Protocol class
:type protocol: type[Protocol]
:param backlog: a number of unaccepted connections that the system
will allow before refusing new connections
:type backlog: int
:param register_sys_signals: Register SIG* events
:type register_sys_signals: bool
:param access_log: Enables writing access logs (slows server)
:type access_log: bool
:param unix: Unix socket to listen on instead of TCP port
:type unix: str
:param noisy_exceptions: Log exceptions that are normally considered
to be quiet/silent
:type noisy_exceptions: bool
:return: Nothing
"""
self.prepare(
host=host,
port=port,
dev=dev,
debug=debug,
auto_reload=auto_reload,
ssl=ssl,
sock=sock,
workers=workers,
protocol=protocol,
backlog=backlog,
register_sys_signals=register_sys_signals,
access_log=access_log,
unix=unix,
loop=loop,
reload_dir=reload_dir,
noisy_exceptions=noisy_exceptions,
motd=motd,
fast=fast,
verbosity=verbosity,
motd_display=motd_display,
)
self.__class__.serve(primary=self) # type: ignore
def prepare(
self,
host: Optional[str] = None,
port: Optional[int] = None,
*,
dev: bool = False,
debug: bool = False,
auto_reload: Optional[bool] = None,
ssl: Union[None, SSLContext, dict, str, list, tuple] = None,
sock: Optional[socket] = None,
workers: int = 1,
protocol: Optional[Type[Protocol]] = None,
backlog: int = 100,
register_sys_signals: bool = True,
access_log: Optional[bool] = None,
unix: Optional[str] = None,
loop: AbstractEventLoop = None,
reload_dir: Optional[Union[List[str], str]] = None,
noisy_exceptions: Optional[bool] = None,
motd: bool = True,
fast: bool = False,
verbosity: int = 0,
motd_display: Optional[Dict[str, str]] = None,
) -> None:
if dev:
debug = True
auto_reload = True
self.state.verbosity = verbosity
if not self.state.auto_reload:
self.state.auto_reload = bool(auto_reload)
if fast and workers != 1:
raise RuntimeError("You cannot use both fast=True and workers=X")
if motd_display:
self.config.MOTD_DISPLAY.update(motd_display)
if reload_dir:
if isinstance(reload_dir, str):
reload_dir = [reload_dir]
for directory in reload_dir:
direc = Path(directory)
if not direc.is_dir():
logger.warning(
f"Directory {directory} could not be located"
)
self.state.reload_dirs.add(Path(directory))
if loop is not None:
raise TypeError(
"loop is not a valid argument. To use an existing loop, "
"change to create_server().\nSee more: "
"https://sanic.readthedocs.io/en/latest/sanic/deploying.html"
"#asynchronous-support"
)
if (
self.__class__.should_auto_reload()
and os.environ.get("SANIC_SERVER_RUNNING") != "true"
): # no cov
return
if sock is None:
host, port = host or "127.0.0.1", port or 8000
if protocol is None:
protocol = (
WebSocketProtocol if self.websocket_enabled else HttpProtocol
)
# Set explicitly passed configuration values
for attribute, value in {
"ACCESS_LOG": access_log,
"AUTO_RELOAD": auto_reload,
"MOTD": motd,
"NOISY_EXCEPTIONS": noisy_exceptions,
}.items():
if value is not None:
setattr(self.config, attribute, value)
if fast:
self.state.fast = True
try:
workers = len(os.sched_getaffinity(0))
except AttributeError: # no cov
workers = os.cpu_count() or 1
server_settings = self._helper(
host=host,
port=port,
debug=debug,
ssl=ssl,
sock=sock,
unix=unix,
workers=workers,
protocol=protocol,
backlog=backlog,
register_sys_signals=register_sys_signals,
)
self.state.server_info.append(
ApplicationServerInfo(settings=server_settings)
)
if self.config.USE_UVLOOP is True or (
self.config.USE_UVLOOP is _default and not OS_IS_WINDOWS
):
try_use_uvloop()
async def create_server(
self,
host: Optional[str] = None,
port: Optional[int] = None,
*,
debug: bool = False,
ssl: Union[None, SSLContext, dict, str, list, tuple] = None,
sock: Optional[socket] = None,
protocol: Type[Protocol] = None,
backlog: int = 100,
access_log: Optional[bool] = None,
unix: Optional[str] = None,
return_asyncio_server: bool = False,
asyncio_server_kwargs: Dict[str, Any] = None,
noisy_exceptions: Optional[bool] = None,
) -> Optional[AsyncioServer]:
"""
Asynchronous version of :func:`run`.
This method will take care of the operations necessary to invoke
the *before_start* events via :func:`trigger_events` method invocation
before starting the *sanic* app in Async mode.
.. note::
This does not support multiprocessing and is not the preferred
way to run a :class:`Sanic` application.
:param host: Address to host on
:type host: str
:param port: Port to host on
:type port: int
:param debug: Enables debug output (slows server)
:type debug: bool
:param ssl: SSLContext, or location of certificate and key
for SSL encryption of worker(s)
:type ssl: SSLContext or dict
:param sock: Socket for the server to accept connections from
:type sock: socket
:param protocol: Subclass of asyncio Protocol class
:type protocol: type[Protocol]
:param backlog: a number of unaccepted connections that the system
will allow before refusing new connections
:type backlog: int
:param access_log: Enables writing access logs (slows server)
:type access_log: bool
:param return_asyncio_server: flag that defines whether there's a need
to return asyncio.Server or
start it serving right away
:type return_asyncio_server: bool
:param asyncio_server_kwargs: key-value arguments for
asyncio/uvloop create_server method
:type asyncio_server_kwargs: dict
:param noisy_exceptions: Log exceptions that are normally considered
to be quiet/silent
:type noisy_exceptions: bool
:return: AsyncioServer if return_asyncio_server is true, else Nothing
"""
if sock is None:
host, port = host or "127.0.0.1", port or 8000
if protocol is None:
protocol = (
WebSocketProtocol if self.websocket_enabled else HttpProtocol
)
# Set explicitly passed configuration values
for attribute, value in {
"ACCESS_LOG": access_log,
"NOISY_EXCEPTIONS": noisy_exceptions,
}.items():
if value is not None:
setattr(self.config, attribute, value)
server_settings = self._helper(
host=host,
port=port,
debug=debug,
ssl=ssl,
sock=sock,
unix=unix,
loop=get_event_loop(),
protocol=protocol,
backlog=backlog,
run_async=return_asyncio_server,
)
if self.config.USE_UVLOOP is not _default:
error_logger.warning(
"You are trying to change the uvloop configuration, but "
"this is only effective when using the run(...) method. "
"When using the create_server(...) method Sanic will use "
"the already existing loop."
)
main_start = server_settings.pop("main_start", None)
main_stop = server_settings.pop("main_stop", None)
if main_start or main_stop:
logger.warning(
"Listener events for the main process are not available "
"with create_server()"
)
return await serve(
asyncio_server_kwargs=asyncio_server_kwargs, **server_settings
)
def stop(self):
"""
This kills the Sanic
"""
if self.state.stage is not ServerStage.STOPPED:
self.shutdown_tasks(timeout=0)
for task in all_tasks():
with suppress(AttributeError):
if task.get_name() == "RunServer":
task.cancel()
get_event_loop().stop()
def _helper(
self,
host: Optional[str] = None,
port: Optional[int] = None,
debug: bool = False,
ssl: Union[None, SSLContext, dict, str, list, tuple] = None,
sock: Optional[socket] = None,
unix: Optional[str] = None,
workers: int = 1,
loop: AbstractEventLoop = None,
protocol: Type[Protocol] = HttpProtocol,
backlog: int = 100,
register_sys_signals: bool = True,
run_async: bool = False,
) -> Dict[str, Any]:
"""Helper function used by `run` and `create_server`."""
if self.config.PROXIES_COUNT and self.config.PROXIES_COUNT < 0:
raise ValueError(
"PROXIES_COUNT cannot be negative. "
"https://sanic.readthedocs.io/en/latest/sanic/config.html"
"#proxy-configuration"
)
ssl = process_to_context(ssl)
if not self.state.is_debug:
self.state.mode = Mode.DEBUG if debug else Mode.PRODUCTION
self.state.host = host or ""
self.state.port = port or 0
self.state.workers = workers
self.state.ssl = ssl
self.state.unix = unix
self.state.sock = sock
server_settings = {
"protocol": protocol,
"host": host,
"port": port,
"sock": sock,
"unix": unix,
"ssl": ssl,
"app": self,
"signal": ServerSignal(),
"loop": loop,
"register_sys_signals": register_sys_signals,
"backlog": backlog,
}
self.motd(self.serve_location)
if sys.stdout.isatty() and not self.state.is_debug:
error_logger.warning(
f"{Colors.YELLOW}Sanic is running in PRODUCTION mode. "
"Consider using '--debug' or '--dev' while actively "
f"developing your application.{Colors.END}"
)
# Register start/stop events
for event_name, settings_name, reverse in (
("main_process_start", "main_start", False),
("main_process_stop", "main_stop", True),
):
listeners = self.listeners[event_name].copy()
if reverse:
listeners.reverse()
# Prepend sanic to the arguments when listeners are triggered
listeners = [partial(listener, self) for listener in listeners]
server_settings[settings_name] = listeners # type: ignore
if run_async:
server_settings["run_async"] = True
return server_settings
def motd(self, serve_location):
if self.config.MOTD:
mode = [f"{self.state.mode},"]
if self.state.fast:
mode.append("goin' fast")
if self.state.asgi:
mode.append("ASGI")
else:
if self.state.workers == 1:
mode.append("single worker")
else:
mode.append(f"w/ {self.state.workers} workers")
display = {
"mode": " ".join(mode),
"server": self.state.server,
"python": platform.python_version(),
"platform": platform.platform(),
}
extra = {}
if self.config.AUTO_RELOAD:
reload_display = "enabled"
if self.state.reload_dirs:
reload_display += ", ".join(
[
"",
*(
str(path.absolute())
for path in self.state.reload_dirs
),
]
)
display["auto-reload"] = reload_display
packages = []
for package_name in SANIC_PACKAGES:
module_name = package_name.replace("-", "_")
try:
module = import_module(module_name)
packages.append(f"{package_name}=={module.__version__}")
except ImportError:
...
if packages:
display["packages"] = ", ".join(packages)
if self.config.MOTD_DISPLAY:
extra.update(self.config.MOTD_DISPLAY)
logo = (
get_logo(coffee=self.state.coffee)
if self.config.LOGO == "" or self.config.LOGO is True
else self.config.LOGO
)
MOTD.output(logo, serve_location, display, extra)
@property
def serve_location(self) -> str:
serve_location = ""
proto = "http"
if self.state.ssl is not None:
proto = "https"
if self.state.unix:
serve_location = f"{self.state.unix} {proto}://..."
elif self.state.sock:
serve_location = f"{self.state.sock.getsockname()} {proto}://..."
elif self.state.host and self.state.port:
# colon(:) is legal for a host only in an ipv6 address
display_host = (
f"[{self.state.host}]"
if ":" in self.state.host
else self.state.host
)
serve_location = f"{proto}://{display_host}:{self.state.port}"
return serve_location
@classmethod
def should_auto_reload(cls) -> bool:
return any(app.state.auto_reload for app in cls._app_registry.values())
@classmethod
def serve(cls, primary: Optional[Sanic] = None) -> None:
apps = list(cls._app_registry.values())
if not primary:
try:
primary = apps[0]
except IndexError:
raise RuntimeError("Did not find any applications.")
reloader_start = primary.listeners.get("reload_process_start")
reloader_stop = primary.listeners.get("reload_process_stop")
# We want to run auto_reload if ANY of the applications have it enabled
if (
cls.should_auto_reload()
and os.environ.get("SANIC_SERVER_RUNNING") != "true"
): # no cov
loop = new_event_loop()
trigger_events(reloader_start, loop, primary)
reload_dirs: Set[Path] = primary.state.reload_dirs.union(
*(app.state.reload_dirs for app in apps)
)
reloader_helpers.watchdog(1.0, reload_dirs)
trigger_events(reloader_stop, loop, primary)
return
# This exists primarily for unit testing
if not primary.state.server_info: # no cov
for app in apps:
app.state.server_info.clear()
return
primary_server_info = primary.state.server_info[0]
primary.before_server_start(partial(primary._start_servers, apps=apps))
try:
primary_server_info.stage = ServerStage.SERVING
if primary.state.workers > 1 and os.name != "posix": # no cov
logger.warn(
f"Multiprocessing is currently not supported on {os.name},"
" using workers=1 instead"
)
primary.state.workers = 1
if primary.state.workers == 1:
serve_single(primary_server_info.settings)
elif primary.state.workers == 0:
raise RuntimeError("Cannot serve with no workers")
else:
serve_multiple(
primary_server_info.settings, primary.state.workers
)
except BaseException:
error_logger.exception(
"Experienced exception while trying to serve"
)
raise
finally:
primary_server_info.stage = ServerStage.STOPPED
logger.info("Server Stopped")
for app in apps:
app.state.server_info.clear()
app.router.reset()
app.signal_router.reset()
async def _start_servers(
self,
primary: Sanic,
_,
apps: List[Sanic],
) -> None:
for app in apps:
if (
app.name is not primary.name
and app.state.workers != primary.state.workers
and app.state.server_info
):
message = (
f"The primary application {repr(primary)} is running "
f"with {primary.state.workers} worker(s). All "
"application instances will run with the same number. "
f"You requested {repr(app)} to run with "
f"{app.state.workers} worker(s), which will be ignored "
"in favor of the primary application."
)
if sys.stdout.isatty():
message = "".join(
[
Colors.YELLOW,
message,
Colors.END,
]
)
error_logger.warning(message, exc_info=True)
for server_info in app.state.server_info:
if server_info.stage is not ServerStage.SERVING:
app.state.primary = False
handlers = [
*server_info.settings.pop("main_start", []),
*server_info.settings.pop("main_stop", []),
]
if handlers:
error_logger.warning(
f"Sanic found {len(handlers)} listener(s) on "
"secondary applications attached to the main "
"process. These will be ignored since main "
"process listeners can only be attached to your "
"primary application: "
f"{repr(primary)}"
)
if not server_info.settings["loop"]:
server_info.settings["loop"] = get_running_loop()
try:
server_info.server = await serve(
**server_info.settings,
run_async=True,
reuse_port=bool(primary.state.workers - 1),
)
except OSError as e: # no cov
first_message = (
"An OSError was detected on startup. "
"The encountered error was: "
)
second_message = str(e)
if sys.stdout.isatty():
message_parts = [
Colors.YELLOW,
first_message,
Colors.RED,
second_message,
Colors.END,
]
else:
message_parts = [first_message, second_message]
message = "".join(message_parts)
error_logger.warning(message, exc_info=True)
continue
primary.add_task(
self._run_server(app, server_info), name="RunServer"
)
async def _run_server(
self,
app: RunnerMixin,
server_info: ApplicationServerInfo,
) -> None:
try:
# We should never get to this point without a server
# This is primarily to keep mypy happy
if not server_info.server: # no cov
raise RuntimeError("Could not locate AsyncioServer")
if app.state.stage is ServerStage.STOPPED:
server_info.stage = ServerStage.SERVING
await server_info.server.startup()
await server_info.server.before_start()
await server_info.server.after_start()
await server_info.server.serve_forever()
except CancelledError:
# We should never get to this point without a server
# This is primarily to keep mypy happy
if not server_info.server: # no cov
raise RuntimeError("Could not locate AsyncioServer")
await server_info.server.before_stop()
await server_info.server.close()
await server_info.server.after_stop()
finally:
server_info.stage = ServerStage.STOPPED
server_info.server = None

View File

@@ -13,7 +13,7 @@ ASGISend = Callable[[ASGIMessage], Awaitable[None]]
ASGIReceive = Callable[[], Awaitable[ASGIMessage]] ASGIReceive = Callable[[], Awaitable[ASGIMessage]]
class MockProtocol: class MockProtocol: # no cov
def __init__(self, transport: "MockTransport", loop): def __init__(self, transport: "MockTransport", loop):
# This should be refactored when < 3.8 support is dropped # This should be refactored when < 3.8 support is dropped
self.transport = transport self.transport = transport
@@ -56,7 +56,7 @@ class MockProtocol:
await self._not_paused.wait() await self._not_paused.wait()
class MockTransport: class MockTransport: # no cov
_protocol: Optional[MockProtocol] _protocol: Optional[MockProtocol]
def __init__( def __init__(

View File

@@ -1,11 +1,13 @@
from asyncio.events import AbstractEventLoop from asyncio.events import AbstractEventLoop
from typing import Any, Callable, Coroutine, Optional, TypeVar, Union from typing import Any, Callable, Coroutine, Optional, TypeVar, Union
import sanic
from sanic.request import Request from sanic.request import Request
from sanic.response import BaseHTTPResponse, HTTPResponse from sanic.response import BaseHTTPResponse, HTTPResponse
Sanic = TypeVar("Sanic") Sanic = TypeVar("Sanic", bound="sanic.Sanic")
MiddlewareResponse = Union[ MiddlewareResponse = Union[
Optional[HTTPResponse], Coroutine[Any, Any, Optional[HTTPResponse]] Optional[HTTPResponse], Coroutine[Any, Any, Optional[HTTPResponse]]
@@ -18,8 +20,9 @@ ErrorMiddlewareType = Callable[
[Request, BaseException], Optional[Coroutine[Any, Any, None]] [Request, BaseException], Optional[Coroutine[Any, Any, None]]
] ]
MiddlewareType = Union[RequestMiddlewareType, ResponseMiddlewareType] MiddlewareType = Union[RequestMiddlewareType, ResponseMiddlewareType]
ListenerType = Callable[ ListenerType = Union[
[Sanic, AbstractEventLoop], Optional[Coroutine[Any, Any, None]] Callable[[Sanic], Optional[Coroutine[Any, Any, None]]],
Callable[[Sanic, AbstractEventLoop], Optional[Coroutine[Any, Any, None]]],
] ]
RouteHandler = Callable[..., Coroutine[Any, Any, Optional[HTTPResponse]]] RouteHandler = Callable[..., Coroutine[Any, Any, Optional[HTTPResponse]]]
SignalHandler = Callable[..., Coroutine[Any, Any, None]] SignalHandler = Callable[..., Coroutine[Any, Any, None]]

View File

@@ -0,0 +1,35 @@
from __future__ import annotations
from base64 import b64decode
from dataclasses import dataclass, field
from typing import Optional
@dataclass()
class Credentials:
auth_type: Optional[str]
token: Optional[str]
_username: Optional[str] = field(default=None)
_password: Optional[str] = field(default=None)
def __post_init__(self):
if self._auth_is_basic:
self._username, self._password = (
b64decode(self.token.encode("utf-8")).decode().split(":")
)
@property
def username(self):
if not self._auth_is_basic:
raise AttributeError("Username is available for Basic Auth only")
return self._username
@property
def password(self):
if not self._auth_is_basic:
raise AttributeError("Password is available for Basic Auth only")
return self._password
@property
def _auth_is_basic(self) -> bool:
return self.auth_type == "Basic"

View File

@@ -1,3 +1,5 @@
from __future__ import annotations
from ssl import SSLObject from ssl import SSLObject
from types import SimpleNamespace from types import SimpleNamespace
from typing import Any, Dict, Optional from typing import Any, Dict, Optional

View File

@@ -77,7 +77,7 @@ def _check_file(filename, mtimes):
return need_reload return need_reload
def watchdog(sleep_interval, app): def watchdog(sleep_interval, reload_dirs):
"""Watch project files, restart worker process if a change happened. """Watch project files, restart worker process if a change happened.
:param sleep_interval: interval in second. :param sleep_interval: interval in second.
@@ -100,7 +100,7 @@ def watchdog(sleep_interval, app):
changed = set() changed = set()
for filename in itertools.chain( for filename in itertools.chain(
_iter_module_files(), _iter_module_files(),
*(d.glob("**/*") for d in app.reload_dirs), *(d.glob("**/*") for d in reload_dirs),
): ):
try: try:
if _check_file(filename, mtimes): if _check_file(filename, mtimes):

View File

@@ -14,6 +14,8 @@ from typing import (
from sanic_routing.route import Route # type: ignore from sanic_routing.route import Route # type: ignore
from sanic.models.http_types import Credentials
if TYPE_CHECKING: # no cov if TYPE_CHECKING: # no cov
from sanic.server import ConnInfo from sanic.server import ConnInfo
@@ -28,15 +30,17 @@ 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 httptools.parser.errors import HttpParserInvalidURLError # type: ignore
from sanic.compat import CancelledErrors, Header from sanic.compat import CancelledErrors, Header
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE
from sanic.exceptions import InvalidUsage, ServerError from sanic.exceptions import BadURL, InvalidUsage, ServerError
from sanic.headers import ( from sanic.headers import (
AcceptContainer, AcceptContainer,
Options, Options,
parse_accept, parse_accept,
parse_content_header, parse_content_header,
parse_credentials,
parse_forwarded, parse_forwarded,
parse_host, parse_host,
parse_xforwarded, parse_xforwarded,
@@ -98,11 +102,13 @@ class Request:
"method", "method",
"parsed_accept", "parsed_accept",
"parsed_args", "parsed_args",
"parsed_not_grouped_args", "parsed_credentials",
"parsed_files", "parsed_files",
"parsed_form", "parsed_form",
"parsed_json",
"parsed_forwarded", "parsed_forwarded",
"parsed_json",
"parsed_not_grouped_args",
"parsed_token",
"raw_url", "raw_url",
"responded", "responded",
"request_middleware_started", "request_middleware_started",
@@ -122,9 +128,12 @@ class Request:
app: Sanic, app: Sanic,
head: bytes = b"", head: bytes = b"",
): ):
self.raw_url = url_bytes self.raw_url = url_bytes
# TODO: Content-Encoding detection try:
self._parsed_url = parse_url(url_bytes) self._parsed_url = parse_url(url_bytes)
except HttpParserInvalidURLError:
raise BadURL(f"Bad URL: {url_bytes.decode()}")
self._id: Optional[Union[uuid.UUID, str, int]] = None self._id: Optional[Union[uuid.UUID, str, int]] = None
self._name: Optional[str] = None self._name: Optional[str] = None
self.app = app self.app = app
@@ -141,9 +150,11 @@ class Request:
self.ctx = SimpleNamespace() self.ctx = SimpleNamespace()
self.parsed_forwarded: Optional[Options] = None self.parsed_forwarded: Optional[Options] = None
self.parsed_accept: Optional[AcceptContainer] = None self.parsed_accept: Optional[AcceptContainer] = None
self.parsed_credentials: Optional[Credentials] = 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_token: Optional[str] = None
self.parsed_args: DefaultDict[ self.parsed_args: DefaultDict[
Tuple[bool, bool, str, str], RequestParameters Tuple[bool, bool, str, str], RequestParameters
] = defaultdict(RequestParameters) ] = defaultdict(RequestParameters)
@@ -189,6 +200,53 @@ class Request:
headers: Optional[Union[Header, Dict[str, str]]] = None, headers: Optional[Union[Header, Dict[str, str]]] = None,
content_type: Optional[str] = None, content_type: Optional[str] = None,
): ):
"""Respond to the request without returning.
This method can only be called once, as you can only respond once.
If no ``response`` argument is passed, one will be created from the
``status``, ``headers`` and ``content_type`` arguments.
**The first typical usecase** is if you wish to respond to the
request without returning from the handler:
.. code-block:: python
@app.get("/")
async def handler(request: Request):
data = ... # Process something
json_response = json({"data": data})
await request.respond(json_response)
# You are now free to continue executing other code
...
@app.on_response
async def add_header(_, response: HTTPResponse):
# Middlewares still get executed as expected
response.headers["one"] = "two"
**The second possible usecase** is for when you want to directly
respond to the request:
.. code-block:: python
response = await request.respond(content_type="text/csv")
await response.send("foo,")
await response.send("bar")
# You can control the completion of the response by calling
# the 'eof()' method:
await response.eof()
:param response: response instance to send
:param status: status code to return in the response
:param headers: headers to return in the response
:param content_type: Content-Type header of the response
:return: final response being sent (may be different from the
``response`` parameter because of middlewares) which can be
used to manually send data
"""
try: try:
if self.stream is not None and self.stream.response: if self.stream is not None and self.stream.response:
raise ServerError("Second respond call is not allowed.") raise ServerError("Second respond call is not allowed.")
@@ -332,20 +390,41 @@ class Request:
return self.parsed_accept return self.parsed_accept
@property @property
def token(self): def token(self) -> Optional[str]:
"""Attempt to return the auth header token. """Attempt to return the auth header token.
:return: token related to request :return: token related to request
""" """
prefixes = ("Bearer", "Token") if self.parsed_token is None:
auth_header = self.headers.getone("authorization", None) prefixes = ("Bearer", "Token")
_, token = parse_credentials(
self.headers.getone("authorization", None), prefixes
)
self.parsed_token = token
return self.parsed_token
if auth_header is not None: @property
for prefix in prefixes: def credentials(self) -> Optional[Credentials]:
if prefix in auth_header: """Attempt to return the auth header value.
return auth_header.partition(prefix)[-1].strip()
return auth_header Covers NoAuth, Basic Auth, Bearer Token, Api Token authentication
schemas.
:return: A Credentials object with token, or username and password
related to the request
"""
if self.parsed_credentials is None:
try:
prefix, credentials = parse_credentials(
self.headers.getone("authorization", None)
)
if credentials:
self.parsed_credentials = Credentials(
auth_type=prefix, token=credentials
)
except ValueError:
pass
return self.parsed_credentials
@property @property
def form(self): def form(self):

View File

@@ -50,6 +50,16 @@ class BaseHTTPResponse:
The base class for all HTTP Responses The base class for all HTTP Responses
""" """
__slots__ = (
"asgi",
"body",
"content_type",
"stream",
"status",
"headers",
"_cookies",
)
_dumps = json_dumps _dumps = json_dumps
def __init__(self): def __init__(self):
@@ -156,7 +166,7 @@ class HTTPResponse(BaseHTTPResponse):
:type content_type: Optional[str] :type content_type: Optional[str]
""" """
__slots__ = ("body", "status", "content_type", "headers", "_cookies") __slots__ = ()
def __init__( def __init__(
self, self,

View File

@@ -1,8 +1,18 @@
from __future__ import annotations
from inspect import isawaitable from inspect import isawaitable
from typing import Any, Callable, Iterable, Optional from typing import TYPE_CHECKING, Any, Callable, Iterable, Optional
def trigger_events(events: Optional[Iterable[Callable[..., Any]]], loop): if TYPE_CHECKING: # no cov
from sanic import Sanic
def trigger_events(
events: Optional[Iterable[Callable[..., Any]]],
loop,
app: Optional[Sanic] = None,
):
""" """
Trigger event callbacks (functions or async) Trigger event callbacks (functions or async)
@@ -11,6 +21,9 @@ def trigger_events(events: Optional[Iterable[Callable[..., Any]]], loop):
""" """
if events: if events:
for event in events: for event in events:
result = event(loop) try:
result = event() if not app else event(app)
except TypeError:
result = event(loop) if not app else event(app, loop)
if isawaitable(result): if isawaitable(result):
loop.run_until_complete(result) loop.run_until_complete(result)

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Optional from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING: if TYPE_CHECKING: # no cov
from sanic.app import Sanic from sanic.app import Sanic
import asyncio import asyncio

View File

@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Optional
from sanic.touchup.meta import TouchUpMeta from sanic.touchup.meta import TouchUpMeta
if TYPE_CHECKING: if TYPE_CHECKING: # no cov
from sanic.app import Sanic from sanic.app import Sanic
from asyncio import CancelledError from asyncio import CancelledError

View File

@@ -5,13 +5,13 @@ from websockets.server import ServerConnection
from websockets.typing import Subprotocol from websockets.typing import Subprotocol
from sanic.exceptions import ServerError from sanic.exceptions import ServerError
from sanic.log import deprecation, error_logger from sanic.log import logger
from sanic.server import HttpProtocol from sanic.server import HttpProtocol
from ..websockets.impl import WebsocketImplProtocol from ..websockets.impl import WebsocketImplProtocol
if TYPE_CHECKING: if TYPE_CHECKING: # no cov
from websockets import http11 from websockets import http11
@@ -29,9 +29,6 @@ class WebSocketProtocol(HttpProtocol):
*args, *args,
websocket_timeout: float = 10.0, websocket_timeout: float = 10.0,
websocket_max_size: Optional[int] = None, websocket_max_size: Optional[int] = None,
websocket_max_queue: Optional[int] = None, # max_queue is deprecated
websocket_read_limit: Optional[int] = None, # read_limit is deprecated
websocket_write_limit: Optional[int] = None, # write_limit deprecated
websocket_ping_interval: Optional[float] = 20.0, websocket_ping_interval: Optional[float] = 20.0,
websocket_ping_timeout: Optional[float] = 20.0, websocket_ping_timeout: Optional[float] = 20.0,
**kwargs, **kwargs,
@@ -40,27 +37,6 @@ class WebSocketProtocol(HttpProtocol):
self.websocket: Optional[WebsocketImplProtocol] = None self.websocket: Optional[WebsocketImplProtocol] = None
self.websocket_timeout = websocket_timeout self.websocket_timeout = websocket_timeout
self.websocket_max_size = websocket_max_size self.websocket_max_size = websocket_max_size
if websocket_max_queue is not None and websocket_max_queue > 0:
# TODO: Reminder remove this warning in v22.3
deprecation(
"Websocket no longer uses queueing, so websocket_max_queue"
" is no longer required.",
22.3,
)
if websocket_read_limit is not None and websocket_read_limit > 0:
# TODO: Reminder remove this warning in v22.3
deprecation(
"Websocket no longer uses read buffers, so "
"websocket_read_limit is not required.",
22.3,
)
if websocket_write_limit is not None and websocket_write_limit > 0:
# TODO: Reminder remove this warning in v22.3
deprecation(
"Websocket no longer uses write buffers, so "
"websocket_write_limit is not required.",
22.3,
)
self.websocket_ping_interval = websocket_ping_interval self.websocket_ping_interval = websocket_ping_interval
self.websocket_ping_timeout = websocket_ping_timeout self.websocket_ping_timeout = websocket_ping_timeout
@@ -128,7 +104,7 @@ class WebSocketProtocol(HttpProtocol):
max_size=self.websocket_max_size, max_size=self.websocket_max_size,
subprotocols=subprotocols, subprotocols=subprotocols,
state=OPEN, state=OPEN,
logger=error_logger, logger=logger,
) )
resp: "http11.Response" = ws_conn.accept(request) resp: "http11.Response" = ws_conn.accept(request)
except Exception: except Exception:

View File

@@ -132,7 +132,7 @@ def serve(
try: try:
http_server = loop.run_until_complete(server_coroutine) http_server = loop.run_until_complete(server_coroutine)
except BaseException: except BaseException:
error_logger.exception("Unable to start server") error_logger.exception("Unable to start server", exc_info=True)
return return
# Ignore SIGINT when run_multiple # Ignore SIGINT when run_multiple

View File

@@ -9,7 +9,7 @@ from websockets.typing import Data
from sanic.exceptions import ServerError from sanic.exceptions import ServerError
if TYPE_CHECKING: if TYPE_CHECKING: # no cov
from .impl import WebsocketImplProtocol from .impl import WebsocketImplProtocol
UTF8Decoder = codecs.getincrementaldecoder("utf-8") UTF8Decoder = codecs.getincrementaldecoder("utf-8")
@@ -37,7 +37,7 @@ class WebsocketFrameAssembler:
"get_id", "get_id",
"put_id", "put_id",
) )
if TYPE_CHECKING: if TYPE_CHECKING: # no cov
protocol: "WebsocketImplProtocol" protocol: "WebsocketImplProtocol"
read_mutex: asyncio.Lock read_mutex: asyncio.Lock
write_mutex: asyncio.Lock write_mutex: asyncio.Lock
@@ -131,7 +131,7 @@ class WebsocketFrameAssembler:
if self.paused: if self.paused:
self.protocol.resume_frames() self.protocol.resume_frames()
self.paused = False self.paused = False
if not self.get_in_progress: if not self.get_in_progress: # no cov
# This should be guarded against with the read_mutex, # This should be guarded against with the read_mutex,
# exception is here as a failsafe # exception is here as a failsafe
raise ServerError( raise ServerError(
@@ -204,7 +204,7 @@ class WebsocketFrameAssembler:
if self.paused: if self.paused:
self.protocol.resume_frames() self.protocol.resume_frames()
self.paused = False self.paused = False
if not self.get_in_progress: if not self.get_in_progress: # no cov
# This should be guarded against with the read_mutex, # This should be guarded against with the read_mutex,
# exception is here as a failsafe # exception is here as a failsafe
raise ServerError( raise ServerError(
@@ -212,7 +212,7 @@ class WebsocketFrameAssembler:
"asynchronous get was in progress." "asynchronous get was in progress."
) )
self.get_in_progress = False self.get_in_progress = False
if not self.message_complete.is_set(): if not self.message_complete.is_set(): # no cov
# This should be guarded against with the read_mutex, # This should be guarded against with the read_mutex,
# exception is here as a failsafe # exception is here as a failsafe
raise ServerError( raise ServerError(
@@ -220,7 +220,7 @@ class WebsocketFrameAssembler:
"message was complete." "message was complete."
) )
self.message_complete.clear() self.message_complete.clear()
if self.message_fetched.is_set(): if self.message_fetched.is_set(): # no cov
# This should be guarded against with the read_mutex, # This should be guarded against with the read_mutex,
# and get_in_progress check, this exception is # and get_in_progress check, this exception is
# here as a failsafe # here as a failsafe

View File

@@ -518,8 +518,12 @@ class WebsocketImplProtocol:
) )
try: try:
self.recv_cancel = asyncio.Future() self.recv_cancel = asyncio.Future()
tasks = (
self.recv_cancel,
asyncio.ensure_future(self.assembler.get(timeout)),
)
done, pending = await asyncio.wait( done, pending = await asyncio.wait(
(self.recv_cancel, self.assembler.get(timeout)), tasks,
return_when=asyncio.FIRST_COMPLETED, return_when=asyncio.FIRST_COMPLETED,
) )
done_task = next(iter(done)) done_task = next(iter(done))
@@ -570,8 +574,12 @@ class WebsocketImplProtocol:
self.can_pause = False self.can_pause = False
self.recv_cancel = asyncio.Future() self.recv_cancel = asyncio.Future()
while True: while True:
tasks = (
self.recv_cancel,
asyncio.ensure_future(self.assembler.get(timeout=0)),
)
done, pending = await asyncio.wait( done, pending = await asyncio.wait(
(self.recv_cancel, self.assembler.get(timeout=0)), tasks,
return_when=asyncio.FIRST_COMPLETED, return_when=asyncio.FIRST_COMPLETED,
) )
done_task = next(iter(done)) done_task = next(iter(done))

View File

@@ -80,6 +80,7 @@ class SignalRouter(BaseRouter):
group_class=SignalGroup, group_class=SignalGroup,
stacking=True, stacking=True,
) )
self.allow_fail_builtin = True
self.ctx.loop = None self.ctx.loop = None
def get( # type: ignore def get( # type: ignore
@@ -129,7 +130,8 @@ class SignalRouter(BaseRouter):
try: try:
group, handlers, params = self.get(event, condition=condition) group, handlers, params = self.get(event, condition=condition)
except NotFound as e: except NotFound as e:
if fail_not_found: is_reserved = event.split(".", 1)[0] in RESERVED_NAMESPACES
if fail_not_found and (not is_reserved or self.allow_fail_builtin):
raise e raise e
else: else:
if self.ctx.app.debug and self.ctx.app.state.verbosity >= 1: if self.ctx.app.debug and self.ctx.app.state.verbosity >= 1:

View File

@@ -10,12 +10,14 @@ from .base import BaseScheme
class OptionalDispatchEvent(BaseScheme): class OptionalDispatchEvent(BaseScheme):
ident = "ODE" ident = "ODE"
SYNC_SIGNAL_NAMESPACES = "http."
def __init__(self, app) -> None: def __init__(self, app) -> None:
super().__init__(app) super().__init__(app)
self._sync_events()
self._registered_events = [ self._registered_events = [
signal.path for signal in app.signal_router.routes signal.name for signal in app.signal_router.routes
] ]
def run(self, method, module_globals): def run(self, method, module_globals):
@@ -31,6 +33,35 @@ class OptionalDispatchEvent(BaseScheme):
return exec_locals[method.__name__] return exec_locals[method.__name__]
def _sync_events(self):
all_events = set()
app_events = {}
for app in self.app.__class__._app_registry.values():
if app.state.server_info:
app_events[app] = {
signal.name for signal in app.signal_router.routes
}
all_events.update(app_events[app])
for app, events in app_events.items():
missing = {
x
for x in all_events.difference(events)
if any(x.startswith(y) for y in self.SYNC_SIGNAL_NAMESPACES)
}
if missing:
was_finalized = app.signal_router.finalized
if was_finalized: # no cov
app.signal_router.reset()
for event in missing:
app.signal(event)(self.noop)
if was_finalized: # no cov
app.signal_router.finalize()
@staticmethod
async def noop(**_): # no cov
...
class RemoveDispatch(NodeTransformer): class RemoveDispatch(NodeTransformer):
def __init__(self, registered_events, verbosity: int = 0) -> None: def __init__(self, registered_events, verbosity: int = 0) -> None:

View File

@@ -1,243 +0,0 @@
import asyncio
import logging
import os
import signal
import sys
import traceback
from gunicorn.workers import base # type: ignore
from sanic.compat import UVLOOP_INSTALLED
from sanic.log import logger
from sanic.server import HttpProtocol, Signal, serve, try_use_uvloop
from sanic.server.protocols.websocket_protocol import WebSocketProtocol
try:
import ssl # type: ignore
except ImportError: # no cov
ssl = None # type: ignore
if UVLOOP_INSTALLED: # no cov
try_use_uvloop()
class GunicornWorker(base.Worker):
http_protocol = HttpProtocol
websocket_protocol = WebSocketProtocol
def __init__(self, *args, **kw): # pragma: no cover
super().__init__(*args, **kw)
cfg = self.cfg
if cfg.is_ssl:
self.ssl_context = self._create_ssl_context(cfg)
else:
self.ssl_context = None
self.servers = {}
self.connections = set()
self.exit_code = 0
self.signal = Signal()
def init_process(self):
# create new event_loop after fork
asyncio.get_event_loop().close()
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
super().init_process()
def run(self):
is_debug = self.log.loglevel == logging.DEBUG
protocol = (
self.websocket_protocol
if self.app.callable.websocket_enabled
else self.http_protocol
)
self._server_settings = self.app.callable._helper(
loop=self.loop,
debug=is_debug,
protocol=protocol,
ssl=self.ssl_context,
run_async=True,
)
self._server_settings["signal"] = self.signal
self._server_settings.pop("sock")
self._await(self.app.callable._startup())
self._await(
self.app.callable._server_event("init", "before", loop=self.loop)
)
main_start = self._server_settings.pop("main_start", None)
main_stop = self._server_settings.pop("main_stop", None)
if main_start or main_stop: # noqa
logger.warning(
"Listener events for the main process are not available "
"with GunicornWorker"
)
try:
self._await(self._run())
self.app.callable.is_running = True
self._await(
self.app.callable._server_event(
"init", "after", loop=self.loop
)
)
self.loop.run_until_complete(self._check_alive())
self._await(
self.app.callable._server_event(
"shutdown", "before", loop=self.loop
)
)
self.loop.run_until_complete(self.close())
except BaseException:
traceback.print_exc()
finally:
try:
self._await(
self.app.callable._server_event(
"shutdown", "after", loop=self.loop
)
)
except BaseException:
traceback.print_exc()
finally:
self.loop.close()
sys.exit(self.exit_code)
async def close(self):
if self.servers:
# stop accepting connections
self.log.info(
"Stopping server: %s, connections: %s",
self.pid,
len(self.connections),
)
for server in self.servers:
server.close()
await server.wait_closed()
self.servers.clear()
# prepare connections for closing
self.signal.stopped = True
for conn in self.connections:
conn.close_if_idle()
# gracefully shutdown timeout
start_shutdown = 0
graceful_shutdown_timeout = self.cfg.graceful_timeout
while self.connections and (
start_shutdown < graceful_shutdown_timeout
):
await asyncio.sleep(0.1)
start_shutdown = start_shutdown + 0.1
# Force close non-idle connection after waiting for
# graceful_shutdown_timeout
for conn in self.connections:
if hasattr(conn, "websocket") and conn.websocket:
conn.websocket.fail_connection(code=1001)
else:
conn.abort()
async def _run(self):
for sock in self.sockets:
state = dict(requests_count=0)
self._server_settings["host"] = None
self._server_settings["port"] = None
server = await serve(
sock=sock,
connections=self.connections,
state=state,
**self._server_settings
)
self.servers[server] = state
async def _check_alive(self):
# If our parent changed then we shut down.
pid = os.getpid()
try:
while self.alive:
self.notify()
req_count = sum(
self.servers[srv]["requests_count"] for srv in self.servers
)
if self.max_requests and req_count > self.max_requests:
self.alive = False
self.log.info(
"Max requests exceeded, shutting down: %s", self
)
elif pid == os.getpid() and self.ppid != os.getppid():
self.alive = False
self.log.info("Parent changed, shutting down: %s", self)
else:
await asyncio.sleep(1.0, loop=self.loop)
except (Exception, BaseException, GeneratorExit, KeyboardInterrupt):
pass
@staticmethod
def _create_ssl_context(cfg):
"""Creates SSLContext instance for usage in asyncio.create_server.
See ssl.SSLSocket.__init__ for more details.
"""
ctx = ssl.SSLContext(cfg.ssl_version)
ctx.load_cert_chain(cfg.certfile, cfg.keyfile)
ctx.verify_mode = cfg.cert_reqs
if cfg.ca_certs:
ctx.load_verify_locations(cfg.ca_certs)
if cfg.ciphers:
ctx.set_ciphers(cfg.ciphers)
return ctx
def init_signals(self):
# Set up signals through the event loop API.
self.loop.add_signal_handler(
signal.SIGQUIT, self.handle_quit, signal.SIGQUIT, None
)
self.loop.add_signal_handler(
signal.SIGTERM, self.handle_exit, signal.SIGTERM, None
)
self.loop.add_signal_handler(
signal.SIGINT, self.handle_quit, signal.SIGINT, None
)
self.loop.add_signal_handler(
signal.SIGWINCH, self.handle_winch, signal.SIGWINCH, None
)
self.loop.add_signal_handler(
signal.SIGUSR1, self.handle_usr1, signal.SIGUSR1, None
)
self.loop.add_signal_handler(
signal.SIGABRT, self.handle_abort, signal.SIGABRT, None
)
# Don't let SIGTERM and SIGUSR1 disturb active requests
# by interrupting system calls
signal.siginterrupt(signal.SIGTERM, False)
signal.siginterrupt(signal.SIGUSR1, False)
def handle_quit(self, sig, frame):
self.alive = False
self.app.callable.is_running = False
self.cfg.worker_int(self)
def handle_abort(self, sig, frame):
self.alive = False
self.exit_code = 1
self.cfg.worker_abort(self)
sys.exit(1)
def _await(self, coro):
fut = asyncio.ensure_future(coro, loop=self.loop)
self.loop.run_until_complete(fut)

View File

@@ -84,17 +84,17 @@ ujson = "ujson>=1.35" + env_dependency
uvloop = "uvloop>=0.5.3" + env_dependency uvloop = "uvloop>=0.5.3" + env_dependency
types_ujson = "types-ujson" + env_dependency types_ujson = "types-ujson" + env_dependency
requirements = [ requirements = [
"sanic-routing~=0.7", "sanic-routing>=22.3.0,<22.6.0",
"httptools>=0.0.10", "httptools>=0.0.10",
uvloop, uvloop,
ujson, ujson,
"aiofiles>=0.6.0", "aiofiles>=0.6.0",
"websockets>=10.0", "websockets>=10.0",
"multidict>=5.0,<6.0", "multidict>=5.0,<7.0",
] ]
tests_require = [ tests_require = [
"sanic-testing>=0.7.0", "sanic-testing>=22.3.0",
"pytest==6.2.5", "pytest==6.2.5",
"coverage==5.3", "coverage==5.3",
"gunicorn==20.0.4", "gunicorn==20.0.4",
@@ -112,6 +112,7 @@ tests_require = [
"docutils", "docutils",
"pygments", "pygments",
"uvicorn<0.15.0", "uvicorn<0.15.0",
"slotscheck>=0.8.0,<1",
types_ujson, types_ujson,
] ]

34
tests/asyncmock.py Normal file
View File

@@ -0,0 +1,34 @@
"""
For 3.7 compat
"""
from unittest.mock import Mock
class AsyncMock(Mock):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.await_count = 0
def __call__(self, *args, **kwargs):
self.call_count += 1
parent = super(AsyncMock, self)
async def dummy():
self.await_count += 1
return parent.__call__(*args, **kwargs)
return dummy()
def __await__(self):
return self().__await__()
def assert_awaited_once(self):
if not self.await_count == 1:
msg = (
f"Expected to have been awaited once."
f" Awaited {self.await_count} times."
)
raise AssertionError(msg)

View File

@@ -175,6 +175,21 @@ def run_startup(caplog):
return run return run
@pytest.fixture
def run_multi(caplog):
def run(app, level=logging.DEBUG):
@app.after_server_start
async def stop(app, _):
app.stop()
with caplog.at_level(level):
Sanic.serve()
return caplog.record_tuples
return run
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def message_in_records(): def message_in_records():
def msg_in_log(records: List[LogRecord], msg: str): def msg_in_log(records: List[LogRecord], msg: str):

View File

@@ -34,3 +34,12 @@ async def shutdown(app: Sanic, _):
def create_app(): def create_app():
return app return app
def create_app_with_args(args):
try:
print(f"foo={args.foo}")
except AttributeError:
print(f"module={args.module}")
return app

View File

@@ -197,7 +197,7 @@ def test_app_enable_websocket(app, websocket_enabled, enable):
assert app.websocket_enabled == True assert app.websocket_enabled == True
@patch("sanic.app.WebSocketProtocol") @patch("sanic.mixins.runner.WebSocketProtocol")
def test_app_websocket_parameters(websocket_protocol_mock, app): def test_app_websocket_parameters(websocket_protocol_mock, app):
app.config.WEBSOCKET_MAX_SIZE = 44 app.config.WEBSOCKET_MAX_SIZE = 44
app.config.WEBSOCKET_PING_TIMEOUT = 48 app.config.WEBSOCKET_PING_TIMEOUT = 48
@@ -473,13 +473,14 @@ def test_custom_context():
assert app.ctx == ctx assert app.ctx == ctx
def test_uvloop_config(app, monkeypatch): @pytest.mark.parametrize("use", (False, True))
def test_uvloop_config(app, monkeypatch, use):
@app.get("/test") @app.get("/test")
def handler(request): def handler(request):
return text("ok") return text("ok")
try_use_uvloop = Mock() try_use_uvloop = Mock()
monkeypatch.setattr(sanic.app, "try_use_uvloop", try_use_uvloop) monkeypatch.setattr(sanic.mixins.runner, "try_use_uvloop", try_use_uvloop)
# Default config # Default config
app.test_client.get("/test") app.test_client.get("/test")
@@ -489,14 +490,13 @@ def test_uvloop_config(app, monkeypatch):
try_use_uvloop.assert_called_once() try_use_uvloop.assert_called_once()
try_use_uvloop.reset_mock() try_use_uvloop.reset_mock()
app.config["USE_UVLOOP"] = False app.config["USE_UVLOOP"] = use
app.test_client.get("/test") app.test_client.get("/test")
try_use_uvloop.assert_not_called()
try_use_uvloop.reset_mock() if use:
app.config["USE_UVLOOP"] = True try_use_uvloop.assert_called_once()
app.test_client.get("/test") else:
try_use_uvloop.assert_called_once() try_use_uvloop.assert_not_called()
def test_uvloop_cannot_never_called_with_create_server(caplog, monkeypatch): def test_uvloop_cannot_never_called_with_create_server(caplog, monkeypatch):
@@ -506,7 +506,7 @@ def test_uvloop_cannot_never_called_with_create_server(caplog, monkeypatch):
apps[2].config.USE_UVLOOP = True apps[2].config.USE_UVLOOP = True
try_use_uvloop = Mock() try_use_uvloop = Mock()
monkeypatch.setattr(sanic.app, "try_use_uvloop", try_use_uvloop) monkeypatch.setattr(sanic.mixins.runner, "try_use_uvloop", try_use_uvloop)
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
@@ -569,3 +569,8 @@ def test_cannot_run_fast_and_workers(app):
message = "You cannot use both fast=True and workers=X" message = "You cannot use both fast=True and workers=X"
with pytest.raises(RuntimeError, match=message): with pytest.raises(RuntimeError, match=message):
app.run(fast=True, workers=4) app.run(fast=True, workers=4)
def test_no_workers(app):
with pytest.raises(RuntimeError, match="Cannot serve with no workers"):
app.run(workers=0)

View File

@@ -1,4 +1,5 @@
import asyncio import asyncio
import logging
from collections import deque, namedtuple from collections import deque, namedtuple
@@ -6,6 +7,7 @@ import pytest
import uvicorn import uvicorn
from sanic import Sanic from sanic import Sanic
from sanic.application.state import Mode
from sanic.asgi import MockTransport from sanic.asgi import MockTransport
from sanic.exceptions import Forbidden, InvalidUsage, ServiceUnavailable from sanic.exceptions import Forbidden, InvalidUsage, ServiceUnavailable
from sanic.request import Request from sanic.request import Request
@@ -44,7 +46,7 @@ def protocol(transport):
return transport.get_protocol() return transport.get_protocol()
def test_listeners_triggered(): def test_listeners_triggered(caplog):
app = Sanic("app") app = Sanic("app")
before_server_start = False before_server_start = False
after_server_start = False after_server_start = False
@@ -82,9 +84,31 @@ def test_listeners_triggered():
config = uvicorn.Config(app=app, loop="asyncio", limit_max_requests=0) config = uvicorn.Config(app=app, loop="asyncio", limit_max_requests=0)
server = CustomServer(config=config) server = CustomServer(config=config)
with pytest.warns(UserWarning): start_message = (
'You have set a listener for "before_server_start" in ASGI mode. '
"It will be executed as early as possible, but not before the ASGI "
"server is started."
)
stop_message = (
'You have set a listener for "after_server_stop" in ASGI mode. '
"It will be executed as late as possible, but not after the ASGI "
"server is stopped."
)
with caplog.at_level(logging.DEBUG):
server.run() server.run()
assert (
"sanic.root",
logging.DEBUG,
start_message,
) not in caplog.record_tuples
assert (
"sanic.root",
logging.DEBUG,
stop_message,
) not in caplog.record_tuples
all_tasks = asyncio.all_tasks(asyncio.get_event_loop()) all_tasks = asyncio.all_tasks(asyncio.get_event_loop())
for task in all_tasks: for task in all_tasks:
task.cancel() task.cancel()
@@ -94,8 +118,38 @@ def test_listeners_triggered():
assert before_server_stop assert before_server_stop
assert after_server_stop assert after_server_stop
app.state.mode = Mode.DEBUG
with caplog.at_level(logging.DEBUG):
server.run()
def test_listeners_triggered_async(app): assert (
"sanic.root",
logging.DEBUG,
start_message,
) not in caplog.record_tuples
assert (
"sanic.root",
logging.DEBUG,
stop_message,
) not in caplog.record_tuples
app.state.verbosity = 2
with caplog.at_level(logging.DEBUG):
server.run()
assert (
"sanic.root",
logging.DEBUG,
start_message,
) in caplog.record_tuples
assert (
"sanic.root",
logging.DEBUG,
stop_message,
) in caplog.record_tuples
def test_listeners_triggered_async(app, caplog):
before_server_start = False before_server_start = False
after_server_start = False after_server_start = False
before_server_stop = False before_server_stop = False
@@ -132,9 +186,31 @@ def test_listeners_triggered_async(app):
config = uvicorn.Config(app=app, loop="asyncio", limit_max_requests=0) config = uvicorn.Config(app=app, loop="asyncio", limit_max_requests=0)
server = CustomServer(config=config) server = CustomServer(config=config)
with pytest.warns(UserWarning): start_message = (
'You have set a listener for "before_server_start" in ASGI mode. '
"It will be executed as early as possible, but not before the ASGI "
"server is started."
)
stop_message = (
'You have set a listener for "after_server_stop" in ASGI mode. '
"It will be executed as late as possible, but not after the ASGI "
"server is stopped."
)
with caplog.at_level(logging.DEBUG):
server.run() server.run()
assert (
"sanic.root",
logging.DEBUG,
start_message,
) not in caplog.record_tuples
assert (
"sanic.root",
logging.DEBUG,
stop_message,
) not in caplog.record_tuples
all_tasks = asyncio.all_tasks(asyncio.get_event_loop()) all_tasks = asyncio.all_tasks(asyncio.get_event_loop())
for task in all_tasks: for task in all_tasks:
task.cancel() task.cancel()
@@ -144,6 +220,36 @@ def test_listeners_triggered_async(app):
assert before_server_stop assert before_server_stop
assert after_server_stop assert after_server_stop
app.state.mode = Mode.DEBUG
with caplog.at_level(logging.DEBUG):
server.run()
assert (
"sanic.root",
logging.DEBUG,
start_message,
) not in caplog.record_tuples
assert (
"sanic.root",
logging.DEBUG,
stop_message,
) not in caplog.record_tuples
app.state.verbosity = 2
with caplog.at_level(logging.DEBUG):
server.run()
assert (
"sanic.root",
logging.DEBUG,
start_message,
) in caplog.record_tuples
assert (
"sanic.root",
logging.DEBUG,
stop_message,
) in caplog.record_tuples
def test_non_default_uvloop_config_raises_warning(app): def test_non_default_uvloop_config_raises_warning(app):
app.config.USE_UVLOOP = True app.config.USE_UVLOOP = True

View File

@@ -39,16 +39,17 @@ def read_app_info(lines):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"appname", "appname,extra",
( (
"fake.server.app", ("fake.server.app", None),
"fake.server:app", ("fake.server:create_app", "--factory"),
"fake.server:create_app()", ("fake.server.create_app()", None),
"fake.server.create_app()",
), ),
) )
def test_server_run(appname): def test_server_run(appname, extra):
command = ["sanic", appname] command = ["sanic", appname]
if extra:
command.append(extra)
out, err, exitcode = capture(command) out, err, exitcode = capture(command)
lines = out.split(b"\n") lines = out.split(b"\n")
firstline = lines[starting_line(lines) + 1] firstline = lines[starting_line(lines) + 1]
@@ -57,6 +58,49 @@ def test_server_run(appname):
assert firstline == b"Goin' Fast @ http://127.0.0.1:8000" assert firstline == b"Goin' Fast @ http://127.0.0.1:8000"
def test_server_run_factory_with_args():
command = [
"sanic",
"fake.server.create_app_with_args",
"--factory",
]
out, err, exitcode = capture(command)
lines = out.split(b"\n")
assert exitcode != 1, lines
assert b"module=fake.server.create_app_with_args" in lines
def test_server_run_factory_with_args_arbitrary():
command = [
"sanic",
"fake.server.create_app_with_args",
"--factory",
"--foo=bar",
]
out, err, exitcode = capture(command)
lines = out.split(b"\n")
assert exitcode != 1, lines
assert b"foo=bar" in lines
def test_error_with_function_as_instance_without_factory_arg():
command = ["sanic", "fake.server.create_app"]
out, err, exitcode = capture(command)
assert b"try: \nsanic fake.server.create_app --factory" in err
assert exitcode != 1
def test_error_with_path_as_instance_without_simple_arg():
command = ["sanic", "./fake/"]
out, err, exitcode = capture(command)
assert (
b"Please use --simple if you are passing a directory to sanic." in err
)
assert exitcode != 1
@pytest.mark.parametrize( @pytest.mark.parametrize(
"cmd", "cmd",
( (
@@ -103,7 +147,7 @@ def test_tls_wrong_options(cmd):
assert not out assert not out
lines = err.decode().split("\n") lines = err.decode().split("\n")
errmsg = lines[8] errmsg = lines[6]
assert errmsg == "TLS certificates must be specified by either of:" assert errmsg == "TLS certificates must be specified by either of:"
@@ -118,10 +162,10 @@ def test_host_port_localhost(cmd):
command = ["sanic", "fake.server.app", *cmd] command = ["sanic", "fake.server.app", *cmd]
out, err, exitcode = capture(command) out, err, exitcode = capture(command)
lines = out.split(b"\n") lines = out.split(b"\n")
firstline = lines[starting_line(lines) + 1] expected = b"Goin' Fast @ http://localhost:9999"
assert exitcode != 1 assert exitcode != 1
assert firstline == b"Goin' Fast @ http://localhost:9999" assert expected in lines, f"Lines found: {lines}\nErr output: {err}"
@pytest.mark.parametrize( @pytest.mark.parametrize(
@@ -135,10 +179,10 @@ def test_host_port_ipv4(cmd):
command = ["sanic", "fake.server.app", *cmd] command = ["sanic", "fake.server.app", *cmd]
out, err, exitcode = capture(command) out, err, exitcode = capture(command)
lines = out.split(b"\n") lines = out.split(b"\n")
firstline = lines[starting_line(lines) + 1] expected = b"Goin' Fast @ http://127.0.0.127:9999"
assert exitcode != 1 assert exitcode != 1
assert firstline == b"Goin' Fast @ http://127.0.0.127:9999" assert expected in lines, f"Lines found: {lines}\nErr output: {err}"
@pytest.mark.parametrize( @pytest.mark.parametrize(
@@ -152,10 +196,10 @@ def test_host_port_ipv6_any(cmd):
command = ["sanic", "fake.server.app", *cmd] command = ["sanic", "fake.server.app", *cmd]
out, err, exitcode = capture(command) out, err, exitcode = capture(command)
lines = out.split(b"\n") lines = out.split(b"\n")
firstline = lines[starting_line(lines) + 1] expected = b"Goin' Fast @ http://[::]:9999"
assert exitcode != 1 assert exitcode != 1
assert firstline == b"Goin' Fast @ http://[::]:9999" assert expected in lines, f"Lines found: {lines}\nErr output: {err}"
@pytest.mark.parametrize( @pytest.mark.parametrize(
@@ -169,10 +213,10 @@ def test_host_port_ipv6_loopback(cmd):
command = ["sanic", "fake.server.app", *cmd] command = ["sanic", "fake.server.app", *cmd]
out, err, exitcode = capture(command) out, err, exitcode = capture(command)
lines = out.split(b"\n") lines = out.split(b"\n")
firstline = lines[starting_line(lines) + 1] expected = b"Goin' Fast @ http://[::1]:9999"
assert exitcode != 1 assert exitcode != 1
assert firstline == b"Goin' Fast @ http://[::1]:9999" assert expected in lines, f"Lines found: {lines}\nErr output: {err}"
@pytest.mark.parametrize( @pytest.mark.parametrize(
@@ -191,24 +235,40 @@ def test_num_workers(num, cmd):
out, err, exitcode = capture(command) out, err, exitcode = capture(command)
lines = out.split(b"\n") lines = out.split(b"\n")
worker_lines = [ if num == 1:
line expected = b"mode: production, single worker"
for line in lines else:
if b"Starting worker" in line or b"Stopping worker" in line expected = (f"mode: production, w/ {num} workers").encode()
]
assert exitcode != 1 assert exitcode != 1
assert len(worker_lines) == num * 2, f"Lines found: {lines}" assert expected in lines, f"Expected {expected}\nLines found: {lines}"
@pytest.mark.parametrize("cmd", ("--debug", "-d")) @pytest.mark.parametrize("cmd", ("--debug",))
def test_debug(cmd): def test_debug(cmd):
command = ["sanic", "fake.server.app", cmd] command = ["sanic", "fake.server.app", cmd]
out, err, exitcode = capture(command) out, err, exitcode = capture(command)
lines = out.split(b"\n") lines = out.split(b"\n")
info = read_app_info(lines) info = read_app_info(lines)
assert info["debug"] is True assert info["debug"] is True, f"Lines found: {lines}\nErr output: {err}"
assert info["auto_reload"] is True assert (
info["auto_reload"] is False
), f"Lines found: {lines}\nErr output: {err}"
assert "dev" not in info, f"Lines found: {lines}\nErr output: {err}"
@pytest.mark.parametrize("cmd", ("--dev", "-d"))
def test_dev(cmd):
command = ["sanic", "fake.server.app", cmd]
out, err, exitcode = capture(command)
lines = out.split(b"\n")
info = read_app_info(lines)
assert info["debug"] is True, f"Lines found: {lines}\nErr output: {err}"
assert (
info["auto_reload"] is True
), f"Lines found: {lines}\nErr output: {err}"
@pytest.mark.parametrize("cmd", ("--auto-reload", "-r")) @pytest.mark.parametrize("cmd", ("--auto-reload", "-r"))
@@ -218,8 +278,11 @@ def test_auto_reload(cmd):
lines = out.split(b"\n") lines = out.split(b"\n")
info = read_app_info(lines) info = read_app_info(lines)
assert info["debug"] is False assert info["debug"] is False, f"Lines found: {lines}\nErr output: {err}"
assert info["auto_reload"] is True assert (
info["auto_reload"] is True
), f"Lines found: {lines}\nErr output: {err}"
assert "dev" not in info, f"Lines found: {lines}\nErr output: {err}"
@pytest.mark.parametrize( @pytest.mark.parametrize(
@@ -231,7 +294,9 @@ def test_access_logs(cmd, expected):
lines = out.split(b"\n") lines = out.split(b"\n")
info = read_app_info(lines) info = read_app_info(lines)
assert info["access_log"] is expected assert (
info["access_log"] is expected
), f"Lines found: {lines}\nErr output: {err}"
@pytest.mark.parametrize("cmd", ("--version", "-v")) @pytest.mark.parametrize("cmd", ("--version", "-v"))
@@ -256,4 +321,6 @@ def test_noisy_exceptions(cmd, expected):
lines = out.split(b"\n") lines = out.split(b"\n")
info = read_app_info(lines) info = read_app_info(lines)
assert info["noisy_exceptions"] is expected assert (
info["noisy_exceptions"] is expected
), f"Lines found: {lines}\nErr output: {err}"

View File

@@ -9,6 +9,8 @@ from unittest.mock import Mock, call
import pytest import pytest
from pytest import MonkeyPatch
from sanic import Sanic from sanic import Sanic
from sanic.config import DEFAULT_CONFIG, Config from sanic.config import DEFAULT_CONFIG, Config
from sanic.exceptions import PyFileError from sanic.exceptions import PyFileError
@@ -39,21 +41,21 @@ class UltimateAnswer:
self.answer = int(answer) self.answer = int(answer)
def test_load_from_object(app): def test_load_from_object(app: Sanic):
app.config.load(ConfigTest) app.config.load(ConfigTest)
assert "CONFIG_VALUE" in app.config assert "CONFIG_VALUE" in app.config
assert app.config.CONFIG_VALUE == "should be used" assert app.config.CONFIG_VALUE == "should be used"
assert "not_for_config" not in app.config assert "not_for_config" not in app.config
def test_load_from_object_string(app): def test_load_from_object_string(app: Sanic):
app.config.load("test_config.ConfigTest") app.config.load("test_config.ConfigTest")
assert "CONFIG_VALUE" in app.config assert "CONFIG_VALUE" in app.config
assert app.config.CONFIG_VALUE == "should be used" assert app.config.CONFIG_VALUE == "should be used"
assert "not_for_config" not in app.config assert "not_for_config" not in app.config
def test_load_from_instance(app): def test_load_from_instance(app: Sanic):
app.config.load(ConfigTest()) app.config.load(ConfigTest())
assert "CONFIG_VALUE" in app.config assert "CONFIG_VALUE" in app.config
assert app.config.CONFIG_VALUE == "should be used" assert app.config.CONFIG_VALUE == "should be used"
@@ -62,7 +64,7 @@ def test_load_from_instance(app):
assert "another_not_for_config" not in app.config assert "another_not_for_config" not in app.config
def test_load_from_object_string_exception(app): def test_load_from_object_string_exception(app: Sanic):
with pytest.raises(ImportError): with pytest.raises(ImportError):
app.config.load("test_config.Config.test") app.config.load("test_config.Config.test")
@@ -120,6 +122,18 @@ def test_env_w_custom_converter():
del environ["SANIC_TEST_ANSWER"] del environ["SANIC_TEST_ANSWER"]
def test_env_lowercase():
with pytest.warns(None) as record:
environ["SANIC_test_answer"] = "42"
app = Sanic(name=__name__)
assert app.config.test_answer == 42
assert str(record[0].message) == (
"[DEPRECATION v22.9] Lowercase environment variables will not be "
"loaded into Sanic config beginning in v22.9."
)
del environ["SANIC_test_answer"]
def test_add_converter_multiple_times(caplog): def test_add_converter_multiple_times(caplog):
def converter(): def converter():
... ...
@@ -136,7 +150,7 @@ def test_add_converter_multiple_times(caplog):
assert len(config._converters) == 5 assert len(config._converters) == 5
def test_load_from_file(app): def test_load_from_file(app: Sanic):
config = dedent( config = dedent(
""" """
VALUE = 'some value' VALUE = 'some value'
@@ -155,12 +169,12 @@ def test_load_from_file(app):
assert "condition" not in app.config assert "condition" not in app.config
def test_load_from_missing_file(app): def test_load_from_missing_file(app: Sanic):
with pytest.raises(IOError): with pytest.raises(IOError):
app.config.load("non-existent file") app.config.load("non-existent file")
def test_load_from_envvar(app): def test_load_from_envvar(app: Sanic):
config = "VALUE = 'some value'" config = "VALUE = 'some value'"
with temp_path() as config_path: with temp_path() as config_path:
config_path.write_text(config) config_path.write_text(config)
@@ -170,7 +184,7 @@ def test_load_from_envvar(app):
assert app.config.VALUE == "some value" assert app.config.VALUE == "some value"
def test_load_from_missing_envvar(app): def test_load_from_missing_envvar(app: Sanic):
with pytest.raises(IOError) as e: with pytest.raises(IOError) as e:
app.config.load("non-existent variable") app.config.load("non-existent variable")
assert str(e.value) == ( assert str(e.value) == (
@@ -180,7 +194,7 @@ def test_load_from_missing_envvar(app):
) )
def test_load_config_from_file_invalid_syntax(app): def test_load_config_from_file_invalid_syntax(app: Sanic):
config = "VALUE = some value" config = "VALUE = some value"
with temp_path() as config_path: with temp_path() as config_path:
config_path.write_text(config) config_path.write_text(config)
@@ -189,7 +203,7 @@ def test_load_config_from_file_invalid_syntax(app):
app.config.load(config_path) app.config.load(config_path)
def test_overwrite_exisiting_config(app): def test_overwrite_exisiting_config(app: Sanic):
app.config.DEFAULT = 1 app.config.DEFAULT = 1
class Config: class Config:
@@ -199,7 +213,7 @@ def test_overwrite_exisiting_config(app):
assert app.config.DEFAULT == 2 assert app.config.DEFAULT == 2
def test_overwrite_exisiting_config_ignore_lowercase(app): def test_overwrite_exisiting_config_ignore_lowercase(app: Sanic):
app.config.default = 1 app.config.default = 1
class Config: class Config:
@@ -209,7 +223,7 @@ def test_overwrite_exisiting_config_ignore_lowercase(app):
assert app.config.default == 1 assert app.config.default == 1
def test_missing_config(app): def test_missing_config(app: Sanic):
with pytest.raises(AttributeError, match="Config has no 'NON_EXISTENT'"): with pytest.raises(AttributeError, match="Config has no 'NON_EXISTENT'"):
_ = app.config.NON_EXISTENT _ = app.config.NON_EXISTENT
@@ -277,7 +291,7 @@ def test_config_custom_defaults_with_env():
del environ[key] del environ[key]
def test_config_access_log_passing_in_run(app): def test_config_access_log_passing_in_run(app: Sanic):
assert app.config.ACCESS_LOG is True assert app.config.ACCESS_LOG is True
@app.listener("after_server_start") @app.listener("after_server_start")
@@ -287,12 +301,15 @@ def test_config_access_log_passing_in_run(app):
app.run(port=1340, access_log=False) app.run(port=1340, access_log=False)
assert app.config.ACCESS_LOG is False assert app.config.ACCESS_LOG is False
app.router.reset()
app.signal_router.reset()
app.run(port=1340, access_log=True) app.run(port=1340, access_log=True)
assert app.config.ACCESS_LOG is True assert app.config.ACCESS_LOG is True
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_config_access_log_passing_in_create_server(app): async def test_config_access_log_passing_in_create_server(app: Sanic):
assert app.config.ACCESS_LOG is True assert app.config.ACCESS_LOG is True
@app.listener("after_server_start") @app.listener("after_server_start")
@@ -341,18 +358,18 @@ _test_setting_as_module = str(
], ],
ids=["from_dict", "from_class", "from_file"], ids=["from_dict", "from_class", "from_file"],
) )
def test_update(app, conf_object): def test_update(app: Sanic, conf_object):
app.update_config(conf_object) app.update_config(conf_object)
assert app.config["TEST_SETTING_VALUE"] == 1 assert app.config["TEST_SETTING_VALUE"] == 1
def test_update_from_lowercase_key(app): def test_update_from_lowercase_key(app: Sanic):
d = {"test_setting_value": 1} d = {"test_setting_value": 1}
app.update_config(d) app.update_config(d)
assert "test_setting_value" not in app.config assert "test_setting_value" not in app.config
def test_deprecation_notice_when_setting_logo(app): def test_deprecation_notice_when_setting_logo(app: Sanic):
message = ( message = (
"Setting the config.LOGO is deprecated and will no longer be " "Setting the config.LOGO is deprecated and will no longer be "
"supported starting in v22.6." "supported starting in v22.6."
@@ -361,7 +378,7 @@ def test_deprecation_notice_when_setting_logo(app):
app.config.LOGO = "My Custom Logo" app.config.LOGO = "My Custom Logo"
def test_config_set_methods(app, monkeypatch): def test_config_set_methods(app: Sanic, monkeypatch: MonkeyPatch):
post_set = Mock() post_set = Mock()
monkeypatch.setattr(Config, "_post_set", post_set) monkeypatch.setattr(Config, "_post_set", post_set)
@@ -406,3 +423,15 @@ def test_config_set_methods(app, monkeypatch):
app.config.update_config({"FOO": 10}) app.config.update_config({"FOO": 10})
post_set.assert_called_once_with("FOO", 10) post_set.assert_called_once_with("FOO", 10)
def test_negative_proxy_count(app: Sanic):
app.config.PROXIES_COUNT = -1
message = (
"PROXIES_COUNT cannot be negative. "
"https://sanic.readthedocs.io/en/latest/sanic/config.html"
"#proxy-configuration"
)
with pytest.raises(ValueError, match=message):
app.prepare()

View File

@@ -67,7 +67,7 @@ def test_auto_fallback_with_data(app):
_, response = app.test_client.get("/error") _, response = app.test_client.get("/error")
assert response.status == 500 assert response.status == 500
assert response.content_type == "text/html; charset=utf-8" assert response.content_type == "text/plain; charset=utf-8"
_, response = app.test_client.post("/error", json={"foo": "bar"}) _, response = app.test_client.post("/error", json={"foo": "bar"})
assert response.status == 500 assert response.status == 500
@@ -75,7 +75,7 @@ def test_auto_fallback_with_data(app):
_, response = app.test_client.post("/error", data={"foo": "bar"}) _, response = app.test_client.post("/error", data={"foo": "bar"})
assert response.status == 500 assert response.status == 500
assert response.content_type == "text/html; charset=utf-8" assert response.content_type == "text/plain; charset=utf-8"
def test_auto_fallback_with_content_type(app): def test_auto_fallback_with_content_type(app):
@@ -91,7 +91,7 @@ def test_auto_fallback_with_content_type(app):
"/error", headers={"content-type": "foo/bar", "accept": "*/*"} "/error", headers={"content-type": "foo/bar", "accept": "*/*"}
) )
assert response.status == 500 assert response.status == 500
assert response.content_type == "text/html; charset=utf-8" assert response.content_type == "text/plain; charset=utf-8"
def test_route_error_format_set_on_auto(app): def test_route_error_format_set_on_auto(app):
@@ -174,6 +174,17 @@ def test_route_error_format_unknown(app):
... ...
def test_fallback_with_content_type_html(app):
app.config.FALLBACK_ERROR_FORMAT = "auto"
_, response = app.test_client.get(
"/error",
headers={"content-type": "application/json", "accept": "text/html"},
)
assert response.status == 500
assert response.content_type == "text/html; charset=utf-8"
def test_fallback_with_content_type_mismatch_accept(app): def test_fallback_with_content_type_mismatch_accept(app):
app.config.FALLBACK_ERROR_FORMAT = "auto" app.config.FALLBACK_ERROR_FORMAT = "auto"
@@ -186,10 +197,10 @@ def test_fallback_with_content_type_mismatch_accept(app):
_, response = app.test_client.get( _, response = app.test_client.get(
"/error", "/error",
headers={"content-type": "text/plain", "accept": "foo/bar"}, headers={"content-type": "text/html", "accept": "foo/bar"},
) )
assert response.status == 500 assert response.status == 500
assert response.content_type == "text/html; charset=utf-8" assert response.content_type == "text/plain; charset=utf-8"
app.router.reset() app.router.reset()
@@ -208,7 +219,7 @@ def test_fallback_with_content_type_mismatch_accept(app):
headers={"accept": "foo/bar"}, headers={"accept": "foo/bar"},
) )
assert response.status == 500 assert response.status == 500
assert response.content_type == "text/html; charset=utf-8" assert response.content_type == "text/plain; charset=utf-8"
_, response = app.test_client.get( _, response = app.test_client.get(
"/alt1", "/alt1",
headers={"accept": "foo/bar,*/*"}, headers={"accept": "foo/bar,*/*"},
@@ -221,7 +232,7 @@ def test_fallback_with_content_type_mismatch_accept(app):
headers={"accept": "foo/bar"}, headers={"accept": "foo/bar"},
) )
assert response.status == 500 assert response.status == 500
assert response.content_type == "text/html; charset=utf-8" assert response.content_type == "text/plain; charset=utf-8"
_, response = app.test_client.get( _, response = app.test_client.get(
"/alt2", "/alt2",
headers={"accept": "foo/bar,*/*"}, headers={"accept": "foo/bar,*/*"},
@@ -234,6 +245,13 @@ def test_fallback_with_content_type_mismatch_accept(app):
headers={"accept": "foo/bar"}, headers={"accept": "foo/bar"},
) )
assert response.status == 500 assert response.status == 500
assert response.content_type == "text/plain; charset=utf-8"
_, response = app.test_client.get(
"/alt3",
headers={"accept": "foo/bar,text/html"},
)
assert response.status == 500
assert response.content_type == "text/html; charset=utf-8" assert response.content_type == "text/html; charset=utf-8"
@@ -288,6 +306,10 @@ def test_allow_fallback_error_format_set_main_process_start(app):
def test_setting_fallback_on_config_changes_as_expected(app): def test_setting_fallback_on_config_changes_as_expected(app):
app.error_handler = ErrorHandler() app.error_handler = ErrorHandler()
_, response = app.test_client.get("/error")
assert response.content_type == "text/plain; charset=utf-8"
app.config.FALLBACK_ERROR_FORMAT = "html"
_, response = app.test_client.get("/error") _, response = app.test_client.get("/error")
assert response.content_type == "text/html; charset=utf-8" assert response.content_type == "text/html; charset=utf-8"

View File

@@ -1,5 +1,4 @@
import logging import logging
import warnings
import pytest import pytest
@@ -34,6 +33,7 @@ class SanicExceptionTestException(Exception):
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def exception_app(): def exception_app():
app = Sanic("test_exceptions") app = Sanic("test_exceptions")
app.config.FALLBACK_ERROR_FORMAT = "html"
@app.route("/") @app.route("/")
def handler(request): def handler(request):

View File

@@ -216,31 +216,6 @@ def test_exception_handler_processed_request_middleware(
assert response.text == "Done." assert response.text == "Done."
def test_single_arg_exception_handler_notice(
exception_handler_app: Sanic, caplog: LogCaptureFixture
):
class CustomErrorHandler(ErrorHandler):
def lookup(self, exception):
return super().lookup(exception, None)
exception_handler_app.error_handler = CustomErrorHandler()
message = (
"[DEPRECATION v22.3] You are using a deprecated error handler. The "
"lookup method should accept two positional parameters: (exception, "
"route_name: Optional[str]). Until you upgrade your "
"ErrorHandler.lookup, Blueprint specific exceptions will not work "
"properly. Beginning in v22.3, the legacy style lookup method will "
"not work at all."
)
with pytest.warns(DeprecationWarning) as record:
_, response = exception_handler_app.test_client.get("/1")
assert len(record) == 1
assert record[0].message.args[0] == message
assert response.status == 400
def test_error_handler_noisy_log( def test_error_handler_noisy_log(
exception_handler_app: Sanic, monkeypatch: MonkeyPatch exception_handler_app: Sanic, monkeypatch: MonkeyPatch
): ):
@@ -279,7 +254,7 @@ def test_exception_handler_response_was_sent(
@app.route("/2") @app.route("/2")
async def handler2(request: Request): async def handler2(request: Request):
response = await request.respond() await request.respond()
raise ServerError("Exception") raise ServerError("Exception")
with caplog.at_level(logging.WARNING): with caplog.at_level(logging.WARNING):

View File

@@ -164,11 +164,12 @@ def test_raw_headers(app):
}, },
) )
assert request.raw_headers == ( assert b"Host: example.com" in request.raw_headers
b"Host: example.com\r\nAccept: */*\r\nAccept-Encoding: gzip, " assert b"Accept: */*" in request.raw_headers
b"deflate\r\nConnection: keep-alive\r\nUser-Agent: " assert b"Accept-Encoding: gzip, deflate" in request.raw_headers
b"Sanic-Testing\r\nFOO: bar" assert b"Connection: keep-alive" in request.raw_headers
) assert b"User-Agent: Sanic-Testing" in request.raw_headers
assert b"FOO: bar" in request.raw_headers
def test_request_line(app): def test_request_line(app):

View File

@@ -1,11 +1,15 @@
import logging import logging
import os
import platform import platform
import sys
from unittest.mock import Mock from unittest.mock import Mock
from sanic import __version__ import pytest
from sanic import Sanic, __version__
from sanic.application.logo import BASE_LOGO from sanic.application.logo import BASE_LOGO
from sanic.application.motd import MOTDTTY from sanic.application.motd import MOTD, MOTDTTY
def test_logo_base(app, run_startup): def test_logo_base(app, run_startup):
@@ -83,3 +87,25 @@ def test_motd_display(caplog):
└───────────────────────┴────────┘ └───────────────────────┴────────┘
""" """
) )
@pytest.mark.skipif(sys.version_info < (3, 8), reason="Not on 3.7")
def test_reload_dirs(app):
app.config.LOGO = None
app.config.AUTO_RELOAD = True
app.prepare(reload_dir="./", auto_reload=True, motd_display={"foo": "bar"})
existing = MOTD.output
MOTD.output = Mock()
app.motd("foo")
MOTD.output.assert_called_once()
assert (
MOTD.output.call_args.args[2]["auto-reload"]
== f"enabled, {os.getcwd()}"
)
assert MOTD.output.call_args.args[3] == {"foo": "bar"}
MOTD.output = existing
Sanic._app_registry = {}

207
tests/test_multi_serve.py Normal file
View File

@@ -0,0 +1,207 @@
import logging
from unittest.mock import Mock
import pytest
from sanic import Sanic
from sanic.response import text
from sanic.server.async_server import AsyncioServer
from sanic.signals import Event
from sanic.touchup.schemes.ode import OptionalDispatchEvent
try:
from unittest.mock import AsyncMock
except ImportError:
from asyncmock import AsyncMock # type: ignore
@pytest.fixture
def app_one():
app = Sanic("One")
@app.get("/one")
async def one(request):
return text("one")
return app
@pytest.fixture
def app_two():
app = Sanic("Two")
@app.get("/two")
async def two(request):
return text("two")
return app
@pytest.fixture(autouse=True)
def clean():
Sanic._app_registry = {}
yield
def test_serve_same_app_multiple_tuples(app_one, run_multi):
app_one.prepare(port=23456)
app_one.prepare(port=23457)
logs = run_multi(app_one)
assert (
"sanic.root",
logging.INFO,
"Goin' Fast @ http://127.0.0.1:23456",
) in logs
assert (
"sanic.root",
logging.INFO,
"Goin' Fast @ http://127.0.0.1:23457",
) in logs
def test_serve_multiple_apps(app_one, app_two, run_multi):
app_one.prepare(port=23456)
app_two.prepare(port=23457)
logs = run_multi(app_one)
assert (
"sanic.root",
logging.INFO,
"Goin' Fast @ http://127.0.0.1:23456",
) in logs
assert (
"sanic.root",
logging.INFO,
"Goin' Fast @ http://127.0.0.1:23457",
) in logs
def test_listeners_on_secondary_app(app_one, app_two, run_multi):
app_one.prepare(port=23456)
app_two.prepare(port=23457)
before_start = AsyncMock()
after_start = AsyncMock()
before_stop = AsyncMock()
after_stop = AsyncMock()
app_two.before_server_start(before_start)
app_two.after_server_start(after_start)
app_two.before_server_stop(before_stop)
app_two.after_server_stop(after_stop)
run_multi(app_one)
before_start.assert_awaited_once()
after_start.assert_awaited_once()
before_stop.assert_awaited_once()
after_stop.assert_awaited_once()
@pytest.mark.parametrize(
"events",
(
(Event.HTTP_LIFECYCLE_BEGIN,),
(Event.HTTP_LIFECYCLE_BEGIN, Event.HTTP_LIFECYCLE_COMPLETE),
(
Event.HTTP_LIFECYCLE_BEGIN,
Event.HTTP_LIFECYCLE_COMPLETE,
Event.HTTP_LIFECYCLE_REQUEST,
),
),
)
def test_signal_synchronization(app_one, app_two, run_multi, events):
app_one.prepare(port=23456)
app_two.prepare(port=23457)
for event in events:
app_one.signal(event)(AsyncMock())
run_multi(app_one)
assert len(app_two.signal_router.routes) == len(events) + 1
signal_handlers = {
signal.handler
for signal in app_two.signal_router.routes
if signal.name.startswith("http")
}
assert len(signal_handlers) == 1
assert list(signal_handlers)[0] is OptionalDispatchEvent.noop
def test_warning_main_process_listeners_on_secondary(
app_one, app_two, run_multi
):
app_two.main_process_start(AsyncMock())
app_two.main_process_stop(AsyncMock())
app_one.prepare(port=23456)
app_two.prepare(port=23457)
log = run_multi(app_one)
message = (
f"Sanic found 2 listener(s) on "
"secondary applications attached to the main "
"process. These will be ignored since main "
"process listeners can only be attached to your "
"primary application: "
f"{repr(app_one)}"
)
assert ("sanic.error", logging.WARNING, message) in log
def test_no_applications():
Sanic._app_registry = {}
message = "Did not find any applications."
with pytest.raises(RuntimeError, match=message):
Sanic.serve()
def test_oserror_warning(app_one, app_two, run_multi, capfd):
orig = AsyncioServer.__await__
AsyncioServer.__await__ = Mock(side_effect=OSError("foo"))
app_one.prepare(port=23456, workers=2)
app_two.prepare(port=23457, workers=2)
run_multi(app_one)
captured = capfd.readouterr()
assert (
"An OSError was detected on startup. The encountered error was: foo"
) in captured.err
AsyncioServer.__await__ = orig
def test_running_multiple_offset_warning(app_one, app_two, run_multi, capfd):
app_one.prepare(port=23456, workers=2)
app_two.prepare(port=23457)
run_multi(app_one)
captured = capfd.readouterr()
assert (
f"The primary application {repr(app_one)} is running "
"with 2 worker(s). All "
"application instances will run with the same number. "
f"You requested {repr(app_two)} to run with "
"1 worker(s), which will be ignored "
"in favor of the primary application."
) in captured.err
def test_running_multiple_secondary(app_one, app_two, run_multi, capfd):
app_one.prepare(port=23456, workers=2)
app_two.prepare(port=23457)
before_start = AsyncMock()
app_two.before_server_start(before_start)
run_multi(app_one)
before_start.await_count == 2

View File

@@ -132,11 +132,11 @@ def test_main_process_event(app, caplog):
logger.info("main_process_stop") logger.info("main_process_stop")
@app.main_process_start @app.main_process_start
def main_process_start(app, loop): def main_process_start2(app, loop):
logger.info("main_process_start") logger.info("main_process_start")
@app.main_process_stop @app.main_process_stop
def main_process_stop(app, loop): def main_process_stop2(app, loop):
logger.info("main_process_stop") logger.info("main_process_stop")
with caplog.at_level(logging.INFO): with caplog.at_level(logging.INFO):

71
tests/test_prepare.py Normal file
View File

@@ -0,0 +1,71 @@
import logging
import os
from pathlib import Path
from unittest.mock import Mock
import pytest
from sanic import Sanic
from sanic.application.state import ApplicationServerInfo
@pytest.fixture(autouse=True)
def no_skip():
should_auto_reload = Sanic.should_auto_reload
Sanic.should_auto_reload = Mock(return_value=False)
yield
Sanic._app_registry = {}
Sanic.should_auto_reload = should_auto_reload
def get_primary(app: Sanic) -> ApplicationServerInfo:
return app.state.server_info[0]
def test_dev(app: Sanic):
app.prepare(dev=True)
assert app.state.is_debug
assert app.state.auto_reload
def test_motd_display(app: Sanic):
app.prepare(motd_display={"foo": "bar"})
assert app.config.MOTD_DISPLAY["foo"] == "bar"
del app.config.MOTD_DISPLAY["foo"]
@pytest.mark.parametrize("dirs", ("./foo", ("./foo", "./bar")))
def test_reload_dir(app: Sanic, dirs, caplog):
messages = []
with caplog.at_level(logging.WARNING):
app.prepare(reload_dir=dirs)
if isinstance(dirs, str):
dirs = (dirs,)
for d in dirs:
assert Path(d) in app.state.reload_dirs
messages.append(
f"Directory {d} could not be located",
)
for message in messages:
assert ("sanic.root", logging.WARNING, message) in caplog.record_tuples
def test_fast(app: Sanic, run_multi):
app.prepare(fast=True)
try:
workers = len(os.sched_getaffinity(0))
except AttributeError:
workers = os.cpu_count() or 1
assert app.state.fast
assert app.state.workers == workers
logs = run_multi(app, logging.INFO)
messages = [m[2] for m in logs]
assert f"mode: production, goin' fast w/ {workers} workers" in messages

View File

@@ -58,6 +58,36 @@ def write_app(filename, **runargs):
return text return text
def write_listener_app(filename, **runargs):
start_text = secrets.token_urlsafe()
stop_text = secrets.token_urlsafe()
with open(filename, "w") as f:
f.write(
dedent(
f"""\
import os
from sanic import Sanic
app = Sanic(__name__)
app.route("/")(lambda x: x)
@app.reload_process_start
async def reload_start(*_):
print("reload_start", os.getpid(), {start_text!r})
@app.reload_process_stop
async def reload_stop(*_):
print("reload_stop", os.getpid(), {stop_text!r})
if __name__ == "__main__":
app.run(**{runargs!r})
"""
)
)
return start_text, stop_text
def write_json_config_app(filename, jsonfile, **runargs): def write_json_config_app(filename, jsonfile, **runargs):
with open(filename, "w") as f: with open(filename, "w") as f:
f.write( f.write(
@@ -92,10 +122,10 @@ def write_file(filename):
return text return text
def scanner(proc): def scanner(proc, trigger="complete"):
for line in proc.stdout: for line in proc.stdout:
line = line.decode().strip() line = line.decode().strip()
if line.startswith("complete"): if line.startswith(trigger):
yield line yield line
@@ -108,7 +138,7 @@ argv = dict(
"sanic", "sanic",
"--port", "--port",
"42204", "42204",
"--debug", "--auto-reload",
"reloader.app", "reloader.app",
], ],
) )
@@ -118,7 +148,7 @@ argv = dict(
"runargs, mode", "runargs, mode",
[ [
(dict(port=42202, auto_reload=True), "script"), (dict(port=42202, auto_reload=True), "script"),
(dict(port=42203, debug=True), "module"), (dict(port=42203, auto_reload=True), "module"),
({}, "sanic"), ({}, "sanic"),
], ],
) )
@@ -151,7 +181,7 @@ async def test_reloader_live(runargs, mode):
"runargs, mode", "runargs, mode",
[ [
(dict(port=42302, auto_reload=True), "script"), (dict(port=42302, auto_reload=True), "script"),
(dict(port=42303, debug=True), "module"), (dict(port=42303, auto_reload=True), "module"),
({}, "sanic"), ({}, "sanic"),
], ],
) )
@@ -183,3 +213,30 @@ async def test_reloader_live_with_dir(runargs, mode):
terminate(proc) terminate(proc)
with suppress(TimeoutExpired): with suppress(TimeoutExpired):
proc.wait(timeout=3) proc.wait(timeout=3)
def test_reload_listeners():
with TemporaryDirectory() as tmpdir:
filename = os.path.join(tmpdir, "reloader.py")
start_text, stop_text = write_listener_app(
filename, port=42305, auto_reload=True
)
proc = Popen(
argv["script"], cwd=tmpdir, stdout=PIPE, creationflags=flags
)
try:
timeout = Timer(TIMER_DELAY, terminate, [proc])
timeout.start()
# Python apparently keeps using the old source sometimes if
# we don't sleep before rewrite (pycache timestamp problem?)
sleep(1)
line = scanner(proc, "reload_start")
assert start_text in next(line)
line = scanner(proc, "reload_stop")
assert stop_text in next(line)
finally:
timeout.cancel()
terminate(proc)
with suppress(TimeoutExpired):
proc.wait(timeout=3)

View File

@@ -4,6 +4,7 @@ from uuid import UUID, uuid4
import pytest import pytest
from sanic import Sanic, response from sanic import Sanic, response
from sanic.exceptions import BadURL
from sanic.request import Request, uuid from sanic.request import Request, uuid
from sanic.server import HttpProtocol from sanic.server import HttpProtocol
@@ -176,3 +177,17 @@ def test_request_accept():
"text/x-dvi; q=0.8", "text/x-dvi; q=0.8",
"text/plain; q=0.5", "text/plain; q=0.5",
] ]
def test_bad_url_parse():
message = "Bad URL: my.redacted-domain.com:443"
with pytest.raises(BadURL, match=message):
Request(
b"my.redacted-domain.com:443",
Mock(),
Mock(),
Mock(),
Mock(),
Mock(),
Mock(),
)

View File

@@ -1,3 +1,4 @@
import base64
import logging import logging
from json import dumps as json_dumps from json import dumps as json_dumps
@@ -15,11 +16,15 @@ from sanic_testing.testing import (
) )
from sanic import Blueprint, Sanic from sanic import Blueprint, Sanic
from sanic.exceptions import SanicException, ServerError from sanic.exceptions import ServerError
from sanic.request import DEFAULT_HTTP_CONTENT_TYPE, Request, RequestParameters from sanic.request import DEFAULT_HTTP_CONTENT_TYPE, RequestParameters
from sanic.response import html, json, text from sanic.response import html, json, text
def encode_basic_auth_credentials(username, password):
return base64.b64encode(f"{username}:{password}".encode()).decode("ascii")
# ------------------------------------------------------------ # # ------------------------------------------------------------ #
# GET # GET
# ------------------------------------------------------------ # # ------------------------------------------------------------ #
@@ -362,93 +367,95 @@ async def test_uri_template_asgi(app):
assert request.uri_template == "/foo/<id:int>/bar/<name:[A-z]+>" assert request.uri_template == "/foo/<id:int>/bar/<name:[A-z]+>"
def test_token(app): @pytest.mark.parametrize(
("auth_type", "token"),
[
# uuid4 generated token set in "Authorization" header
(None, "a1d895e0-553a-421a-8e22-5ff8ecb48cbf"),
# uuid4 generated token with API Token authorization
("Token", "a1d895e0-553a-421a-8e22-5ff8ecb48cbf"),
# uuid4 generated token with Bearer Token authorization
("Bearer", "a1d895e0-553a-421a-8e22-5ff8ecb48cbf"),
# no Authorization header
(None, None),
],
)
def test_token(app, auth_type, token):
@app.route("/") @app.route("/")
async def handler(request): async def handler(request):
return text("OK") return text("OK")
# uuid4 generated token. if token:
token = "a1d895e0-553a-421a-8e22-5ff8ecb48cbf" headers = {
headers = { "content-type": "application/json",
"content-type": "application/json", "Authorization": f"{auth_type} {token}"
"Authorization": f"{token}", if auth_type
} else f"{token}",
}
else:
headers = {"content-type": "application/json"}
request, response = app.test_client.get("/", headers=headers) request, response = app.test_client.get("/", headers=headers)
assert request.token == token assert request.token == token
token = "a1d895e0-553a-421a-8e22-5ff8ecb48cbf"
headers = {
"content-type": "application/json",
"Authorization": f"Token {token}",
}
request, response = app.test_client.get("/", headers=headers) @pytest.mark.parametrize(
("auth_type", "token", "username", "password"),
assert request.token == token [
# uuid4 generated token set in "Authorization" header
token = "a1d895e0-553a-421a-8e22-5ff8ecb48cbf" (None, "a1d895e0-553a-421a-8e22-5ff8ecb48cbf", None, None),
headers = { # uuid4 generated token with API Token authorization
"content-type": "application/json", ("Token", "a1d895e0-553a-421a-8e22-5ff8ecb48cbf", None, None),
"Authorization": f"Bearer {token}", # uuid4 generated token with Bearer Token authorization
} ("Bearer", "a1d895e0-553a-421a-8e22-5ff8ecb48cbf", None, None),
# username and password with Basic Auth authorization
request, response = app.test_client.get("/", headers=headers) (
"Basic",
assert request.token == token encode_basic_auth_credentials("some_username", "some_pass"),
"some_username",
# no Authorization headers "some_pass",
headers = {"content-type": "application/json"} ),
# no Authorization header
request, response = app.test_client.get("/", headers=headers) (None, None, None, None),
],
assert request.token is None )
def test_credentials(app, capfd, auth_type, token, username, password):
@pytest.mark.asyncio
async def test_token_asgi(app):
@app.route("/") @app.route("/")
async def handler(request): async def handler(request):
return text("OK") return text("OK")
# uuid4 generated token. if token:
token = "a1d895e0-553a-421a-8e22-5ff8ecb48cbf" headers = {
headers = { "content-type": "application/json",
"content-type": "application/json", "Authorization": f"{auth_type} {token}"
"Authorization": f"{token}", if auth_type
} else f"{token}",
}
else:
headers = {"content-type": "application/json"}
request, response = await app.asgi_client.get("/", headers=headers) request, response = app.test_client.get("/", headers=headers)
assert request.token == token if auth_type == "Basic":
assert request.credentials.username == username
assert request.credentials.password == password
else:
_, err = capfd.readouterr()
with pytest.raises(AttributeError):
request.credentials.password
assert "Password is available for Basic Auth only" in err
request.credentials.username
assert "Username is available for Basic Auth only" in err
token = "a1d895e0-553a-421a-8e22-5ff8ecb48cbf" if token:
headers = { assert request.credentials.token == token
"content-type": "application/json", assert request.credentials.auth_type == auth_type
"Authorization": f"Token {token}", else:
} assert request.credentials is None
assert not hasattr(request.credentials, "token")
request, response = await app.asgi_client.get("/", headers=headers) assert not hasattr(request.credentials, "auth_type")
assert not hasattr(request.credentials, "_username")
assert request.token == token assert not hasattr(request.credentials, "_password")
token = "a1d895e0-553a-421a-8e22-5ff8ecb48cbf"
headers = {
"content-type": "application/json",
"Authorization": f"Bearer {token}",
}
request, response = await app.asgi_client.get("/", headers=headers)
assert request.token == token
# no Authorization headers
headers = {"content-type": "application/json"}
request, response = await app.asgi_client.get("/", headers=headers)
assert request.token is None
def test_content_type(app): def test_content_type(app):
@@ -1714,7 +1721,6 @@ async def test_request_query_args_custom_parsing_asgi(app):
def test_request_cookies(app): def test_request_cookies(app):
cookies = {"test": "OK"} cookies = {"test": "OK"}
@app.get("/") @app.get("/")
@@ -1729,7 +1735,6 @@ def test_request_cookies(app):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_request_cookies_asgi(app): async def test_request_cookies_asgi(app):
cookies = {"test": "OK"} cookies = {"test": "OK"}
@app.get("/") @app.get("/")

View File

@@ -1,8 +1,6 @@
import asyncio import asyncio
import re import re
from unittest.mock import Mock
import pytest import pytest
from sanic_routing.exceptions import ( from sanic_routing.exceptions import (
@@ -256,7 +254,7 @@ def test_route_strict_slash(app):
def test_route_invalid_parameter_syntax(app): def test_route_invalid_parameter_syntax(app):
with pytest.raises(ValueError): with pytest.raises(InvalidUsage):
@app.get("/get/<:str>", strict_slashes=True) @app.get("/get/<:str>", strict_slashes=True)
def handler(request): def handler(request):

View File

@@ -33,9 +33,17 @@ def create_listener(listener_name, in_list):
return _listener return _listener
def create_listener_no_loop(listener_name, in_list):
async def _listener(app):
print(f"DEBUG MESSAGE FOR PYTEST for {listener_name}")
in_list.insert(0, app.name + listener_name)
return _listener
def start_stop_app(random_name_app, **run_kwargs): def start_stop_app(random_name_app, **run_kwargs):
def stop_on_alarm(signum, frame): def stop_on_alarm(signum, frame):
raise KeyboardInterrupt("SIGINT for sanic to stop gracefully") random_name_app.stop()
signal.signal(signal.SIGALRM, stop_on_alarm) signal.signal(signal.SIGALRM, stop_on_alarm)
signal.alarm(1) signal.alarm(1)
@@ -56,6 +64,17 @@ def test_single_listener(app, listener_name):
assert app.name + listener_name == output.pop() assert app.name + listener_name == output.pop()
@skipif_no_alarm
@pytest.mark.parametrize("listener_name", AVAILABLE_LISTENERS)
def test_single_listener_no_loop(app, listener_name):
"""Test that listeners on their own work"""
output = []
# Register listener
app.listener(listener_name)(create_listener_no_loop(listener_name, output))
start_stop_app(app)
assert app.name + listener_name == output.pop()
@skipif_no_alarm @skipif_no_alarm
@pytest.mark.parametrize("listener_name", AVAILABLE_LISTENERS) @pytest.mark.parametrize("listener_name", AVAILABLE_LISTENERS)
def test_register_listener(app, listener_name): def test_register_listener(app, listener_name):
@@ -130,6 +149,9 @@ async def test_trigger_before_events_create_server_missing_event(app):
def test_create_server_trigger_events(app): def test_create_server_trigger_events(app):
"""Test if create_server can trigger server events""" """Test if create_server can trigger server events"""
def stop_on_alarm(signum, frame):
raise KeyboardInterrupt("...")
flag1 = False flag1 = False
flag2 = False flag2 = False
flag3 = False flag3 = False
@@ -137,8 +159,7 @@ def test_create_server_trigger_events(app):
async def stop(app, loop): async def stop(app, loop):
nonlocal flag1 nonlocal flag1
flag1 = True flag1 = True
await asyncio.sleep(0.1) signal.alarm(1)
app.stop()
async def before_stop(app, loop): async def before_stop(app, loop):
nonlocal flag2 nonlocal flag2
@@ -155,6 +176,8 @@ def test_create_server_trigger_events(app):
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
# Use random port for tests # Use random port for tests
signal.signal(signal.SIGALRM, stop_on_alarm)
with closing(socket()) as sock: with closing(socket()) as sock:
sock.bind(("127.0.0.1", 0)) sock.bind(("127.0.0.1", 0))
@@ -195,3 +218,16 @@ async def test_missing_startup_raises_exception(app):
with pytest.raises(SanicException): with pytest.raises(SanicException):
await srv.before_start() await srv.before_start()
def test_reload_listeners_attached(app):
async def dummy(*_):
...
app.reload_process_start(dummy)
app.reload_process_stop(dummy)
app.listener("reload_process_start")(dummy)
app.listener("reload_process_stop")(dummy)
assert len(app.listeners.get("reload_process_start")) == 2
assert len(app.listeners.get("reload_process_stop")) == 2

View File

@@ -10,6 +10,7 @@ import pytest
from sanic_testing.testing import HOST, PORT from sanic_testing.testing import HOST, PORT
from sanic.compat import ctrlc_workaround_for_windows from sanic.compat import ctrlc_workaround_for_windows
from sanic.exceptions import InvalidUsage
from sanic.response import HTTPResponse from sanic.response import HTTPResponse
@@ -108,3 +109,17 @@ def test_windows_workaround():
assert res == "OK" assert res == "OK"
res = loop.run_until_complete(atest(True)) res = loop.run_until_complete(atest(True))
assert res == "OK" assert res == "OK"
@pytest.mark.skipif(os.name == "nt", reason="May hang CI on py38/windows")
def test_signals_with_invalid_invocation(app):
"""Test if sanic register fails with invalid invocation"""
@app.route("/hello")
async def hello_route(request):
return HTTPResponse()
with pytest.raises(
InvalidUsage, match="Invalid event registration: Missing event name"
):
app.listener(stop)

View File

@@ -1,7 +1,6 @@
import inspect import inspect
import logging import logging
import os import os
import sys
from collections import Counter from collections import Counter
from pathlib import Path from pathlib import Path
@@ -9,7 +8,7 @@ from time import gmtime, strftime
import pytest import pytest
from sanic import Sanic, text from sanic import text
from sanic.exceptions import FileNotFound from sanic.exceptions import FileNotFound
@@ -22,22 +21,6 @@ def static_file_directory():
return static_directory return static_directory
@pytest.fixture(scope="module")
def double_dotted_directory_file(static_file_directory: str):
"""Generate double dotted directory and its files"""
if sys.platform == "win32":
raise Exception("Windows doesn't support double dotted directories")
file_path = Path(static_file_directory) / "dotted.." / "dot.txt"
double_dotted_dir = file_path.parent
Path.mkdir(double_dotted_dir, exist_ok=True)
with open(file_path, "w") as f:
f.write("DOT\n")
yield file_path
Path.unlink(file_path)
Path.rmdir(double_dotted_dir)
def get_file_path(static_file_directory, file_name): def get_file_path(static_file_directory, file_name):
return os.path.join(static_file_directory, file_name) return os.path.join(static_file_directory, file_name)
@@ -595,43 +578,3 @@ def test_resource_type_dir(app, static_file_directory):
def test_resource_type_unknown(app, static_file_directory, caplog): def test_resource_type_unknown(app, static_file_directory, caplog):
with pytest.raises(ValueError): with pytest.raises(ValueError):
app.static("/static", static_file_directory, resource_type="unknown") app.static("/static", static_file_directory, resource_type="unknown")
@pytest.mark.skipif(
sys.platform == "win32",
reason="Windows does not support double dotted directories",
)
def test_dotted_dir_ok(
app: Sanic, static_file_directory: str, double_dotted_directory_file: Path
):
app.static("/foo", static_file_directory)
dot_relative_path = str(
double_dotted_directory_file.relative_to(static_file_directory)
)
_, response = app.test_client.get("/foo/" + dot_relative_path)
assert response.status == 200
assert response.body == b"DOT\n"
def test_breakout(app: Sanic, static_file_directory: str):
app.static("/foo", static_file_directory)
_, response = app.test_client.get("/foo/..%2Ffake/server.py")
assert response.status == 404
_, response = app.test_client.get("/foo/..%2Fstatic/test.file")
assert response.status == 404
@pytest.mark.skipif(
sys.platform != "win32", reason="Block backslash on Windows only"
)
def test_double_backslash_prohibited_on_win32(
app: Sanic, static_file_directory: str
):
app.static("/foo", static_file_directory)
_, response = app.test_client.get("/foo/static/..\\static/test.file")
assert response.status == 404
_, response = app.test_client.get("/foo/static\\../static/test.file")
assert response.status == 404

View File

@@ -1,5 +1,4 @@
import asyncio import asyncio
import sys
from asyncio.tasks import Task from asyncio.tasks import Task
from unittest.mock import Mock, call from unittest.mock import Mock, call
@@ -7,9 +6,15 @@ from unittest.mock import Mock, call
import pytest import pytest
from sanic.app import Sanic from sanic.app import Sanic
from sanic.application.state import ApplicationServerInfo, ServerStage
from sanic.response import empty from sanic.response import empty
try:
from unittest.mock import AsyncMock
except ImportError:
from asyncmock import AsyncMock # type: ignore
pytestmark = pytest.mark.asyncio pytestmark = pytest.mark.asyncio
@@ -20,11 +25,14 @@ async def dummy(n=0):
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def mark_app_running(app): def mark_app_running(app: Sanic):
app.is_running = True app.state.server_info.append(
ApplicationServerInfo(
stage=ServerStage.SERVING, settings={}, server=AsyncMock()
)
)
@pytest.mark.skipif(sys.version_info < (3, 8), reason="Not supported in 3.7")
async def test_add_task_returns_task(app: Sanic): async def test_add_task_returns_task(app: Sanic):
task = app.add_task(dummy()) task = app.add_task(dummy())
@@ -32,7 +40,6 @@ async def test_add_task_returns_task(app: Sanic):
assert len(app._task_registry) == 0 assert len(app._task_registry) == 0
@pytest.mark.skipif(sys.version_info < (3, 8), reason="Not supported in 3.7")
async def test_add_task_with_name(app: Sanic): async def test_add_task_with_name(app: Sanic):
task = app.add_task(dummy(), name="dummy") task = app.add_task(dummy(), name="dummy")
@@ -44,7 +51,6 @@ async def test_add_task_with_name(app: Sanic):
assert task in app._task_registry.values() assert task in app._task_registry.values()
@pytest.mark.skipif(sys.version_info < (3, 8), reason="Not supported in 3.7")
async def test_cancel_task(app: Sanic): async def test_cancel_task(app: Sanic):
task = app.add_task(dummy(3), name="dummy") task = app.add_task(dummy(3), name="dummy")
@@ -62,7 +68,6 @@ async def test_cancel_task(app: Sanic):
assert task.cancelled() assert task.cancelled()
@pytest.mark.skipif(sys.version_info < (3, 8), reason="Not supported in 3.7")
async def test_purge_tasks(app: Sanic): async def test_purge_tasks(app: Sanic):
app.add_task(dummy(3), name="dummy") app.add_task(dummy(3), name="dummy")
@@ -75,7 +80,18 @@ async def test_purge_tasks(app: Sanic):
assert len(app._task_registry) == 0 assert len(app._task_registry) == 0
@pytest.mark.skipif(sys.version_info < (3, 8), reason="Not supported in 3.7") async def test_purge_tasks_with_create_task(app: Sanic):
app.add_task(asyncio.create_task(dummy(3)), name="dummy")
await app.cancel_task("dummy")
assert len(app._task_registry) == 1
app.purge_tasks()
assert len(app._task_registry) == 0
def test_shutdown_tasks_on_app_stop(): def test_shutdown_tasks_on_app_stop():
class TestSanic(Sanic): class TestSanic(Sanic):
shutdown_tasks = Mock() shutdown_tasks = Mock()

View File

@@ -2,6 +2,8 @@ import logging
import pytest import pytest
from sanic_routing.exceptions import NotFound
from sanic.signals import RESERVED_NAMESPACES from sanic.signals import RESERVED_NAMESPACES
from sanic.touchup import TouchUp from sanic.touchup import TouchUp
@@ -28,3 +30,50 @@ async def test_ode_removes_dispatch_events(app, caplog, verbosity, result):
) )
in logs in logs
) is result ) is result
@pytest.mark.parametrize("skip_it,result", ((False, True), (True, False)))
async def test_skip_touchup(app, caplog, skip_it, result):
app.config.TOUCHUP = not skip_it
with caplog.at_level(logging.DEBUG, logger="sanic.root"):
app.state.verbosity = 2
await app._startup()
assert app.signal_router.allow_fail_builtin is (not skip_it)
logs = caplog.record_tuples
for signal in RESERVED_NAMESPACES["http"]:
assert (
(
"sanic.root",
logging.DEBUG,
f"Disabling event: {signal}",
)
in logs
) is result
not_found_exceptions = 0
# Skip-touchup disables NotFound exceptions on the dispatcher
for signal in RESERVED_NAMESPACES["http"]:
try:
await app.dispatch(event=signal, inline=True)
except NotFound:
not_found_exceptions += 1
assert (not_found_exceptions > 0) is result
@pytest.mark.parametrize("skip_it,result", ((False, True), (True, True)))
async def test_skip_touchup_non_reserved(app, caplog, skip_it, result):
app.config.TOUCHUP = not skip_it
@app.signal("foo.bar.one")
def sync_signal(*_):
...
await app._startup()
assert app.signal_router.allow_fail_builtin is (not skip_it)
not_found_exception = False
# Skip-touchup doesn't disable NotFound exceptions for user-defined signals
try:
await app.dispatch(event="foo.baz.two", inline=True)
except NotFound:
not_found_exception = True
assert not_found_exception is result

View File

@@ -72,14 +72,12 @@ def test_unix_socket_creation(caplog):
assert not os.path.exists(SOCKPATH) assert not os.path.exists(SOCKPATH)
def test_invalid_paths(): @pytest.mark.parametrize("path", (".", "no-such-directory/sanictest.sock"))
def test_invalid_paths(path):
app = Sanic(name=__name__) app = Sanic(name=__name__)
with pytest.raises(FileExistsError): with pytest.raises((FileExistsError, FileNotFoundError)):
app.run(unix=".") app.run(unix=path)
with pytest.raises(FileNotFoundError):
app.run(unix="no-such-directory/sanictest.sock")
def test_dont_replace_file(): def test_dont_replace_file():
@@ -201,7 +199,7 @@ async def test_zero_downtime():
for _ in range(40): for _ in range(40):
async with httpx.AsyncClient(transport=transport) as client: async with httpx.AsyncClient(transport=transport) as client:
r = await client.get("http://localhost/sleep/0.1") r = await client.get("http://localhost/sleep/0.1")
assert r.status_code == 200 assert r.status_code == 200, r.text
assert r.text == "Slept 0.1 seconds.\n" assert r.text == "Slept 0.1 seconds.\n"
def spawn(): def spawn():
@@ -209,6 +207,7 @@ async def test_zero_downtime():
sys.executable, sys.executable,
"-m", "-m",
"sanic", "sanic",
"--debug",
"--unix", "--unix",
SOCKPATH, SOCKPATH,
"examples.delayed_response.app", "examples.delayed_response.app",

243
tests/test_websockets.py Normal file
View File

@@ -0,0 +1,243 @@
import re
from asyncio import Event, Queue, TimeoutError
from unittest.mock import Mock, call
import pytest
from websockets.frames import CTRL_OPCODES, DATA_OPCODES, Frame
from sanic.exceptions import ServerError
from sanic.server.websockets.frame import WebsocketFrameAssembler
try:
from unittest.mock import AsyncMock
except ImportError:
from asyncmock import AsyncMock # type: ignore
@pytest.mark.asyncio
async def test_ws_frame_get_message_incomplete_timeout_0():
assembler = WebsocketFrameAssembler(Mock())
assembler.message_complete = AsyncMock(spec=Event)
assembler.message_complete.is_set = Mock(return_value=False)
data = await assembler.get(0)
assert data is None
assembler.message_complete.is_set.assert_called_once()
@pytest.mark.asyncio
async def test_ws_frame_get_message_in_progress():
assembler = WebsocketFrameAssembler(Mock())
assembler.get_in_progress = True
message = re.escape(
"Called get() on Websocket frame assembler "
"while asynchronous get is already in progress."
)
with pytest.raises(ServerError, match=message):
await assembler.get()
@pytest.mark.asyncio
async def test_ws_frame_get_message_incomplete():
assembler = WebsocketFrameAssembler(Mock())
assembler.message_complete.wait = AsyncMock(return_value=True)
assembler.message_complete.is_set = Mock(return_value=False)
data = await assembler.get()
assert data is None
assembler.message_complete.wait.assert_awaited_once()
@pytest.mark.asyncio
async def test_ws_frame_get_message():
assembler = WebsocketFrameAssembler(Mock())
assembler.message_complete.wait = AsyncMock(return_value=True)
assembler.message_complete.is_set = Mock(return_value=True)
data = await assembler.get()
assert data == b""
assembler.message_complete.wait.assert_awaited_once()
@pytest.mark.asyncio
async def test_ws_frame_get_message_with_timeout():
assembler = WebsocketFrameAssembler(Mock())
assembler.message_complete.wait = AsyncMock(return_value=True)
assembler.message_complete.is_set = Mock(return_value=True)
data = await assembler.get(0.1)
assert data == b""
assembler.message_complete.wait.assert_awaited_once()
assert assembler.message_complete.is_set.call_count == 2
@pytest.mark.asyncio
async def test_ws_frame_get_message_with_timeouterror():
assembler = WebsocketFrameAssembler(Mock())
assembler.message_complete.wait = AsyncMock(return_value=True)
assembler.message_complete.is_set = Mock(return_value=True)
assembler.message_complete.wait.side_effect = TimeoutError("...")
data = await assembler.get(0.1)
assert data == b""
assembler.message_complete.wait.assert_awaited_once()
assert assembler.message_complete.is_set.call_count == 2
@pytest.mark.asyncio
async def test_ws_frame_get_not_completed():
assembler = WebsocketFrameAssembler(Mock())
assembler.message_complete = AsyncMock(spec=Event)
assembler.message_complete.is_set = Mock(return_value=False)
data = await assembler.get()
assert data is None
@pytest.mark.asyncio
async def test_ws_frame_get_not_completed_start():
assembler = WebsocketFrameAssembler(Mock())
assembler.message_complete = AsyncMock(spec=Event)
assembler.message_complete.is_set = Mock(side_effect=[False, True])
data = await assembler.get(0.1)
assert data is None
@pytest.mark.asyncio
async def test_ws_frame_get_paused():
assembler = WebsocketFrameAssembler(Mock())
assembler.message_complete = AsyncMock(spec=Event)
assembler.message_complete.is_set = Mock(side_effect=[False, True])
assembler.paused = True
data = await assembler.get()
assert data is None
assembler.protocol.resume_frames.assert_called_once()
@pytest.mark.asyncio
async def test_ws_frame_get_data():
assembler = WebsocketFrameAssembler(Mock())
assembler.message_complete = AsyncMock(spec=Event)
assembler.message_complete.is_set = Mock(return_value=True)
assembler.chunks = [b"foo", b"bar"]
data = await assembler.get()
assert data == b"foobar"
@pytest.mark.asyncio
async def test_ws_frame_get_iter_in_progress():
assembler = WebsocketFrameAssembler(Mock())
assembler.get_in_progress = True
message = re.escape(
"Called get_iter on Websocket frame assembler "
"while asynchronous get is already in progress."
)
with pytest.raises(ServerError, match=message):
[x async for x in assembler.get_iter()]
@pytest.mark.asyncio
async def test_ws_frame_get_iter_none_in_queue():
assembler = WebsocketFrameAssembler(Mock())
assembler.message_complete.set()
assembler.chunks = [b"foo", b"bar"]
chunks = [x async for x in assembler.get_iter()]
assert chunks == [b"foo", b"bar"]
@pytest.mark.asyncio
async def test_ws_frame_get_iter_paused():
assembler = WebsocketFrameAssembler(Mock())
assembler.message_complete.set()
assembler.paused = True
[x async for x in assembler.get_iter()]
assembler.protocol.resume_frames.assert_called_once()
@pytest.mark.asyncio
@pytest.mark.parametrize("opcode", DATA_OPCODES)
async def test_ws_frame_put_not_fetched(opcode):
assembler = WebsocketFrameAssembler(Mock())
assembler.message_fetched.set()
message = re.escape(
"Websocket put() got a new message when the previous message was "
"not yet fetched."
)
with pytest.raises(ServerError, match=message):
await assembler.put(Frame(opcode, b""))
@pytest.mark.asyncio
@pytest.mark.parametrize("opcode", DATA_OPCODES)
async def test_ws_frame_put_fetched(opcode):
assembler = WebsocketFrameAssembler(Mock())
assembler.message_fetched = AsyncMock()
assembler.message_fetched.is_set = Mock(return_value=False)
await assembler.put(Frame(opcode, b""))
assembler.message_fetched.wait.assert_awaited_once()
assembler.message_fetched.clear.assert_called_once()
@pytest.mark.asyncio
@pytest.mark.parametrize("opcode", DATA_OPCODES)
async def test_ws_frame_put_message_complete(opcode):
assembler = WebsocketFrameAssembler(Mock())
assembler.message_complete.set()
message = re.escape(
"Websocket put() got a new message when a message was "
"already in its chamber."
)
with pytest.raises(ServerError, match=message):
await assembler.put(Frame(opcode, b""))
@pytest.mark.asyncio
@pytest.mark.parametrize("opcode", DATA_OPCODES)
async def test_ws_frame_put_message_into_queue(opcode):
assembler = WebsocketFrameAssembler(Mock())
assembler.chunks_queue = AsyncMock(spec=Queue)
assembler.message_fetched = AsyncMock()
assembler.message_fetched.is_set = Mock(return_value=False)
await assembler.put(Frame(opcode, b"foo"))
assembler.chunks_queue.put.has_calls(
call(b"foo"),
call(None),
)
@pytest.mark.asyncio
@pytest.mark.parametrize("opcode", DATA_OPCODES)
async def test_ws_frame_put_not_fin(opcode):
assembler = WebsocketFrameAssembler(Mock())
retval = await assembler.put(Frame(opcode, b"foo", fin=False))
assert retval is None
@pytest.mark.asyncio
@pytest.mark.parametrize("opcode", CTRL_OPCODES)
async def test_ws_frame_put_skip_ctrl(opcode):
assembler = WebsocketFrameAssembler(Mock())
retval = await assembler.put(Frame(opcode, b""))
assert retval is None

View File

@@ -1,200 +0,0 @@
import asyncio
import json
import shlex
import subprocess
import time
import urllib.request
from unittest import mock
import pytest
from sanic_testing.testing import ASGI_PORT as PORT
from sanic.app import Sanic
from sanic.worker import GunicornWorker
@pytest.fixture
def gunicorn_worker():
command = (
"gunicorn "
f"--bind 127.0.0.1:{PORT} "
"--worker-class sanic.worker.GunicornWorker "
"examples.hello_world:app"
)
worker = subprocess.Popen(shlex.split(command))
time.sleep(2)
yield
worker.kill()
@pytest.fixture
def gunicorn_worker_with_access_logs():
command = (
"gunicorn "
f"--bind 127.0.0.1:{PORT + 1} "
"--worker-class sanic.worker.GunicornWorker "
"examples.hello_world:app"
)
worker = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE)
time.sleep(2)
return worker
@pytest.fixture
def gunicorn_worker_with_env_var():
command = (
'env SANIC_ACCESS_LOG="False" '
"gunicorn "
f"--bind 127.0.0.1:{PORT + 2} "
"--worker-class sanic.worker.GunicornWorker "
"--log-level info "
"examples.hello_world:app"
)
worker = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE)
time.sleep(2)
return worker
def test_gunicorn_worker(gunicorn_worker):
with urllib.request.urlopen(f"http://localhost:{PORT}/") as f:
res = json.loads(f.read(100).decode())
assert res["test"]
def test_gunicorn_worker_no_logs(gunicorn_worker_with_env_var):
"""
if SANIC_ACCESS_LOG was set to False do not show access logs
"""
with urllib.request.urlopen(f"http://localhost:{PORT + 2}/") as _:
gunicorn_worker_with_env_var.kill()
logs = list(
filter(
lambda x: b"sanic.access" in x,
gunicorn_worker_with_env_var.stdout.read().split(b"\n"),
)
)
assert len(logs) == 0
def test_gunicorn_worker_with_logs(gunicorn_worker_with_access_logs):
"""
default - show access logs
"""
with urllib.request.urlopen(f"http://localhost:{PORT + 1}/") as _:
gunicorn_worker_with_access_logs.kill()
assert (
b"(sanic.access)[INFO][127.0.0.1"
in gunicorn_worker_with_access_logs.stdout.read()
)
class GunicornTestWorker(GunicornWorker):
def __init__(self):
self.app = mock.Mock()
self.app.callable = Sanic("test_gunicorn_worker")
self.servers = {}
self.exit_code = 0
self.cfg = mock.Mock()
self.notify = mock.Mock()
@pytest.fixture
def worker():
return GunicornTestWorker()
def test_worker_init_process(worker):
with mock.patch("sanic.worker.asyncio") as mock_asyncio:
try:
worker.init_process()
except TypeError:
pass
assert mock_asyncio.get_event_loop.return_value.close.called
assert mock_asyncio.new_event_loop.called
assert mock_asyncio.set_event_loop.called
def test_worker_init_signals(worker):
worker.loop = mock.Mock()
worker.init_signals()
assert worker.loop.add_signal_handler.called
def test_handle_abort(worker):
with mock.patch("sanic.worker.sys") as mock_sys:
worker.handle_abort(object(), object())
assert not worker.alive
assert worker.exit_code == 1
mock_sys.exit.assert_called_with(1)
def test_handle_quit(worker):
worker.handle_quit(object(), object())
assert not worker.alive
assert worker.exit_code == 0
async def _a_noop(*a, **kw):
pass
def test_run_max_requests_exceeded(worker):
loop = asyncio.new_event_loop()
worker.ppid = 1
worker.alive = True
sock = mock.Mock()
sock.cfg_addr = ("localhost", 8080)
worker.sockets = [sock]
worker.wsgi = mock.Mock()
worker.connections = set()
worker.log = mock.Mock()
worker.loop = loop
worker.servers = {
"server1": {"requests_count": 14},
"server2": {"requests_count": 15},
}
worker.max_requests = 10
worker._run = mock.Mock(wraps=_a_noop)
# exceeding request count
_runner = asyncio.ensure_future(worker._check_alive(), loop=loop)
loop.run_until_complete(_runner)
assert not worker.alive
worker.notify.assert_called_with()
worker.log.info.assert_called_with(
"Max requests exceeded, shutting " "down: %s", worker
)
def test_worker_close(worker):
loop = asyncio.new_event_loop()
asyncio.sleep = mock.Mock(wraps=_a_noop)
worker.ppid = 1
worker.pid = 2
worker.cfg.graceful_timeout = 1.0
worker.signal = mock.Mock()
worker.signal.stopped = False
worker.wsgi = mock.Mock()
conn = mock.Mock()
conn.websocket = mock.Mock()
conn.websocket.fail_connection = mock.Mock(wraps=_a_noop)
worker.connections = set([conn])
worker.log = mock.Mock()
worker.loop = loop
server = mock.Mock()
server.close = mock.Mock(wraps=lambda *a, **kw: None)
server.wait_closed = mock.Mock(wraps=_a_noop)
worker.servers = {server: {"requests_count": 14}}
worker.max_requests = 10
# close worker
_close = asyncio.ensure_future(worker.close(), loop=loop)
loop.run_until_complete(_close)
assert worker.signal.stopped
assert conn.websocket.fail_connection.called
assert len(worker.servers) == 0

View File

@@ -7,6 +7,9 @@ setenv =
{py37,py38,py39,py310,pyNightly}-no-ext: SANIC_NO_UJSON=1 {py37,py38,py39,py310,pyNightly}-no-ext: SANIC_NO_UJSON=1
{py37,py38,py39,py310,pyNightly}-no-ext: SANIC_NO_UVLOOP=1 {py37,py38,py39,py310,pyNightly}-no-ext: SANIC_NO_UVLOOP=1
extras = test extras = test
allowlist_externals =
pytest
coverage
commands = commands =
pytest {posargs:tests --cov sanic} pytest {posargs:tests --cov sanic}
- coverage combine --append - coverage combine --append
@@ -18,6 +21,7 @@ commands =
flake8 sanic flake8 sanic
black --config ./.black.toml --check --verbose sanic/ black --config ./.black.toml --check --verbose sanic/
isort --check-only sanic --profile=black isort --check-only sanic --profile=black
slotscheck --verbose -m sanic
[testenv:type-checking] [testenv:type-checking]
commands = commands =
@@ -41,7 +45,7 @@ commands =
[testenv:docs] [testenv:docs]
platform = linux|linux2|darwin platform = linux|linux2|darwin
whitelist_externals = make allowlist_externals = make
extras = docs extras = docs
commands = commands =
make docs-test make docs-test