Compare commits

..

2 Commits

Author SHA1 Message Date
Adam Hopkins
8aecbdb52c Bump version 2022-05-10 22:38:00 +03:00
Adam Hopkins
3a1a9f071d Resolve warning issue with error handler mismatch warning 2022-05-10 22:37:51 +03:00
171 changed files with 2962 additions and 10358 deletions

2
.black.toml Normal file
View File

@@ -0,0 +1,2 @@
[tool.black]
line-length = 79

View File

@@ -4,8 +4,8 @@ source = sanic
omit =
site-packages
sanic/__main__.py
sanic/server/legacy.py
sanic/compat.py
sanic/reloader_helpers.py
sanic/simple.py
sanic/utils.py
sanic/cli
@@ -20,5 +20,12 @@ exclude_lines =
noqa
NOQA
pragma: no cover
TYPE_CHECKING
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

@@ -1,66 +0,0 @@
name: 🐞 Bug report
description: Create a report to help us improve
labels: ["bug", "triage"]
body:
- type: checkboxes
id: existing
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the bug you encountered.
options:
- label: I have searched the existing issues
required: true
- type: textarea
id: description
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is, make sure to paste any exceptions and tracebacks using markdown code-block syntax to make it easier to read.
validations:
required: true
- type: textarea
id: code
attributes:
label: Code snippet
description: Relevant source code, make sure to remove what is not necessary.
validations:
required: false
- type: textarea
id: expected
attributes:
label: Expected Behavior
description: A concise description of what you expected to happen.
validations:
required: false
- type: dropdown
id: running
attributes:
label: How do you run Sanic?
options:
- Sanic CLI
- As a module
- As a script (`app.run` or `Sanic.serve`)
- ASGI
validations:
required: true
- type: input
id: os
attributes:
label: Operating System
description: What OS?
validations:
required: true
- type: input
id: version
attributes:
label: Sanic Version
description: Check startup logs or try `sanic --version`
validations:
required: true
- type: textarea
id: additional
attributes:
label: Additional context
description: Add any other context about the problem here.
validations:
required: false

25
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,25 @@
---
name: Bug report
about: Create a report to help us improve
---
**Describe the bug**
A clear and concise description of what the bug is, make sure to paste any exceptions and tracebacks.
**Code snippet**
Relevant source code, make sure to remove what is not necessary.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Environment (please complete the following information):**
- OS: [e.g. iOS]
- Version [e.g. 0.8.3]
**Additional context**
Add any other context about the problem here.

View File

@@ -1,8 +1,5 @@
blank_issues_enabled: false
blank_issues_enabled: true
contact_links:
- name: Questions and Help
url: https://community.sanicframework.org/c/questions-and-help
about: Do you need help with Sanic? Ask your questions here.
- name: Discussion and Support
url: https://discord.gg/FARQzAEMAA
about: For live discussion and support, checkout the Sanic Discord server.

View File

@@ -1,34 +0,0 @@
name: 🌟 Feature request
description: Suggest an enhancement for Sanic
labels: ["feature request"]
body:
- type: checkboxes
id: existing
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the enhancement you are proposing.
options:
- label: I have searched the existing issues
required: true
- type: textarea
id: description
attributes:
label: Is your feature request related to a problem? Please describe.
description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
validations:
required: false
- type: textarea
id: code
attributes:
label: Describe the solution you'd like
description: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
id: additional
attributes:
label: Additional context
description: Add any other context about the problem here.
validations:
required: false

View File

@@ -0,0 +1,16 @@
---
name: Feature request
about: Suggest an idea for Sanic
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Additional context**
Add any other context or sample code about the feature request here.

View File

@@ -313,10 +313,8 @@ Version 21.3.0
`#2074 <https://github.com/sanic-org/sanic/pull/2074>`_
Performance adjustments in ``handle_request_``
Version 20.12.3 🔷
------------------
`Current LTS version`
Version 20.12.3
---------------
**Bugfixes**
@@ -350,8 +348,8 @@ Version 19.12.5
`#2027 <https://github.com/sanic-org/sanic/pull/2027>`_
Remove old chardet requirement, add in hard multidict requirement
Version 20.12.0 🔹
-----------------
Version 20.12.0
---------------
**Features**
@@ -359,6 +357,11 @@ Version 20.12.0 🔹
`#1993 <https://github.com/sanic-org/sanic/pull/1993>`_
Add disable app registry
Version 20.12.0
---------------
**Features**
*
`#1945 <https://github.com/sanic-org/sanic/pull/1945>`_
Static route more verbose if file not found

View File

@@ -55,7 +55,7 @@ further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at adam@sanicframework.org. All
reported by contacting the project team at sanic-maintainers@googlegroups.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.

View File

@@ -71,9 +71,9 @@ To execute only unittests, run ``tox`` with environment like so:
.. code-block:: bash
tox -e py37 -v -- tests/test_config.py
tox -e py36 -v -- tests/test_config.py
# or
tox -e py310 -v -- tests/test_config.py
tox -e py37 -v -- tests/test_config.py
Run lint checks
---------------

View File

@@ -66,15 +66,15 @@ ifdef include_tests
isort -rc sanic tests
else
$(info Sorting Imports)
isort -rc sanic tests
isort -rc sanic tests --profile=black
endif
endif
black:
black sanic tests
black --config ./.black.toml sanic tests
isort:
isort sanic tests
isort sanic tests --profile=black
pretty: black isort

View File

@@ -102,6 +102,9 @@ Installation
If you are running on a clean install of Fedora 28 or above, please make sure you have the ``redhat-rpm-config`` package installed in case if you want to
use ``sanic`` with ``ujson`` dependency.
.. note::
Windows support is currently "experimental" and on a best-effort basis. Multiple workers are also not currently supported on Windows (see `Issue #1517 <https://github.com/sanic-org/sanic/issues/1517>`_), but setting ``workers=1`` should launch the server successfully.
Hello World Example
-------------------
@@ -111,7 +114,7 @@ Hello World Example
from sanic import Sanic
from sanic.response import json
app = Sanic("my-hello-world-app")
app = Sanic("My Hello, world app")
@app.route('/')
async def test(request):

View File

@@ -4,41 +4,31 @@
Sanic releases long term support release once a year in December. LTS releases receive bug and security updates for **24 months**. Interim releases throughout the year occur every three months, and are supported until the subsequent interim release.
| Version | LTS | Supported |
| ------- | ------------- | ------------------ |
| 20.12 | until 2022-12 | :heavy_check_mark: |
| 20.9 | | :x: |
| 20.6 | | :x: |
| 20.3 | | :x: |
| 19.12 | until 2021-12 | :white_check_mark: |
| 19.9 | | :x: |
| 19.6 | | :x: |
| 19.3 | | :x: |
| 18.12 | | :x: |
| 0.8.3 | | :x: |
| 0.7.0 | | :x: |
| 0.6.0 | | :x: |
| 0.5.4 | | :x: |
| 0.4.1 | | :x: |
| 0.3.1 | | :x: |
| 0.2.0 | | :x: |
| 0.1.9 | | :x: |
| Version | LTS | Supported |
| ------- | ------------- | ----------------------- |
| 22.9 | | :white_check_mark: |
| 22.6 | | :x: |
| 22.3 | | :x: |
| 21.12 | until 2023-12 | :white_check_mark: |
| 21.9 | | :x: |
| 21.6 | | :x: |
| 21.3 | | :x: |
| 20.12 | until 2022-12 | :ballot_box_with_check: |
| 20.9 | | :x: |
| 20.6 | | :x: |
| 20.3 | | :x: |
| 19.12 | | :x: |
| 19.9 | | :x: |
| 19.6 | | :x: |
| 19.3 | | :x: |
| 18.12 | | :x: |
| 0.8.3 | | :x: |
| 0.7.0 | | :x: |
| 0.6.0 | | :x: |
| 0.5.4 | | :x: |
| 0.4.1 | | :x: |
| 0.3.1 | | :x: |
| 0.2.0 | | :x: |
| 0.1.9 | | :x: |
:ballot_box_with_check: = security/bug fixes
:white_check_mark: = full support
:white_check_mark: = security/bug fixes
:heavy_check_mark: = full support
## Reporting a Vulnerability
If you discover a security vulnerability, we ask that you **do not** create an issue on GitHub. Instead, please [send a message to the core-devs](https://community.sanicframework.org/g/core-devs) on the community forums. Once logged in, you can send a message to the core-devs by clicking the message button.
Alternatively, you can send a private message to Adam Hopkins on Discord. Find him on the [Sanic discord server](https://discord.gg/FARQzAEMAA).
This will help to not publicize the issue until the team can address it and resolve it.

View File

@@ -4,7 +4,6 @@ coverage:
default:
target: auto
threshold: 0.75
informational: true
project:
default:
target: auto
@@ -15,6 +14,7 @@ codecov:
ignore:
- "sanic/__main__.py"
- "sanic/compat.py"
- "sanic/reloader_helpers.py"
- "sanic/simple.py"
- "sanic/utils.py"
- "sanic/cli"

View File

@@ -2,12 +2,3 @@
.wy-nav-top {
background: #444444;
}
#changelog section {
padding-left: 3rem;
}
#changelog section h2,
#changelog section h3 {
margin-left: -3rem;
}

View File

@@ -24,11 +24,7 @@ import sanic
# -- General configuration ------------------------------------------------
extensions = [
"sphinx.ext.autodoc",
"m2r2",
"enum_tools.autoenum",
]
extensions = ["sphinx.ext.autodoc", "m2r2"]
templates_path = ["_templates"]

View File

@@ -9,7 +9,7 @@ API
===
.. toctree::
:maxdepth: 3
:maxdepth: 2
👥 User Guide <https://sanicframework.org/guide/>
sanic/api_reference

View File

@@ -15,19 +15,3 @@ sanic.config
.. automodule:: sanic.config
:members:
:show-inheritance:
sanic.application.constants
---------------------------
.. automodule:: sanic.application.constants
:exclude-members: StrEnum
:members:
:show-inheritance:
:inherited-members:
sanic.application.state
-----------------------
.. automodule:: sanic.application.state
:members:
:show-inheritance:

View File

@@ -17,14 +17,6 @@ sanic.handlers
:show-inheritance:
sanic.headers
--------------
.. automodule:: sanic.headers
:members:
:show-inheritance:
sanic.request
-------------

View File

@@ -16,3 +16,10 @@ sanic.server
:members:
:show-inheritance:
sanic.worker
------------
.. automodule:: sanic.worker
:members:
:show-inheritance:

View File

@@ -1,8 +1,6 @@
📜 Changelog
============
.. mdinclude:: ./releases/22/22.9.md
.. mdinclude:: ./releases/22/22.6.md
.. mdinclude:: ./releases/22/22.3.md
.. mdinclude:: ./releases/21/21.12.md
.. mdinclude:: ./releases/21/21.9.md

View File

@@ -1,12 +1,10 @@
## Version 21.12.1 🔷
_Current LTS version_
## Version 21.12.1
- [#2349](https://github.com/sanic-org/sanic/pull/2349) Only display MOTD on startup
- [#2354](https://github.com/sanic-org/sanic/pull/2354) Ignore name argument in Python 3.7
- [#2355](https://github.com/sanic-org/sanic/pull/2355) Add config.update support for all config values
## Version 21.12.0 🔹
## Version 21.12.0
### Features
- [#2260](https://github.com/sanic-org/sanic/pull/2260) Allow early Blueprint registrations to still apply later added objects

View File

@@ -1,54 +0,0 @@
## Version 22.6.2
### Bugfixes
- [#2522](https://github.com/sanic-org/sanic/pull/2522) Always show server location in ASGI
## Version 22.6.1
### Bugfixes
- [#2477](https://github.com/sanic-org/sanic/pull/2477) Sanic static directory fails when folder name ends with ".."
## Version 22.6.0
### Features
- [#2378](https://github.com/sanic-org/sanic/pull/2378) Introduce HTTP/3 and autogeneration of TLS certificates in `DEBUG` mode
- 👶 *EARLY RELEASE FEATURE*: Serving Sanic over HTTP/3 is an early release feature. It does not yet fully cover the HTTP/3 spec, but instead aims for feature parity with Sanic's existing HTTP/1.1 server. Websockets, WebTransport, push responses are examples of some features not yet implemented.
- 📦 *EXTRA REQUIREMENT*: Not all HTTP clients are capable of interfacing with HTTP/3 servers. You may need to install a [HTTP/3 capable client](https://curl.se/docs/http3.html).
- 📦 *EXTRA REQUIREMENT*: In order to use TLS autogeneration, you must install either [mkcert](https://github.com/FiloSottile/mkcert) or [trustme](https://github.com/python-trio/trustme).
- [#2416](https://github.com/sanic-org/sanic/pull/2416) Add message to `task.cancel`
- [#2420](https://github.com/sanic-org/sanic/pull/2420) Add exception aliases for more consistent naming with standard HTTP response types (`BadRequest`, `MethodNotAllowed`, `RangeNotSatisfiable`)
- [#2432](https://github.com/sanic-org/sanic/pull/2432) Expose ASGI `scope` as a property on the `Request` object
- [#2438](https://github.com/sanic-org/sanic/pull/2438) Easier access to websocket class for annotation: `from sanic import Websocket`
- [#2439](https://github.com/sanic-org/sanic/pull/2439) New API for reading form values with options: `Request.get_form`
- [#2445](https://github.com/sanic-org/sanic/pull/2445) Add custom `loads` function
- [#2447](https://github.com/sanic-org/sanic/pull/2447), [#2486](https://github.com/sanic-org/sanic/pull/2486) Improved API to support setting cache control headers
- [#2453](https://github.com/sanic-org/sanic/pull/2453) Move verbosity filtering to logger
- [#2475](https://github.com/sanic-org/sanic/pull/2475) Expose getter for current request using `Request.get_current()`
### Bugfixes
- [#2448](https://github.com/sanic-org/sanic/pull/2448) Fix to allow running with `pythonw.exe` or places where there is no `sys.stdout`
- [#2451](https://github.com/sanic-org/sanic/pull/2451) Trigger `http.lifecycle.request` signal in ASGI mode
- [#2455](https://github.com/sanic-org/sanic/pull/2455) Resolve typing of stacked route definitions
- [#2463](https://github.com/sanic-org/sanic/pull/2463) Properly catch websocket CancelledError in websocket handler in Python 3.7
### Deprecations and Removals
- [#2487](https://github.com/sanic-org/sanic/pull/2487) v22.6 deprecations and changes
1. Optional application registry
1. Execution of custom handlers after some part of response was sent
1. Configuring fallback handlers on the `ErrorHandler`
1. Custom `LOGO` setting
1. `sanic.response.stream`
1. `AsyncioServer.init`
### Developer infrastructure
- [#2449](https://github.com/sanic-org/sanic/pull/2449) Clean up `black` and `isort` config
- [#2479](https://github.com/sanic-org/sanic/pull/2479) Fix some flappy tests
### Improved Documentation
- [#2461](https://github.com/sanic-org/sanic/pull/2461) Update example to match current application naming standards
- [#2466](https://github.com/sanic-org/sanic/pull/2466) Better type annotation for `Extend`
- [#2485](https://github.com/sanic-org/sanic/pull/2485) Improved help messages in CLI

View File

@@ -1,47 +0,0 @@
## Version 22.9.0 🔶
_Current version_
### Features
- [#2445](https://github.com/sanic-org/sanic/pull/2445) Add custom loads function
- [#2490](https://github.com/sanic-org/sanic/pull/2490) Make `WebsocketImplProtocol` async iterable
- [#2499](https://github.com/sanic-org/sanic/pull/2499) Sanic Server WorkerManager refactor
- [#2506](https://github.com/sanic-org/sanic/pull/2506) Use `pathlib` for path resolution (for static file serving)
- [#2508](https://github.com/sanic-org/sanic/pull/2508) Use `path.parts` instead of `match` (for static file serving)
- [#2513](https://github.com/sanic-org/sanic/pull/2513) Better request cancel handling
- [#2516](https://github.com/sanic-org/sanic/pull/2516) Add request properties for HTTP method info:
- `request.is_safe`
- `request.is_idempotent`
- `request.is_cacheable`
- *See* [MDN docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods) *for more information about when these apply*
- [#2522](https://github.com/sanic-org/sanic/pull/2522) Always show server location in ASGI
- [#2526](https://github.com/sanic-org/sanic/pull/2526) Cache control support for static files for returning 304 when appropriate
- [#2533](https://github.com/sanic-org/sanic/pull/2533) Refactor `_static_request_handler`
- [#2540](https://github.com/sanic-org/sanic/pull/2540) Add signals before and after handler execution
- `http.handler.before`
- `http.handler.after`
- [#2542](https://github.com/sanic-org/sanic/pull/2542) Add *[redacted]* to CLI :)
- [#2546](https://github.com/sanic-org/sanic/pull/2546) Add deprecation warning filter
- [#2550](https://github.com/sanic-org/sanic/pull/2550) Middleware priority and performance enhancements
### Bugfixes
- [#2495](https://github.com/sanic-org/sanic/pull/2495) Prevent directory traversion with static files
- [#2515](https://github.com/sanic-org/sanic/pull/2515) Do not apply double slash to paths in certain static dirs in Blueprints
### Deprecations and Removals
- [#2525](https://github.com/sanic-org/sanic/pull/2525) Warn on duplicate route names, will be prevented outright in v23.3
- [#2537](https://github.com/sanic-org/sanic/pull/2537) Raise warning and deprecation notice on duplicate exceptions, will be prevented outright in v23.3
### Developer infrastructure
- [#2504](https://github.com/sanic-org/sanic/pull/2504) Cleanup test suite
- [#2505](https://github.com/sanic-org/sanic/pull/2505) Replace Unsupported Python Version Number from the Contributing Doc
- [#2530](https://github.com/sanic-org/sanic/pull/2530) Do not include tests folder in installed package resolver
### Improved Documentation
- [#2502](https://github.com/sanic-org/sanic/pull/2502) Fix a few typos
- [#2517](https://github.com/sanic-org/sanic/pull/2517) [#2536](https://github.com/sanic-org/sanic/pull/2536) Add some type hints

View File

@@ -1,3 +1,6 @@
import os
import socket
from sanic import Sanic, response
@@ -10,4 +13,13 @@ async def test(request):
if __name__ == "__main__":
app.run(unix="./uds_socket")
server_address = "./uds_socket"
# Make sure the socket does not already exist
try:
os.unlink(server_address)
except OSError:
if os.path.exists(server_address):
raise
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.bind(server_address)
app.run(sock=sock)

View File

@@ -1,26 +1,3 @@
[build-system]
requires = ["setuptools<60.0", "wheel"]
build-backend = "setuptools.build_meta"
[tool.black]
line-length = 79
[tool.isort]
atomic = true
default_section = "THIRDPARTY"
include_trailing_comma = true
known_first_party = "sanic"
known_third_party = "pytest"
line_length = 79
lines_after_imports = 2
lines_between_types = 1
multi_line_output = 3
profile = "black"
[[tool.mypy.overrides]]
module = [
"httptools.*",
"trustme.*",
"sanic_routing.*",
]
ignore_missing_imports = true

View File

@@ -3,16 +3,7 @@ from sanic.app import Sanic
from sanic.blueprints import Blueprint
from sanic.constants import HTTPMethod
from sanic.request import Request
from sanic.response import (
HTTPResponse,
empty,
file,
html,
json,
redirect,
text,
)
from sanic.server.websockets.impl import WebsocketImplProtocol as Websocket
from sanic.response import HTTPResponse, html, json, text
__all__ = (
@@ -22,11 +13,7 @@ __all__ = (
"HTTPMethod",
"HTTPResponse",
"Request",
"Websocket",
"empty",
"file",
"html",
"json",
"redirect",
"text",
)

View File

@@ -6,10 +6,10 @@ if OS_IS_WINDOWS:
enable_windows_color_support()
def main(args=None):
def main():
cli = SanicCLI()
cli.attach()
cli.run(args)
cli.run()
if __name__ == "__main__":

View File

@@ -1 +1 @@
__version__ = "22.12.0a0"
__version__ = "22.3.2"

View File

@@ -19,7 +19,6 @@ from collections import defaultdict, deque
from contextlib import suppress
from functools import partial
from inspect import isawaitable
from os import environ
from socket import socket
from traceback import format_exc
from types import SimpleNamespace
@@ -42,12 +41,16 @@ from typing import (
Union,
)
from urllib.parse import urlencode, urlunparse
from warnings import filterwarnings
from sanic_routing.exceptions import FinalizationError, NotFound
from sanic_routing.route import Route
from sanic_routing.exceptions import ( # type: ignore
FinalizationError,
NotFound,
)
from sanic_routing.route import Route # type: ignore
from sanic.application.ext import setup_ext
from sanic.application.state import ApplicationState, ServerStage
from sanic.application.state import ApplicationState, Mode, ServerStage
from sanic.asgi import ASGIApp
from sanic.base.root import BaseSanic
from sanic.blueprint_group import BlueprintGroup
@@ -55,13 +58,12 @@ from sanic.blueprints import Blueprint
from sanic.compat import OS_IS_WINDOWS, enable_windows_color_support
from sanic.config import SANIC_PREFIX, Config
from sanic.exceptions import (
BadRequest,
InvalidUsage,
SanicException,
ServerError,
URLBuildError,
)
from sanic.handlers import ErrorHandler
from sanic.helpers import Default
from sanic.http import Stage
from sanic.log import (
LOGGING_CONFIG_DEFAULTS,
@@ -70,7 +72,7 @@ from sanic.log import (
logger,
)
from sanic.mixins.listeners import ListenerEvent
from sanic.mixins.startup import StartupMixin
from sanic.mixins.runner import RunnerMixin
from sanic.models.futures import (
FutureException,
FutureListener,
@@ -88,24 +90,23 @@ from sanic.router import Router
from sanic.server.websockets.impl import ConnectionClosed
from sanic.signals import Signal, SignalRouter
from sanic.touchup import TouchUp, TouchUpMeta
from sanic.types.shared_ctx import SharedContext
from sanic.worker.inspector import Inspector
from sanic.worker.manager import WorkerManager
if TYPE_CHECKING:
if TYPE_CHECKING: # no cov
try:
from sanic_ext import Extend # type: ignore
from sanic_ext.extensions.base import Extension # type: ignore
except ImportError:
Extend = TypeVar("Extend", Type) # type: ignore
Extend = TypeVar("Extend") # type: ignore
if OS_IS_WINDOWS: # no cov
enable_windows_color_support()
filterwarnings("once", category=DeprecationWarning)
class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
"""
The main application instance
"""
@@ -129,8 +130,6 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
"_future_routes",
"_future_signals",
"_future_statics",
"_inspector",
"_manager",
"_state",
"_task_registry",
"_test_client",
@@ -142,14 +141,12 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
"error_handler",
"go_fast",
"listeners",
"multiplexer",
"named_request_middleware",
"named_response_middleware",
"request_class",
"request_middleware",
"response_middleware",
"router",
"shared_ctx",
"signal_router",
"sock",
"strict_slashes",
@@ -158,6 +155,7 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
)
_app_registry: Dict[str, "Sanic"] = {}
_uvloop_setting = None # TODO: Remove in v22.6
test_mode = False
def __init__(
@@ -173,11 +171,11 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
strict_slashes: bool = False,
log_config: Optional[Dict[str, Any]] = None,
configure_logging: bool = True,
register: Optional[bool] = None,
dumps: Optional[Callable[..., AnyStr]] = None,
loads: Optional[Callable[..., Any]] = None,
inspector: bool = False,
) -> None:
super().__init__(name=name)
# logging
if configure_logging:
dict_config = log_config or LOGGING_CONFIG_DEFAULTS
@@ -191,16 +189,12 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
# First setup config
self.config: Config = config or Config(env_prefix=env_prefix)
if inspector:
self.config.INSPECTOR = inspector
# Then we can do the rest
self._asgi_client: Any = None
self._blueprint_order: List[Blueprint] = []
self._delayed_tasks: List[str] = []
self._future_registry: FutureRegistry = FutureRegistry()
self._inspector: Optional[Inspector] = None
self._manager: Optional[WorkerManager] = None
self._state: ApplicationState = ApplicationState(app=self)
self._task_registry: Dict[str, Task] = {}
self._test_client: Any = None
@@ -218,7 +212,6 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
self.request_middleware: Deque[MiddlewareType] = deque()
self.response_middleware: Deque[MiddlewareType] = deque()
self.router: Router = router or Router()
self.shared_ctx: SharedContext = SharedContext()
self.signal_router: SignalRouter = signal_router or SignalRouter()
self.sock: Optional[socket] = None
self.strict_slashes: bool = strict_slashes
@@ -227,14 +220,23 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
# Register alternative method names
self.go_fast = self.run
if register is not None:
deprecation(
"The register argument is deprecated and will stop working "
"in v22.6. After v22.6 all apps will be added to the Sanic "
"app registry.",
22.6,
)
self.config.REGISTER = register
if self.config.REGISTER:
self.__class__.register_app(self)
self.router.ctx.app = self
self.signal_router.ctx.app = self
self.__class__.register_app(self)
if dumps:
BaseHTTPResponse._dumps = dumps # type: ignore
if loads:
Request._loads = loads # type: ignore
@property
def loop(self):
@@ -252,7 +254,7 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
)
try:
return get_running_loop()
except RuntimeError: # no cov
except RuntimeError:
if sys.version_info > (3, 10):
return asyncio.get_event_loop_policy().get_event_loop()
else:
@@ -279,7 +281,7 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
valid = ", ".join(
map(lambda x: x.lower(), ListenerEvent.__members__.keys())
)
raise BadRequest(f"Invalid event: {event}. Use one of: {valid}")
raise InvalidUsage(f"Invalid event: {event}. Use one of: {valid}")
if "." in _event:
self.signal(_event.value)(
@@ -393,8 +395,8 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
routes = [routes]
for r in routes:
r.extra.websocket = websocket
r.extra.static = params.get("static", False)
r.ctx.websocket = websocket
r.ctx.static = params.get("static", False)
r.ctx.__dict__.update(ctx)
return routes
@@ -467,7 +469,9 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
def blueprint(
self,
blueprint: Union[Blueprint, Iterable[Blueprint], BlueprintGroup],
blueprint: Union[
Blueprint, List[Blueprint], Tuple[Blueprint], BlueprintGroup
],
**options: Any,
):
"""Register a blueprint on the application.
@@ -476,20 +480,21 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
:param options: option dictionary with blueprint defaults
:return: Nothing
"""
if isinstance(blueprint, (Iterable, BlueprintGroup)):
if isinstance(blueprint, (list, tuple, BlueprintGroup)):
for item in blueprint:
params = {**options}
if isinstance(blueprint, BlueprintGroup):
merge_from = [
options.get("url_prefix", ""),
blueprint.url_prefix or "",
]
if not isinstance(item, BlueprintGroup):
merge_from.append(item.url_prefix or "")
merged_prefix = "/".join(
u.strip("/") for u in merge_from if u
).rstrip("/")
params["url_prefix"] = f"/{merged_prefix}"
if blueprint.url_prefix:
merge_from = [
options.get("url_prefix", ""),
blueprint.url_prefix,
]
if not isinstance(item, BlueprintGroup):
merge_from.append(item.url_prefix or "")
merged_prefix = "/".join(
u.strip("/") for u in merge_from
).rstrip("/")
params["url_prefix"] = f"/{merged_prefix}"
for _attr in ["version", "strict_slashes"]:
if getattr(item, _attr) is None:
@@ -587,7 +592,7 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
uri = route.path
if getattr(route.extra, "static", None):
if getattr(route.ctx, "static", None):
filename = kwargs.pop("filename", "")
# it's static folder
if "__file_uri__" in uri:
@@ -620,18 +625,18 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
host = kwargs.pop("_host", None)
external = kwargs.pop("_external", False) or bool(host)
scheme = kwargs.pop("_scheme", "")
if route.extra.hosts and external:
if not host and len(route.extra.hosts) > 1:
if route.ctx.hosts and external:
if not host and len(route.ctx.hosts) > 1:
raise ValueError(
f"Host is ambiguous: {', '.join(route.extra.hosts)}"
f"Host is ambiguous: {', '.join(route.ctx.hosts)}"
)
elif host and host not in route.extra.hosts:
elif host and host not in route.ctx.hosts:
raise ValueError(
f"Requested host ({host}) is not available for this "
f"route: {route.extra.hosts}"
f"route: {route.ctx.hosts}"
)
elif not host:
host = list(route.extra.hosts)[0]
host = list(route.ctx.hosts)[0]
if scheme and not external:
raise ValueError("When specifying _scheme, _external must be True")
@@ -707,10 +712,7 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
# -------------------------------------------------------------------- #
async def handle_exception(
self,
request: Request,
exception: BaseException,
run_middleware: bool = True,
self, request: Request, exception: BaseException
): # no cov
"""
A handler that catches specific exceptions and outputs a response.
@@ -719,7 +721,6 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
:param exception: The exception that was raised
:raises ServerError: response 500
"""
response = None
await self.dispatch(
"http.lifecycle.exception",
inline=True,
@@ -737,34 +738,45 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
"has at least partially been sent."
)
# ----------------- deprecated -----------------
handler = self.error_handler._lookup(
exception, request.name if request else None
)
if handler:
logger.warning(
deprecation(
"An error occurred while handling the request after at "
"least some part of the response was sent to the client. "
"The response from your custom exception handler "
f"{handler.__name__} will not be sent to the client."
"Exception handlers should only be used to generate the "
"exception responses. If you would like to perform any "
"other action on a raised exception, consider using a "
"Therefore, the response from your custom exception "
f"handler {handler.__name__} will not be sent to the "
"client. Beginning in v22.6, Sanic will stop executing "
"custom exception handlers in this scenario. Exception "
"handlers should only be used to generate the exception "
"responses. If you would like to perform any other "
"action on a raised exception, please consider using a "
"signal handler like "
'`@app.signal("http.lifecycle.exception")`\n'
"For further information, please see the docs: "
"https://sanicframework.org/en/guide/advanced/"
"signals.html",
22.6,
)
try:
response = self.error_handler.response(request, exception)
if isawaitable(response):
response = await response
except BaseException as e:
logger.error("An error occurred in the exception handler.")
error_logger.exception(e)
# ----------------------------------------------
return
# -------------------------------------------- #
# Request Middleware
# -------------------------------------------- #
if run_middleware:
middleware = (
request.route and request.route.extra.request_middleware
) or self.request_middleware
response = await self._run_request_middleware(request, middleware)
response = await self._run_request_middleware(
request, request_name=None
)
# No middleware results
if not response:
try:
@@ -844,13 +856,7 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
# Define `response` var here to remove warnings about
# allocation before assignment below.
response: Optional[
Union[
BaseHTTPResponse,
Coroutine[Any, Any, Optional[BaseHTTPResponse]],
]
] = None
run_middleware = True
response = None
try:
await self.dispatch(
@@ -882,7 +888,7 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
if (
request.stream
and request.stream.request_body
and not route.extra.ignore_body
and not route.ctx.ignore_body
):
if hasattr(handler, "is_stream"):
@@ -895,11 +901,9 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
# -------------------------------------------- #
# Request Middleware
# -------------------------------------------- #
run_middleware = False
if request.route.extra.request_middleware:
response = await self._run_request_middleware(
request, request.route.extra.request_middleware
)
response = await self._run_request_middleware(
request, request_name=route.name
)
# No middleware results
if not response:
@@ -916,19 +920,9 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
)
# Run response handler
await self.dispatch(
"http.handler.before",
inline=True,
context={"request": request},
)
response = handler(request, **request.match_info)
if isawaitable(response):
response = await response
await self.dispatch(
"http.handler.after",
inline=True,
context={"request": request},
)
if request.responded:
if response is not None:
@@ -940,7 +934,7 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
if request.stream is not None:
response = request.stream.response
elif response is not None:
response = await request.respond(response) # type: ignore
response = await request.respond(response)
elif not hasattr(handler, "is_websocket"):
response = request.stream.response # type: ignore
@@ -955,10 +949,9 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
"response": response,
},
)
...
await response.send(end_stream=True)
elif isinstance(response, ResponseStream):
resp = await response(request) # type: ignore
resp = await response(request)
await self.dispatch(
"http.lifecycle.response",
inline=True,
@@ -967,7 +960,7 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
"response": resp,
},
)
await response.eof() # type: ignore
await response.eof()
else:
if not hasattr(handler, "is_websocket"):
raise ServerError(
@@ -979,9 +972,7 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
raise
except Exception as e:
# Response Generation Failed
await self.handle_exception(
request, e, run_middleware=run_middleware
)
await self.handle_exception(request, e)
async def _websocket_handler(
self, handler, request, *args, subprotocols=None, **kwargs
@@ -1001,10 +992,10 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
cancelled = False
try:
await fut
except (CancelledError, ConnectionClosed):
cancelled = True
except Exception as e:
self.error_handler.log(request, e)
except (CancelledError, ConnectionClosed):
cancelled = True
finally:
self.websocket_tasks.remove(fut)
if cancelled:
@@ -1031,7 +1022,7 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
def asgi_client(self): # noqa
"""
A testing client that uses ASGI to reach into the application to
execute handlers.
execute hanlers.
:return: testing client
:rtype: :class:`SanicASGITestClient`
@@ -1050,72 +1041,86 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
# -------------------------------------------------------------------- #
async def _run_request_middleware(
self, request, middleware_collection
self, request, request_name=None
): # no cov
request._request_middleware_started = True
# The if improves speed. I don't know why
named_middleware = self.named_request_middleware.get(
request_name, deque()
)
applicable_middleware = self.request_middleware + named_middleware
for middleware in middleware_collection:
await self.dispatch(
"http.middleware.before",
inline=True,
context={
"request": request,
"response": None,
},
condition={"attach_to": "request"},
)
# request.request_middleware_started is meant as a stop-gap solution
# until RFC 1630 is adopted
if applicable_middleware and not request.request_middleware_started:
request.request_middleware_started = True
response = middleware(request)
if isawaitable(response):
response = await response
for middleware in applicable_middleware:
await self.dispatch(
"http.middleware.before",
inline=True,
context={
"request": request,
"response": None,
},
condition={"attach_to": "request"},
)
await self.dispatch(
"http.middleware.after",
inline=True,
context={
"request": request,
"response": None,
},
condition={"attach_to": "request"},
)
response = middleware(request)
if isawaitable(response):
response = await response
if response:
return response
await self.dispatch(
"http.middleware.after",
inline=True,
context={
"request": request,
"response": None,
},
condition={"attach_to": "request"},
)
if response:
return response
return None
async def _run_response_middleware(
self, request, response, middleware_collection
self, request, response, request_name=None
): # no cov
for middleware in middleware_collection:
await self.dispatch(
"http.middleware.before",
inline=True,
context={
"request": request,
"response": response,
},
condition={"attach_to": "response"},
)
named_middleware = self.named_response_middleware.get(
request_name, deque()
)
applicable_middleware = self.response_middleware + named_middleware
if applicable_middleware:
for middleware in applicable_middleware:
await self.dispatch(
"http.middleware.before",
inline=True,
context={
"request": request,
"response": response,
},
condition={"attach_to": "response"},
)
_response = middleware(request, response)
if isawaitable(_response):
_response = await _response
_response = middleware(request, response)
if isawaitable(_response):
_response = await _response
await self.dispatch(
"http.middleware.after",
inline=True,
context={
"request": request,
"response": _response if _response else response,
},
condition={"attach_to": "response"},
)
await self.dispatch(
"http.middleware.after",
inline=True,
context={
"request": request,
"response": _response if _response else response,
},
condition={"attach_to": "response"},
)
if _response:
response = _response
if isinstance(response, BaseHTTPResponse):
response = request.stream.respond(response)
break
if _response:
response = _response
if isinstance(response, BaseHTTPResponse):
response = request.stream.respond(response)
break
return response
def _build_endpoint_name(self, *parts):
@@ -1202,7 +1207,7 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
*,
name: Optional[str] = None,
register: bool = True,
) -> Optional[Task[Any]]:
) -> Optional[Task]:
"""
Schedule a task to run later, after the loop has started.
Different from asyncio.ensure_future in that it does not
@@ -1212,7 +1217,7 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
`See user guide re: background tasks
<https://sanicframework.org/guide/basics/tasks.html#background-tasks>`__
:param task: future, coroutine or awaitable
:param task: future, couroutine or awaitable
"""
try:
loop = self.loop # Will raise SanicError if loop is not started
@@ -1333,7 +1338,7 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
self.config.update_config(config)
@property
def asgi(self) -> bool:
def asgi(self):
return self.state.asgi
@asgi.setter
@@ -1344,6 +1349,18 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
def debug(self):
return self.state.is_debug
@debug.setter
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
self.state.mode = mode
@property
def auto_reload(self):
return self.config.AUTO_RELOAD
@@ -1351,15 +1368,63 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
@auto_reload.setter
def auto_reload(self, value: bool):
self.config.AUTO_RELOAD = value
self.state.auto_reload = value
@property
def state(self) -> ApplicationState: # type: ignore
"""
:return: The application state
"""
def state(self):
return self._state
@property
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
@is_running.setter
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
@property
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
@is_stopping.setter
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
@property
def reload_dirs(self):
return self.state.reload_dirs
@@ -1417,18 +1482,6 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
cls._app_registry[name] = app
@classmethod
def unregister_app(cls, app: "Sanic") -> None:
"""
Unregister a Sanic instance
"""
if not isinstance(app, cls):
raise SanicException("Registered app must be an instance of Sanic")
name = app.name
if name in cls._app_registry:
del cls._app_registry[name]
@classmethod
def get_app(
cls, name: Optional[str] = None, *, force_create: bool = False
@@ -1448,28 +1501,9 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
try:
return cls._app_registry[name]
except KeyError:
if name == "__main__":
return cls.get_app("__mp_main__", force_create=force_create)
if force_create:
return cls(name)
raise SanicException(
f"Sanic app name '{name}' not found.\n"
"App instantiation must occur outside "
"if __name__ == '__main__' "
"block or by using an AppLoader.\nSee "
"https://sanic.dev/en/guide/deployment/app-loader.html"
" for more details."
)
@classmethod
def _check_uvloop_conflict(cls) -> None:
values = {app.config.USE_UVLOOP for app in cls._app_registry.values()}
if len(values) > 1:
error_logger.warning(
"It looks like you're running several apps with different "
"uvloop settings. This is not supported and may lead to "
"unintended behaviour."
)
raise SanicException(f'Sanic app name "{name}" not found.')
# -------------------------------------------------------------------- #
# Lifecycle
@@ -1481,7 +1515,6 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
except FinalizationError as e:
if not Sanic.test_mode:
raise e
self.finalize_middleware()
def signalize(self, allow_fail_builtin=True):
self.signal_router.allow_fail_builtin = allow_fail_builtin
@@ -1499,41 +1532,35 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
if hasattr(self, "_ext"):
self.ext._display()
if self.state.is_debug and self.config.TOUCHUP is not True:
if self.state.is_debug:
self.config.TOUCHUP = False
elif isinstance(self.config.TOUCHUP, Default):
self.config.TOUCHUP = True
# Setup routers
self.signalize(self.config.TOUCHUP)
self.finalize()
route_names = [route.name for route in self.router.routes]
duplicates = {
name for name in route_names if route_names.count(name) > 1
}
if duplicates:
names = ", ".join(duplicates)
deprecation(
f"Duplicate route names detected: {names}. In the future, "
"Sanic will enforce uniqueness in route naming.",
23.3,
# TODO: Replace in v22.6 to check against apps in app registry
if (
self.__class__._uvloop_setting is not None
and self.__class__._uvloop_setting != self.config.USE_UVLOOP
):
error_logger.warning(
"It looks like you're running several apps with different "
"uvloop settings. This is not supported and may lead to "
"unintended behaviour."
)
Sanic._check_uvloop_conflict()
self.__class__._uvloop_setting = self.config.USE_UVLOOP
# Startup time optimizations
if self.state.primary:
# 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
if hasattr(self, "multiplexer"):
self.multiplexer.ack()
async def _server_event(
self,
concern: str,
@@ -1546,9 +1573,8 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
"shutdown",
):
raise SanicException(f"Invalid server event: {event}")
logger.debug(
f"Triggering server events: {event}", extra={"verbosity": 1}
)
if self.state.verbosity >= 1:
logger.debug(f"Triggering server events: {event}")
reverse = concern == "shutdown"
if loop is None:
loop = self.loop
@@ -1562,43 +1588,3 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
"loop": loop,
},
)
# -------------------------------------------------------------------- #
# Process Management
# -------------------------------------------------------------------- #
def refresh(
self,
passthru: Optional[Dict[str, Any]] = None,
):
registered = self.__class__.get_app(self.name)
if self is not registered:
if not registered.state.server_info:
registered.state.server_info = self.state.server_info
self = registered
if passthru:
for attr, info in passthru.items():
if isinstance(info, dict):
for key, value in info.items():
setattr(getattr(self, attr), key, value)
else:
setattr(self, attr, info)
if hasattr(self, "multiplexer"):
self.shared_ctx.lock()
return self
@property
def inspector(self):
if environ.get("SANIC_WORKER_PROCESS") or not self._inspector:
raise SanicException(
"Can only access the inspector from the main process"
)
return self._inspector
@property
def manager(self):
if environ.get("SANIC_WORKER_PROCESS") or not self._manager:
raise SanicException(
"Can only access the manager from the main process"
)
return self._manager

View File

@@ -1,32 +0,0 @@
from enum import Enum, IntEnum, auto
class StrEnum(str, Enum): # no cov
def _generate_next_value_(name: str, *args) -> str: # type: ignore
return name.lower()
def __eq__(self, value: object) -> bool:
value = str(value).upper()
return super().__eq__(value)
def __hash__(self) -> int:
return hash(self.value)
def __str__(self) -> str:
return self.value
class Server(StrEnum):
SANIC = auto()
ASGI = auto()
class Mode(StrEnum):
PRODUCTION = auto()
DEBUG = auto()
class ServerStage(IntEnum):
STOPPED = auto()
PARTIAL = auto()
SERVING = auto()

View File

@@ -5,9 +5,14 @@ from importlib import import_module
from typing import TYPE_CHECKING
if TYPE_CHECKING:
if TYPE_CHECKING: # no cov
from sanic import Sanic
try:
from sanic_ext import Extend # type: ignore
except ImportError:
...
def setup_ext(app: Sanic, *, fail: bool = False, **kwargs):
if not app.config.AUTO_EXTEND:
@@ -17,7 +22,7 @@ def setup_ext(app: Sanic, *, fail: bool = False, **kwargs):
with suppress(ModuleNotFoundError):
sanic_ext = import_module("sanic_ext")
if not sanic_ext: # no cov
if not sanic_ext:
if fail:
raise RuntimeError(
"Sanic Extensions is not installed. You can add it to your "
@@ -28,7 +33,7 @@ def setup_ext(app: Sanic, *, fail: bool = False, **kwargs):
return
if not getattr(app, "_ext", None):
Ext = getattr(sanic_ext, "Extend")
Ext: Extend = getattr(sanic_ext, "Extend")
app._ext = Ext(app, **kwargs)
return app.ext

View File

@@ -3,8 +3,6 @@ import sys
from os import environ
from sanic.compat import is_atty
BASE_LOGO = """
@@ -46,7 +44,7 @@ ansi_pattern = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
def get_logo(full=False, coffee=False):
logo = (
(FULL_COLOR_LOGO if full else (COFFEE_LOGO if coffee else COLOR_LOGO))
if is_atty()
if sys.stdout.isatty()
else BASE_LOGO
)

View File

@@ -1,10 +1,11 @@
import sys
from abc import ABC, abstractmethod
from shutil import get_terminal_size
from textwrap import indent, wrap
from typing import Dict, Optional
from sanic import __version__
from sanic.compat import is_atty
from sanic.log import logger
@@ -35,7 +36,7 @@ class MOTD(ABC):
data: Dict[str, str],
extra: Dict[str, str],
) -> None:
motd_class = MOTDTTY if is_atty() else MOTDBasic
motd_class = MOTDTTY if sys.stdout.isatty() else MOTDBasic
motd_class(logo, serve_location, data, extra).display()
@@ -80,23 +81,20 @@ class MOTDTTY(MOTD):
)
self.display_length = self.key_width + self.value_width + 2
def display(self, version=True, action="Goin' Fast", out=None):
if not out:
out = logger.info
header = "Sanic"
if version:
header += f" v{__version__}"
header = header.center(self.centering_length)
def display(self):
version = f"Sanic v{__version__}".center(self.centering_length)
running = (
f"{action} @ {self.serve_location}" if self.serve_location else ""
f"Goin' Fast @ {self.serve_location}"
if self.serve_location
else ""
).center(self.centering_length)
length = len(header) + 2 - self.logo_line_length
length = len(version) + 2 - self.logo_line_length
first_filler = "" * (self.logo_line_length - 1)
second_filler = "" * length
display_filler = "" * (self.display_length + 2)
lines = [
f"\n{first_filler}{second_filler}",
f"{header}",
f"{version}",
f"{running}",
f"{first_filler}{second_filler}",
]
@@ -110,7 +108,7 @@ class MOTDTTY(MOTD):
self._render_fill(lines)
lines.append(f"{first_filler}{second_filler}\n")
out(indent("\n".join(lines), " "))
logger.info(indent("\n".join(lines), " "))
def _render_data(self, lines, data, start):
offset = 0

View File

@@ -1,86 +0,0 @@
import os
import sys
import time
from contextlib import contextmanager
from queue import Queue
from threading import Thread
if os.name == "nt": # noqa
import ctypes # noqa
class _CursorInfo(ctypes.Structure):
_fields_ = [("size", ctypes.c_int), ("visible", ctypes.c_byte)]
class Spinner: # noqa
def __init__(self, message: str) -> None:
self.message = message
self.queue: Queue[int] = Queue()
self.spinner = self.cursor()
self.thread = Thread(target=self.run)
def start(self):
self.queue.put(1)
self.thread.start()
self.hide()
def run(self):
while self.queue.get():
output = f"\r{self.message} [{next(self.spinner)}]"
sys.stdout.write(output)
sys.stdout.flush()
time.sleep(0.1)
self.queue.put(1)
def stop(self):
self.queue.put(0)
self.thread.join()
self.show()
@staticmethod
def cursor():
while True:
for cursor in "|/-\\":
yield cursor
@staticmethod
def hide():
if os.name == "nt":
ci = _CursorInfo()
handle = ctypes.windll.kernel32.GetStdHandle(-11)
ctypes.windll.kernel32.GetConsoleCursorInfo(
handle, ctypes.byref(ci)
)
ci.visible = False
ctypes.windll.kernel32.SetConsoleCursorInfo(
handle, ctypes.byref(ci)
)
elif os.name == "posix":
sys.stdout.write("\033[?25l")
sys.stdout.flush()
@staticmethod
def show():
if os.name == "nt":
ci = _CursorInfo()
handle = ctypes.windll.kernel32.GetStdHandle(-11)
ctypes.windll.kernel32.GetConsoleCursorInfo(
handle, ctypes.byref(ci)
)
ci.visible = True
ctypes.windll.kernel32.SetConsoleCursorInfo(
handle, ctypes.byref(ci)
)
elif os.name == "posix":
sys.stdout.write("\033[?25h")
sys.stdout.flush()
@contextmanager
def loading(message: str = "Loading"): # noqa
spinner = Spinner(message)
spinner.start()
yield
spinner.stop()

View File

@@ -3,20 +3,42 @@ from __future__ import annotations
import logging
from dataclasses import dataclass, field
from enum import Enum, IntEnum, auto
from pathlib import Path
from socket import socket
from ssl import SSLContext
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Union
from sanic.application.constants import Mode, Server, ServerStage
from sanic.log import VerbosityFilter, 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
class StrEnum(str, Enum):
def _generate_next_value_(name: str, *args) -> str: # type: ignore
return name.lower()
class Server(StrEnum):
SANIC = auto()
ASGI = auto()
GUNICORN = auto()
class Mode(StrEnum):
PRODUCTION = auto()
DEBUG = auto()
class ServerStage(IntEnum):
STOPPED = auto()
PARTIAL = auto()
SERVING = auto()
@dataclass
class ApplicationServerInfo:
settings: Dict[str, Any]
@@ -69,9 +91,6 @@ class ApplicationState:
if getattr(self.app, "configure_logging", False) and self.app.debug:
logger.setLevel(logging.DEBUG)
def set_verbosity(self, value: int):
VerbosityFilter.verbosity = value
@property
def is_debug(self):
return self.mode is Mode.DEBUG

View File

@@ -7,7 +7,7 @@ from urllib.parse import quote
from sanic.compat import Header
from sanic.exceptions import ServerError
from sanic.helpers import Default
from sanic.helpers import _default
from sanic.http import Stage
from sanic.log import logger
from sanic.models.asgi import ASGIReceive, ASGIScope, ASGISend, MockTransport
@@ -17,7 +17,7 @@ from sanic.server import ConnInfo
from sanic.server.websockets.connection import WebSocketConnection
if TYPE_CHECKING:
if TYPE_CHECKING: # no cov
from sanic import Sanic
@@ -25,28 +25,27 @@ class Lifespan:
def __init__(self, asgi_app: ASGIApp) -> None:
self.asgi_app = asgi_app
if (
"server.init.before"
in self.asgi_app.sanic_app.signal_router.name_index
):
logger.debug(
'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.",
extra={"verbosity": 1},
)
if (
"server.shutdown.after"
in self.asgi_app.sanic_app.signal_router.name_index
):
logger.debug(
'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.",
extra={"verbosity": 1},
)
if self.asgi_app.sanic_app.state.verbosity > 0:
if (
"server.init.before"
in self.asgi_app.sanic_app.signal_router.name_index
):
logger.debug(
'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."
)
if (
"server.shutdown.after"
in self.asgi_app.sanic_app.signal_router.name_index
):
logger.debug(
'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."
)
async def startup(self) -> None:
"""
@@ -61,7 +60,7 @@ class Lifespan:
await self.asgi_app.sanic_app._server_event("init", "before")
await self.asgi_app.sanic_app._server_event("init", "after")
if not isinstance(self.asgi_app.sanic_app.config.USE_UVLOOP, Default):
if self.asgi_app.sanic_app.config.USE_UVLOOP is not _default:
warnings.warn(
"You have set the USE_UVLOOP configuration option, but Sanic "
"cannot control the event loop when running in ASGI mode."
@@ -164,13 +163,6 @@ class ASGIApp:
instance.request_body = True
instance.request.conn_info = ConnInfo(instance.transport)
await sanic_app.dispatch(
"http.lifecycle.request",
inline=True,
context={"request": instance.request},
fail_not_found=False,
)
return instance
async def read(self) -> Optional[bytes]:
@@ -234,7 +226,4 @@ class ASGIApp:
self.stage = Stage.HANDLER
await self.sanic_app.handle_request(self.request)
except Exception as e:
try:
await self.sanic_app.handle_exception(self.request, e)
except Exception as exc:
await self.sanic_app.handle_exception(self.request, exc, False)
await self.sanic_app.handle_exception(self.request, e)

View File

@@ -5,7 +5,7 @@ from functools import partial
from typing import TYPE_CHECKING, List, Optional, Union
if TYPE_CHECKING:
if TYPE_CHECKING: # no cov
from sanic.blueprints import Blueprint

View File

@@ -21,8 +21,8 @@ from typing import (
Union,
)
from sanic_routing.exceptions import NotFound
from sanic_routing.route import Route
from sanic_routing.exceptions import NotFound # type: ignore
from sanic_routing.route import Route # type: ignore
from sanic.base.root import BaseSanic
from sanic.blueprint_group import BlueprintGroup
@@ -36,7 +36,7 @@ from sanic.models.handler_types import (
)
if TYPE_CHECKING:
if TYPE_CHECKING: # no cov
from sanic import Sanic
@@ -308,7 +308,7 @@ class Blueprint(BaseSanic):
# prefixed properly in the router
future.handler.__blueprintname__ = self.name
# Prepend the blueprint URI prefix if available
uri = self._setup_uri(future.uri, url_prefix)
uri = url_prefix + future.uri if url_prefix else future.uri
version_prefix = self.version_prefix
for prefix in (
@@ -333,7 +333,7 @@ class Blueprint(BaseSanic):
apply_route = FutureRoute(
future.handler,
uri,
uri[1:] if uri.startswith("//") else uri,
future.methods,
host,
strict_slashes,
@@ -363,7 +363,7 @@ class Blueprint(BaseSanic):
# Static Files
for future in self._future_statics:
# Prepend the blueprint URI prefix if available
uri = self._setup_uri(future.uri, url_prefix)
uri = url_prefix + future.uri if url_prefix else future.uri
apply_route = FutureStatic(uri, *future[1:])
if (self, apply_route) in app._future_registry:
@@ -406,7 +406,7 @@ class Blueprint(BaseSanic):
self.routes += [route for route in routes if isinstance(route, Route)]
self.websocket_routes += [
route for route in self.routes if route.extra.websocket
route for route in self.routes if route.ctx.websocket
]
self.middlewares += middleware
self.exceptions += exception_handlers
@@ -456,18 +456,6 @@ class Blueprint(BaseSanic):
break
return value
@staticmethod
def _setup_uri(base: str, prefix: Optional[str]):
uri = base
if prefix:
uri = prefix
if base.startswith("/") and prefix.endswith("/"):
uri += base[1:]
else:
uri += base
return uri[1:] if uri.startswith("//") else uri
@staticmethod
def register_futures(
apps: Set[Sanic], bp: Blueprint, futures: Sequence[Tuple[Any, ...]]

View File

@@ -1,10 +1,10 @@
import logging
import os
import shutil
import sys
from argparse import ArgumentParser, RawTextHelpFormatter
from functools import partial
from importlib import import_module
from pathlib import Path
from textwrap import indent
from typing import Any, List, Union
@@ -12,8 +12,7 @@ from sanic.app import Sanic
from sanic.application.logo import get_logo
from sanic.cli.arguments import Group
from sanic.log import error_logger
from sanic.worker.inspector import inspect
from sanic.worker.loader import AppLoader
from sanic.simple import create_simple_server
class SanicArgumentParser(ArgumentParser):
@@ -59,25 +58,18 @@ Or, a path to a directory to run as a simple HTTP server:
os.environ.get("SANIC_RELOADER_PROCESS", "") != "true"
)
self.args: List[Any] = []
self.groups: List[Group] = []
def attach(self):
for group in Group._registry:
instance = group.create(self.parser)
instance.attach()
self.groups.append(instance)
group.create(self.parser).attach()
def run(self):
# This is to provide backwards compat -v to display version
legacy_version = len(sys.argv) == 2 and sys.argv[-1] == "-v"
parse_args = ["--version"] if legacy_version else None
def run(self, parse_args=None):
legacy_version = False
if not parse_args:
# This is to provide backwards compat -v to display version
legacy_version = len(sys.argv) == 2 and sys.argv[-1] == "-v"
parse_args = ["--version"] if legacy_version else None
elif parse_args == ["-v"]:
parse_args = ["--version"]
if not legacy_version:
parsed, unknown = self.parser.parse_known_args(args=parse_args)
parsed, unknown = self.parser.parse_known_args()
if unknown and parsed.factory:
for arg in unknown:
if arg.startswith("--"):
@@ -85,47 +77,16 @@ Or, a path to a directory to run as a simple HTTP server:
self.args = self.parser.parse_args(args=parse_args)
self._precheck()
app_loader = AppLoader(
self.args.module,
self.args.factory,
self.args.simple,
self.args,
)
try:
app = self._get_app(app_loader)
app = self._get_app()
kwargs = self._build_run_kwargs()
except ValueError as e:
error_logger.exception(f"Failed to run app: {e}")
else:
if self.args.inspect or self.args.inspect_raw or self.args.trigger:
os.environ["SANIC_IGNORE_PRODUCTION_WARNING"] = "true"
else:
for http_version in self.args.http:
app.prepare(**kwargs, version=http_version)
if self.args.inspect or self.args.inspect_raw or self.args.trigger:
action = self.args.trigger or (
"raw" if self.args.inspect_raw else "pretty"
)
inspect(
app.config.INSPECTOR_HOST,
app.config.INSPECTOR_PORT,
action,
)
del os.environ["SANIC_IGNORE_PRODUCTION_WARNING"]
return
if self.args.single:
serve = Sanic.serve_single
elif self.args.legacy:
serve = Sanic.serve_legacy
else:
serve = partial(Sanic.serve, app_loader=app_loader)
serve(app)
app.run(**kwargs)
except ValueError:
error_logger.exception("Failed to run app")
def _precheck(self):
# Custom TLS mismatch handling for better diagnostics
# # Custom TLS mismatch handling for better diagnostics
if self.main_process and (
# one of cert/key missing
bool(self.args.cert) != bool(self.args.key)
@@ -145,27 +106,68 @@ Or, a path to a directory to run as a simple HTTP server:
)
error_logger.error(message)
sys.exit(1)
if self.args.inspect or self.args.inspect_raw:
logging.disable(logging.CRITICAL)
def _get_app(self, app_loader: AppLoader):
def _get_app(self):
try:
app = app_loader.load()
module_path = os.path.abspath(os.getcwd())
if module_path not in sys.path:
sys.path.append(module_path)
if self.args.simple:
path = Path(self.args.module)
app = create_simple_server(path)
else:
delimiter = ":" if ":" in self.args.module else "."
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("()"):
self.args.factory = True
app_name = app_name[:-2]
module = import_module(module_name)
app = getattr(module, app_name, None)
if self.args.factory:
try:
app = app(self.args)
except TypeError:
app = app()
app_type_name = type(app).__name__
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(
f"Module is not a Sanic app, it is a {app_type_name}\n"
f" Perhaps you meant {self.args.module}:app?"
)
except ImportError as e:
if app_loader.module_name.startswith(e.name): # type: ignore
if module_name.startswith(e.name):
error_logger.error(
f"No module named {e.name} found.\n"
" Example File: project/sanic_server.py -> app\n"
" Example Module: project.sanic_server.app"
)
sys.exit(1)
else:
raise e
return app
def _build_run_kwargs(self):
for group in self.groups:
group.prepare(self.args)
ssl: Union[None, dict, str, list] = []
if self.args.tlshost:
ssl.append(None)
@@ -178,10 +180,8 @@ Or, a path to a directory to run as a simple HTTP server:
elif len(ssl) == 1 and ssl[0] is not None:
# Use only one cert, no TLSSelector.
ssl = ssl[0]
kwargs = {
"access_log": self.args.access_log,
"coffee": self.args.coffee,
"debug": self.args.debug,
"fast": self.args.fast,
"host": self.args.host,
@@ -192,9 +192,6 @@ Or, a path to a directory to run as a simple HTTP server:
"unix": self.args.unix,
"verbosity": self.args.verbosity or 0,
"workers": self.args.workers,
"auto_tls": self.args.auto_tls,
"single_process": self.args.single,
"legacy": self.args.legacy,
}
for maybe_arg in ("auto_reload", "dev"):
@@ -204,5 +201,4 @@ Or, a path to a directory to run as a simple HTTP server:
if self.args.path:
kwargs["auto_reload"] = True
kwargs["reload_dir"] = self.args.path
return kwargs

View File

@@ -3,10 +3,9 @@ from __future__ import annotations
from argparse import ArgumentParser, _ArgumentGroup
from typing import List, Optional, Type, Union
from sanic_routing import __version__ as __routing_version__
from sanic_routing import __version__ as __routing_version__ # type: ignore
from sanic import __version__
from sanic.http.constants import HTTP
class Group:
@@ -30,7 +29,7 @@ class Group:
instance = cls(parser, cls.name)
return instance
def add_bool_arguments(self, *args, nullable=False, **kwargs):
def add_bool_arguments(self, *args, **kwargs):
group = self.container.add_mutually_exclusive_group()
kwargs["help"] = kwargs["help"].capitalize()
group.add_argument(*args, action="store_true", **kwargs)
@@ -38,12 +37,6 @@ class Group:
group.add_argument(
"--no-" + args[0][2:], *args[1:], action="store_false", **kwargs
)
if nullable:
params = {args[0][2:].replace("-", "_"): None}
group.set_defaults(**params)
def prepare(self, args) -> None:
...
class GeneralGroup(Group):
@@ -70,8 +63,7 @@ class ApplicationGroup(Group):
name = "Application"
def attach(self):
group = self.container.add_mutually_exclusive_group()
group.add_argument(
self.container.add_argument(
"--factory",
action="store_true",
help=(
@@ -79,7 +71,7 @@ class ApplicationGroup(Group):
"i.e. a () -> <Sanic app> callable"
),
)
group.add_argument(
self.container.add_argument(
"-s",
"--simple",
dest="simple",
@@ -89,70 +81,6 @@ class ApplicationGroup(Group):
"a directory\n(module arg should be a path)"
),
)
group.add_argument(
"--inspect",
dest="inspect",
action="store_true",
help=("Inspect the state of a running instance, human readable"),
)
group.add_argument(
"--inspect-raw",
dest="inspect_raw",
action="store_true",
help=("Inspect the state of a running instance, JSON output"),
)
group.add_argument(
"--trigger-reload",
dest="trigger",
action="store_const",
const="reload",
help=("Trigger worker processes to reload"),
)
group.add_argument(
"--trigger-shutdown",
dest="trigger",
action="store_const",
const="shutdown",
help=("Trigger all processes to shutdown"),
)
class HTTPVersionGroup(Group):
name = "HTTP version"
def attach(self):
http_values = [http.value for http in HTTP.__members__.values()]
self.container.add_argument(
"--http",
dest="http",
action="append",
choices=http_values,
type=int,
help=(
"Which HTTP version to use: HTTP/1.1 or HTTP/3. Value should\n"
"be either 1, or 3. [default 1]"
),
)
self.container.add_argument(
"-1",
dest="http",
action="append_const",
const=1,
help=("Run Sanic server using HTTP/1.1"),
)
self.container.add_argument(
"-3",
dest="http",
action="append_const",
const=3,
help=("Run Sanic server using HTTP/3"),
)
def prepare(self, args):
if not args.http:
args.http = [1]
args.http = tuple(sorted(set(map(HTTP, args.http)), reverse=True))
class SocketGroup(Group):
@@ -164,6 +92,7 @@ class SocketGroup(Group):
"--host",
dest="host",
type=str,
default="127.0.0.1",
help="Host address [default 127.0.0.1]",
)
self.container.add_argument(
@@ -171,6 +100,7 @@ class SocketGroup(Group):
"--port",
dest="port",
type=int,
default=8000,
help="Port to serve on [default 8000]",
)
self.container.add_argument(
@@ -237,22 +167,8 @@ class WorkerGroup(Group):
action="store_true",
help="Set the number of workers to max allowed",
)
group.add_argument(
"--single-process",
dest="single",
action="store_true",
help="Do not use multiprocessing, run server in a single process",
)
self.container.add_argument(
"--legacy",
action="store_true",
help="Use the legacy server manager",
)
self.add_bool_arguments(
"--access-logs",
dest="access_log",
help="display access logs",
default=None,
"--access-logs", dest="access_log", help="display access logs"
)
@@ -264,7 +180,11 @@ class DevelopmentGroup(Group):
"--debug",
dest="debug",
action="store_true",
help="Run the server in debug mode",
help=(
"Run the server in DEBUG mode. It includes DEBUG logging,\n"
"additional context on exceptions, and other settings\n"
"not-safe for PRODUCTION, but helpful for debugging problems."
),
)
self.container.add_argument(
"-r",
@@ -289,16 +209,7 @@ class DevelopmentGroup(Group):
"--dev",
dest="dev",
action="store_true",
help=("debug + auto reload"),
)
self.container.add_argument(
"--auto-tls",
dest="auto_tls",
action="store_true",
help=(
"Create a temporary TLS certificate for local development "
"(requires mkcert or trustme)"
),
help=("debug + auto reload."),
)
@@ -306,12 +217,6 @@ class OutputGroup(Group):
name = "Output"
def attach(self):
self.add_bool_arguments(
"--coffee",
dest="coffee",
default=False,
help="Uhm, coffee?",
)
self.add_bool_arguments(
"--motd",
dest="motd",

View File

@@ -1,10 +1,8 @@
import asyncio
import os
import signal
import sys
from enum import Enum
from typing import Awaitable
from sys import argv
from multidict import CIMultiDict # type: ignore
@@ -20,31 +18,6 @@ except ImportError:
pass
# Python 3.11 changed the way Enum formatting works for mixed-in types.
if sys.version_info < (3, 11, 0):
class StrEnum(str, Enum):
pass
else:
from enum import StrEnum # type: ignore # noqa
class UpperStrEnum(StrEnum):
def _generate_next_value_(name, start, count, last_values):
return name.upper()
def __eq__(self, value: object) -> bool:
value = str(value).upper()
return super().__eq__(value)
def __hash__(self) -> int:
return hash(self.value)
def __str__(self) -> str:
return self.value
def enable_windows_color_support():
import ctypes
@@ -74,12 +47,12 @@ class Header(CIMultiDict):
return self.getall(key, default=[])
use_trio = sys.argv[0].endswith("hypercorn") and "trio" in sys.argv
use_trio = argv[0].endswith("hypercorn") and "trio" in argv
if use_trio: # pragma: no cover
import trio # type: ignore
def stat_async(path) -> Awaitable[os.stat_result]:
def stat_async(path):
return trio.Path(path).stat()
open_async = trio.open_file
@@ -116,7 +89,3 @@ def ctrlc_workaround_for_windows(app):
die = False
signal.signal(signal.SIGINT, ctrlc_handler)
app.add_task(stay_active)
def is_atty() -> bool:
return bool(sys.stdout and sys.stdout.isatty())

View File

@@ -1,77 +1,54 @@
from __future__ import annotations
import sys
from inspect import getmembers, isclass, isdatadescriptor
from os import environ
from pathlib import Path
from typing import Any, Callable, Dict, Optional, Sequence, Union
from warnings import filterwarnings
from sanic.constants import LocalCertCreator, RestartOrder
from sanic.errorpages import DEFAULT_FORMAT, check_error_format
from sanic.helpers import Default, _default
from sanic.http import Http
from sanic.log import error_logger
from sanic.log import deprecation, error_logger
from sanic.utils import load_module_from_file_location, str_to_bool
if sys.version_info >= (3, 8):
from typing import Literal
FilterWarningType = Union[
Literal["default"],
Literal["error"],
Literal["ignore"],
Literal["always"],
Literal["module"],
Literal["once"],
]
else:
FilterWarningType = str
SANIC_PREFIX = "SANIC_"
DEFAULT_CONFIG = {
"_FALLBACK_ERROR_FORMAT": _default,
"ACCESS_LOG": False,
"ACCESS_LOG": True,
"AUTO_EXTEND": True,
"AUTO_RELOAD": False,
"EVENT_AUTOREGISTER": False,
"DEPRECATION_FILTER": "once",
"FORWARDED_FOR_HEADER": "X-Forwarded-For",
"FORWARDED_SECRET": None,
"GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec
"INSPECTOR": False,
"INSPECTOR_HOST": "localhost",
"INSPECTOR_PORT": 6457,
"KEEP_ALIVE_TIMEOUT": 5, # 5 seconds
"KEEP_ALIVE": True,
"LOCAL_CERT_CREATOR": LocalCertCreator.AUTO,
"LOCAL_TLS_KEY": _default,
"LOCAL_TLS_CERT": _default,
"LOCALHOST": "localhost",
"MOTD": True,
"MOTD_DISPLAY": {},
"NOISY_EXCEPTIONS": False,
"PROXIES_COUNT": None,
"REAL_IP_HEADER": None,
"REGISTER": True,
"REQUEST_BUFFER_SIZE": 65536, # 64 KiB
"REQUEST_MAX_HEADER_SIZE": 8192, # 8 KiB, but cannot exceed 16384
"REQUEST_ID_HEADER": "X-Request-ID",
"REQUEST_MAX_SIZE": 100000000, # 100 megabytes
"REQUEST_TIMEOUT": 60, # 60 seconds
"RESPONSE_TIMEOUT": 60, # 60 seconds
"RESTART_ORDER": RestartOrder.SHUTDOWN_FIRST,
"TLS_CERT_PASSWORD": "",
"TOUCHUP": _default,
"TOUCHUP": True,
"USE_UVLOOP": _default,
"WEBSOCKET_MAX_SIZE": 2**20, # 1 megabyte
"WEBSOCKET_PING_INTERVAL": 20,
"WEBSOCKET_PING_TIMEOUT": 20,
}
# These values will be removed from the Config object in v22.6 and moved
# to the application state
DEPRECATED_CONFIG = ("SERVER_RUNNING", "RELOADER_PROCESS", "RELOADED_FILES")
class DescriptorMeta(type):
def __init__(cls, *_):
@@ -87,34 +64,25 @@ class Config(dict, metaclass=DescriptorMeta):
AUTO_EXTEND: bool
AUTO_RELOAD: bool
EVENT_AUTOREGISTER: bool
DEPRECATION_FILTER: FilterWarningType
FORWARDED_FOR_HEADER: str
FORWARDED_SECRET: Optional[str]
GRACEFUL_SHUTDOWN_TIMEOUT: float
INSPECTOR: bool
INSPECTOR_HOST: str
INSPECTOR_PORT: int
KEEP_ALIVE_TIMEOUT: int
KEEP_ALIVE: bool
LOCAL_CERT_CREATOR: Union[str, LocalCertCreator]
LOCAL_TLS_KEY: Union[Path, str, Default]
LOCAL_TLS_CERT: Union[Path, str, Default]
LOCALHOST: str
NOISY_EXCEPTIONS: bool
MOTD: bool
MOTD_DISPLAY: Dict[str, str]
NOISY_EXCEPTIONS: bool
PROXIES_COUNT: Optional[int]
REAL_IP_HEADER: Optional[str]
REGISTER: bool
REQUEST_BUFFER_SIZE: int
REQUEST_MAX_HEADER_SIZE: int
REQUEST_ID_HEADER: str
REQUEST_MAX_SIZE: int
REQUEST_TIMEOUT: int
RESPONSE_TIMEOUT: int
RESTART_ORDER: Union[str, RestartOrder]
SERVER_NAME: str
TLS_CERT_PASSWORD: str
TOUCHUP: Union[Default, bool]
TOUCHUP: bool
USE_UVLOOP: Union[Default, bool]
WEBSOCKET_MAX_SIZE: int
WEBSOCKET_PING_INTERVAL: int
@@ -130,9 +98,9 @@ class Config(dict, metaclass=DescriptorMeta):
):
defaults = defaults or {}
super().__init__({**DEFAULT_CONFIG, **defaults})
self._configure_warnings()
self._converters = [str, str_to_bool, float, int]
self._LOGO = ""
if converters:
for converter in converters:
@@ -151,19 +119,19 @@ class Config(dict, metaclass=DescriptorMeta):
self._check_error_format()
self._init = True
def __getattr__(self, attr: Any):
def __getattr__(self, attr):
try:
return self[attr]
except KeyError as ke:
raise AttributeError(f"Config has no '{ke.args[0]}'")
def __setattr__(self, attr: str, value: Any) -> None:
def __setattr__(self, attr, value) -> None:
self.update({attr: value})
def __setitem__(self, attr: str, value: Any) -> None:
def __setitem__(self, attr, value) -> None:
self.update({attr: value})
def update(self, *other: Any, **kwargs: Any) -> None:
def update(self, *other, **kwargs) -> None:
kwargs.update({k: v for item in other for k, v in dict(item).items()})
setters: Dict[str, Any] = {
k: kwargs.pop(k)
@@ -189,23 +157,21 @@ class Config(dict, metaclass=DescriptorMeta):
"REQUEST_MAX_SIZE",
):
self._configure_header_size()
elif attr == "LOGO":
self._LOGO = value
deprecation(
"Setting the config.LOGO is deprecated and will no longer "
"be supported starting in v22.6.",
22.6,
)
if attr == "LOCAL_CERT_CREATOR" and not isinstance(
self.LOCAL_CERT_CREATOR, LocalCertCreator
):
self.LOCAL_CERT_CREATOR = LocalCertCreator[
self.LOCAL_CERT_CREATOR.upper()
]
elif attr == "RESTART_ORDER" and not isinstance(
self.RESTART_ORDER, RestartOrder
):
self.RESTART_ORDER = RestartOrder[self.RESTART_ORDER.upper()]
elif attr == "DEPRECATION_FILTER":
self._configure_warnings()
@property
def LOGO(self):
return self._LOGO
@property
def FALLBACK_ERROR_FORMAT(self) -> str:
if isinstance(self._FALLBACK_ERROR_FORMAT, Default):
if self._FALLBACK_ERROR_FORMAT is _default:
return DEFAULT_FORMAT
return self._FALLBACK_ERROR_FORMAT
@@ -213,7 +179,7 @@ class Config(dict, metaclass=DescriptorMeta):
def FALLBACK_ERROR_FORMAT(self, value):
self._check_error_format(value)
if (
not isinstance(self._FALLBACK_ERROR_FORMAT, Default)
self._FALLBACK_ERROR_FORMAT is not _default
and value != self._FALLBACK_ERROR_FORMAT
):
error_logger.warning(
@@ -229,13 +195,6 @@ class Config(dict, metaclass=DescriptorMeta):
self.REQUEST_MAX_SIZE,
)
def _configure_warnings(self):
filterwarnings(
self.DEPRECATION_FILTER,
category=DeprecationWarning,
module=r"sanic.*",
)
def _check_error_format(self, format: Optional[str] = None):
check_error_format(format or self.FALLBACK_ERROR_FORMAT)
@@ -243,9 +202,7 @@ class Config(dict, metaclass=DescriptorMeta):
"""
Looks for prefixed environment variables and applies them to the
configuration if present. This is called automatically when Sanic
starts up to load environment variables into config. Environment
variables should start with the defined prefix and should only
contain uppercase letters.
starts up to load environment variables into config.
It will automatically hydrate the following types:
@@ -271,9 +228,12 @@ class Config(dict, metaclass=DescriptorMeta):
`See user guide re: config
<https://sanicframework.org/guide/deployment/configuration.html>`__
"""
lower_case_var_found = False
for key, value in environ.items():
if not key.startswith(prefix) or not key.isupper():
if not key.startswith(prefix):
continue
if not key.isupper():
lower_case_var_found = True
_, config_key = key.split(prefix, 1)
@@ -283,6 +243,12 @@ class Config(dict, metaclass=DescriptorMeta):
break
except ValueError:
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]):
"""

View File

@@ -1,9 +1,19 @@
from enum import auto
from sanic.compat import UpperStrEnum
from enum import Enum, auto
class HTTPMethod(UpperStrEnum):
class HTTPMethod(str, Enum):
def _generate_next_value_(name, start, count, last_values):
return name.upper()
def __eq__(self, value: object) -> bool:
value = str(value).upper()
return super().__eq__(value)
def __hash__(self) -> int:
return hash(self.value)
def __str__(self) -> str:
return self.value
GET = auto()
POST = auto()
@@ -14,29 +24,5 @@ class HTTPMethod(UpperStrEnum):
DELETE = auto()
class LocalCertCreator(UpperStrEnum):
AUTO = auto()
TRUSTME = auto()
MKCERT = auto()
class RestartOrder(UpperStrEnum):
SHUTDOWN_FIRST = auto()
STARTUP_FIRST = auto()
HTTP_METHODS = tuple(HTTPMethod.__members__.values())
SAFE_HTTP_METHODS = (HTTPMethod.GET, HTTPMethod.HEAD, HTTPMethod.OPTIONS)
IDEMPOTENT_HTTP_METHODS = (
HTTPMethod.GET,
HTTPMethod.HEAD,
HTTPMethod.PUT,
HTTPMethod.DELETE,
HTTPMethod.OPTIONS,
)
CACHEABLE_HTTP_METHODS = (HTTPMethod.GET, HTTPMethod.HEAD)
DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream"
DEFAULT_LOCAL_TLS_KEY = "key.pem"
DEFAULT_LOCAL_TLS_CERT = "cert.pem"

View File

@@ -19,7 +19,7 @@ import typing as t
from functools import partial
from traceback import extract_tb
from sanic.exceptions import BadRequest, SanicException
from sanic.exceptions import InvalidUsage, SanicException
from sanic.helpers import STATUS_CODES
from sanic.request import Request
from sanic.response import HTTPResponse, html, json, text
@@ -448,8 +448,8 @@ def exception_response(
# from the route
if request.route:
try:
if request.route.extra.error_format:
render_format = request.route.extra.error_format
if request.route.ctx.error_format:
render_format = request.route.ctx.error_format
except AttributeError:
...
@@ -506,7 +506,7 @@ def exception_response(
# $ curl localhost:8000 -d '{"foo": "bar"}'
# And provide them with JSONRenderer
renderer = JSONRenderer if request.json else base
except BadRequest:
except InvalidUsage:
renderer = base
else:
renderer = RENDERERS_BY_CONFIG.get(render_format, renderer)

View File

@@ -1,17 +1,8 @@
from asyncio import CancelledError
from typing import Any, Dict, Optional, Union
from sanic.helpers import STATUS_CODES
class RequestCancelled(CancelledError):
quiet = True
class ServerKilled(Exception):
...
class SanicException(Exception):
message: str = ""
@@ -51,7 +42,7 @@ class NotFound(SanicException):
quiet = True
class BadRequest(SanicException):
class InvalidUsage(SanicException):
"""
**Status**: 400 Bad Request
"""
@@ -60,14 +51,11 @@ class BadRequest(SanicException):
quiet = True
InvalidUsage = BadRequest
class BadURL(BadRequest):
class BadURL(InvalidUsage):
...
class MethodNotAllowed(SanicException):
class MethodNotSupported(SanicException):
"""
**Status**: 405 Method Not Allowed
"""
@@ -80,9 +68,6 @@ class MethodNotAllowed(SanicException):
self.headers = {"Allow": ", ".join(allowed_methods)}
MethodNotSupported = MethodNotAllowed
class ServerError(SanicException):
"""
**Status**: 500 Internal Server Error
@@ -144,19 +129,19 @@ class PayloadTooLarge(SanicException):
quiet = True
class HeaderNotFound(BadRequest):
class HeaderNotFound(InvalidUsage):
"""
**Status**: 400 Bad Request
"""
class InvalidHeader(BadRequest):
class InvalidHeader(InvalidUsage):
"""
**Status**: 400 Bad Request
"""
class RangeNotSatisfiable(SanicException):
class ContentRangeError(SanicException):
"""
**Status**: 416 Range Not Satisfiable
"""
@@ -169,10 +154,7 @@ class RangeNotSatisfiable(SanicException):
self.headers = {"Content-Range": f"bytes */{content_range.total}"}
ContentRangeError = RangeNotSatisfiable
class ExpectationFailed(SanicException):
class HeaderExpectationFailed(SanicException):
"""
**Status**: 417 Expectation Failed
"""
@@ -181,9 +163,6 @@ class ExpectationFailed(SanicException):
quiet = True
HeaderExpectationFailed = ExpectationFailed
class Forbidden(SanicException):
"""
**Status**: 403 Forbidden
@@ -193,7 +172,7 @@ class Forbidden(SanicException):
quiet = True
class InvalidRangeType(RangeNotSatisfiable):
class InvalidRangeType(ContentRangeError):
"""
**Status**: 416 Range Not Satisfiable
"""

View File

@@ -1,13 +1,21 @@
from __future__ import annotations
from typing import Dict, List, Optional, Tuple, Type
from typing import Dict, List, Optional, Tuple, Type, Union
from sanic.errorpages import BaseRenderer, TextRenderer, exception_response
from sanic.config import Config
from sanic.errorpages import (
DEFAULT_FORMAT,
BaseRenderer,
TextRenderer,
exception_response,
)
from sanic.exceptions import (
ContentRangeError,
HeaderNotFound,
InvalidRangeType,
RangeNotSatisfiable,
SanicException,
)
from sanic.helpers import Default, _default
from sanic.log import deprecation, error_logger
from sanic.models.handler_types import RouteHandler
from sanic.response import text
@@ -28,47 +36,94 @@ class ErrorHandler:
def __init__(
self,
fallback: Union[str, Default] = _default,
base: Type[BaseRenderer] = TextRenderer,
):
self.cached_handlers: Dict[
Tuple[Type[BaseException], Optional[str]], Optional[RouteHandler]
] = {}
self.debug = False
self._fallback = fallback
self.base = base
@classmethod
def finalize(cls, *args, **kwargs):
if fallback is not _default:
self._warn_fallback_deprecation()
@property
def fallback(self): # no cov
# This is for backwards compat and can be removed in v22.6
if self._fallback is _default:
return DEFAULT_FORMAT
return self._fallback
@fallback.setter
def fallback(self, value: str): # no cov
self._warn_fallback_deprecation()
if not isinstance(value, str):
raise SanicException(
f"Cannot set error handler fallback to: value={value}"
)
self._fallback = value
@staticmethod
def _warn_fallback_deprecation():
deprecation(
"ErrorHandler.finalize is deprecated and no longer needed. "
"Please remove update your code to remove it. ",
22.12,
"Setting the ErrorHandler fallback value directly is "
"deprecated and no longer supported. This feature will "
"be removed in v22.6. Instead, use "
"app.config.FALLBACK_ERROR_FORMAT.",
22.6,
)
@classmethod
def _get_fallback_value(cls, error_handler: ErrorHandler, config: Config):
if error_handler._fallback is not _default:
if config._FALLBACK_ERROR_FORMAT == error_handler._fallback:
return error_handler.fallback
error_logger.warning(
"Conflicting error fallback values were found in the "
"error handler and in the app.config while handling an "
"exception. Using the value from app.config."
)
return config.FALLBACK_ERROR_FORMAT
@classmethod
def finalize(
cls,
error_handler: ErrorHandler,
config: Config,
fallback: Optional[str] = None,
):
if fallback:
deprecation(
"Setting the ErrorHandler fallback value via finalize() "
"is deprecated and no longer supported. This feature will "
"be removed in v22.6. Instead, use "
"app.config.FALLBACK_ERROR_FORMAT.",
22.6,
)
if not fallback:
fallback = config.FALLBACK_ERROR_FORMAT
if fallback != DEFAULT_FORMAT:
if error_handler._fallback is not _default:
error_logger.warning(
f"Setting the fallback value to {fallback}. This changes "
"the current non-default value "
f"'{error_handler._fallback}'."
)
error_handler._fallback = fallback
if not isinstance(error_handler, cls):
error_logger.warning(
f"Error handler is non-conforming: {type(error_handler)}"
)
def _full_lookup(self, exception, route_name: Optional[str] = None):
return self.lookup(exception, route_name)
def _add(
self,
key: Tuple[Type[BaseException], Optional[str]],
handler: RouteHandler,
) -> None:
if key in self.cached_handlers:
exc, name = key
if name is None:
name = "__ALL_ROUTES__"
error_logger.warning(
f"Duplicate exception handler definition on: route={name} "
f"and exception={exc}"
)
deprecation(
"A duplicate exception handler definition was discovered. "
"This may cause unintended consequences. A warning has been "
"issued now, but it will not be allowed starting in v23.3.",
23.3,
)
self.cached_handlers[key] = handler
def add(self, exception, handler, route_names: Optional[List[str]] = None):
"""
Add a new exception handler to an already existing handler object.
@@ -84,9 +139,9 @@ class ErrorHandler:
"""
if route_names:
for route in route_names:
self._add((exception, route), handler)
self.cached_handlers[(exception, route)] = handler
else:
self._add((exception, None), handler)
self.cached_handlers[(exception, None)] = handler
def lookup(self, exception, route_name: Optional[str] = None):
"""
@@ -182,7 +237,7 @@ class ErrorHandler:
:return:
"""
self.log(request, exception)
fallback = request.app.config.FALLBACK_ERROR_FORMAT
fallback = ErrorHandler._get_fallback_value(self, request.app.config)
return exception_response(
request,
exception,
@@ -241,18 +296,18 @@ class ContentRangeHandler:
try:
self.start = int(start_b) if start_b else None
except ValueError:
raise RangeNotSatisfiable(
raise ContentRangeError(
"'%s' is invalid for Content Range" % (start_b,), self
)
try:
self.end = int(end_b) if end_b else None
except ValueError:
raise RangeNotSatisfiable(
raise ContentRangeError(
"'%s' is invalid for Content Range" % (end_b,), self
)
if self.end is None:
if self.start is None:
raise RangeNotSatisfiable(
raise ContentRangeError(
"Invalid for Content Range parameters", self
)
else:
@@ -264,7 +319,7 @@ class ContentRangeHandler:
self.start = self.total - self.end
self.end = self.total - 1
if self.start >= self.end:
raise RangeNotSatisfiable(
raise ContentRangeError(
"Invalid for Content Range parameters", self
)
self.size = self.end - self.start + 1

View File

@@ -3,53 +3,71 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING:
if TYPE_CHECKING: # no cov
from sanic.request import Request
from sanic.response import BaseHTTPResponse
from asyncio import CancelledError, sleep
from enum import Enum
from sanic.compat import Header
from sanic.exceptions import (
BadRequest,
ExpectationFailed,
HeaderExpectationFailed,
InvalidUsage,
PayloadTooLarge,
RequestCancelled,
ServerError,
ServiceUnavailable,
)
from sanic.headers import format_http1_response
from sanic.helpers import has_message_body
from sanic.http.constants import Stage
from sanic.http.stream import Stream
from sanic.log import access_logger, error_logger, logger
from sanic.touchup import TouchUpMeta
class Stage(Enum):
"""
Enum for representing the stage of the request/response cycle
| ``IDLE`` Waiting for request
| ``REQUEST`` Request headers being received
| ``HANDLER`` Headers done, handler running
| ``RESPONSE`` Response headers sent, body in progress
| ``FAILED`` Unrecoverable state (error while sending response)
|
"""
IDLE = 0 # Waiting for request
REQUEST = 1 # Request headers being received
HANDLER = 3 # Headers done, handler running
RESPONSE = 4 # Response headers sent, body in progress
FAILED = 100 # Unrecoverable state (error while sending response)
HTTP_CONTINUE = b"HTTP/1.1 100 Continue\r\n\r\n"
class Http(Stream, metaclass=TouchUpMeta):
class Http(metaclass=TouchUpMeta):
"""
Internal helper for managing the HTTP/1.1 request/response cycle
Internal helper for managing the HTTP request/response cycle
:raises ServerError:
:raises PayloadTooLarge:
:raises Exception:
:raises BadRequest:
:raises ExpectationFailed:
:raises InvalidUsage:
:raises HeaderExpectationFailed:
:raises RuntimeError:
:raises ServerError:
:raises ServerError:
:raises BadRequest:
:raises BadRequest:
:raises BadRequest:
:raises InvalidUsage:
:raises InvalidUsage:
:raises InvalidUsage:
:raises PayloadTooLarge:
:raises RuntimeError:
"""
HEADER_CEILING = 16_384
HEADER_MAX_SIZE = 0
__touchup__ = (
"http1_request_header",
"http1_response_header",
@@ -133,7 +151,7 @@ class Http(Stream, metaclass=TouchUpMeta):
if self.stage is Stage.RESPONSE:
await self.response.send(end_stream=True)
except CancelledError as exc:
except CancelledError:
# Write an appropriate response before exiting
if not self.protocol.transport:
logger.info(
@@ -141,11 +159,7 @@ class Http(Stream, metaclass=TouchUpMeta):
"stopped. Transport is closed."
)
return
e = (
RequestCancelled()
if self.protocol.conn_info.lost
else (self.exception or exc)
)
e = self.exception or ServiceUnavailable("Cancelled")
self.exception = None
self.keep_alive = False
await self.error_response(e)
@@ -234,7 +248,7 @@ class Http(Stream, metaclass=TouchUpMeta):
headers.append(h)
except Exception:
raise BadRequest("Bad Request")
raise InvalidUsage("Bad Request")
headers_instance = Header(headers)
self.upgrade_websocket = (
@@ -251,7 +265,6 @@ class Http(Stream, metaclass=TouchUpMeta):
transport=self.protocol.transport,
app=self.protocol.app,
)
self.protocol.request_class._current.set(request)
await self.dispatch(
"http.lifecycle.request",
inline=True,
@@ -268,7 +281,7 @@ class Http(Stream, metaclass=TouchUpMeta):
if expect.lower() == "100-continue":
self.expecting_continue = True
else:
raise ExpectationFailed(f"Unknown Expect: {expect}")
raise HeaderExpectationFailed(f"Unknown Expect: {expect}")
if headers.getone("transfer-encoding", None) == "chunked":
self.request_body = "chunked"
@@ -339,12 +352,6 @@ class Http(Stream, metaclass=TouchUpMeta):
self.response_func = self.head_response_ignored
headers["connection"] = "keep-alive" if self.keep_alive else "close"
# This header may be removed or modified by the AltSvcCheck Touchup
# service. At server start, we either remove this header from ever
# being assigned, or we change the value as required.
headers["alt-svc"] = ""
ret = format_http1_response(status, res.processed_headers)
if data:
ret += data
@@ -429,13 +436,7 @@ class Http(Stream, metaclass=TouchUpMeta):
if self.request is None:
self.create_empty_request()
request_middleware = not isinstance(exception, ServiceUnavailable)
try:
await app.handle_exception(
self.request, exception, request_middleware
)
except Exception as e:
await app.handle_exception(self.request, e, False)
await app.handle_exception(self.request, exception)
def create_empty_request(self) -> None:
"""
@@ -509,7 +510,7 @@ class Http(Stream, metaclass=TouchUpMeta):
if len(buf) > 64:
self.keep_alive = False
raise BadRequest("Bad chunked encoding")
raise InvalidUsage("Bad chunked encoding")
await self._receive_more()
@@ -517,14 +518,14 @@ class Http(Stream, metaclass=TouchUpMeta):
size = int(buf[2:pos].split(b";", 1)[0].decode(), 16)
except Exception:
self.keep_alive = False
raise BadRequest("Bad chunked encoding")
raise InvalidUsage("Bad chunked encoding")
if size <= 0:
self.request_body = None
if size < 0:
self.keep_alive = False
raise BadRequest("Bad chunked encoding")
raise InvalidUsage("Bad chunked encoding")
# Consume CRLF, chunk size 0 and the two CRLF that follow
pos += 4

View File

@@ -1,6 +0,0 @@
from .constants import Stage
from .http1 import Http
from .http3 import Http3
__all__ = ("Http", "Stage", "Http3")

View File

@@ -1,29 +0,0 @@
from enum import Enum, IntEnum
class Stage(Enum):
"""
Enum for representing the stage of the request/response cycle
| ``IDLE`` Waiting for request
| ``REQUEST`` Request headers being received
| ``HANDLER`` Headers done, handler running
| ``RESPONSE`` Response headers sent, body in progress
| ``FAILED`` Unrecoverable state (error while sending response)
|
"""
IDLE = 0 # Waiting for request
REQUEST = 1 # Request headers being received
HANDLER = 3 # Headers done, handler running
RESPONSE = 4 # Response headers sent, body in progress
FAILED = 100 # Unrecoverable state (error while sending response)
class HTTP(IntEnum):
VERSION_1 = 1
VERSION_3 = 3
def display(self) -> str:
value = 1.1 if self.value == 1 else self.value
return f"HTTP/{value}"

View File

@@ -1,406 +0,0 @@
from __future__ import annotations
import asyncio
from abc import ABC, abstractmethod
from ssl import SSLContext
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
List,
Optional,
Tuple,
Union,
cast,
)
from sanic.compat import Header
from sanic.constants import LocalCertCreator
from sanic.exceptions import PayloadTooLarge, SanicException, ServerError
from sanic.helpers import has_message_body
from sanic.http.constants import Stage
from sanic.http.stream import Stream
from sanic.http.tls.context import CertSelector, SanicSSLContext
from sanic.log import Colors, logger
from sanic.models.protocol_types import TransportProtocol
from sanic.models.server_types import ConnInfo
try:
from aioquic.h0.connection import H0_ALPN, H0Connection
from aioquic.h3.connection import H3_ALPN, H3Connection
from aioquic.h3.events import (
DatagramReceived,
DataReceived,
H3Event,
HeadersReceived,
WebTransportStreamDataReceived,
)
from aioquic.quic.configuration import QuicConfiguration
from aioquic.tls import SessionTicket
HTTP3_AVAILABLE = True
except ModuleNotFoundError: # no cov
HTTP3_AVAILABLE = False
if TYPE_CHECKING:
from sanic import Sanic
from sanic.request import Request
from sanic.response import BaseHTTPResponse
from sanic.server.protocols.http_protocol import Http3Protocol
HttpConnection = Union[H0Connection, H3Connection]
class HTTP3Transport(TransportProtocol):
__slots__ = ("_protocol",)
def __init__(self, protocol: Http3Protocol):
self._protocol = protocol
def get_protocol(self) -> Http3Protocol:
return self._protocol
def get_extra_info(self, info: str, default: Any = None) -> Any:
if (
info in ("socket", "sockname", "peername")
and self._protocol._transport
):
return self._protocol._transport.get_extra_info(info, default)
elif info == "network_paths":
return self._protocol._quic._network_paths
elif info == "ssl_context":
return self._protocol.app.state.ssl
return default
class Receiver(ABC):
future: asyncio.Future
def __init__(self, transmit, protocol, request: Request) -> None:
self.transmit = transmit
self.protocol = protocol
self.request = request
@abstractmethod
async def run(self): # no cov
...
class HTTPReceiver(Receiver, Stream):
stage: Stage
request: Request
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.request_body = None
self.stage = Stage.IDLE
self.headers_sent = False
self.response: Optional[BaseHTTPResponse] = None
self.request_max_size = self.protocol.request_max_size
self.request_bytes = 0
async def run(self, exception: Optional[Exception] = None):
self.stage = Stage.HANDLER
self.head_only = self.request.method.upper() == "HEAD"
if exception:
logger.info( # no cov
f"{Colors.BLUE}[exception]: "
f"{Colors.RED}{exception}{Colors.END}",
exc_info=True,
extra={"verbosity": 1},
)
await self.error_response(exception)
else:
try:
logger.info( # no cov
f"{Colors.BLUE}[request]:{Colors.END} {self.request}",
extra={"verbosity": 1},
)
await self.protocol.request_handler(self.request)
except Exception as e: # no cov
# This should largely be handled within the request handler.
# But, just in case...
await self.run(e)
self.stage = Stage.IDLE
async def error_response(self, exception: Exception) -> None:
"""
Handle response when exception encountered
"""
# From request and handler states we can respond, otherwise be silent
app = self.protocol.app
await app.handle_exception(self.request, exception)
def _prepare_headers(
self, response: BaseHTTPResponse
) -> List[Tuple[bytes, bytes]]:
size = len(response.body) if response.body else 0
headers = response.headers
status = response.status
if not has_message_body(status) and (
size
or "content-length" in headers
or "transfer-encoding" in headers
):
headers.pop("content-length", None)
headers.pop("transfer-encoding", None)
logger.warning( # no cov
f"Message body set in response on {self.request.path}. "
f"A {status} response may only have headers, no body."
)
elif "content-length" not in headers:
if size:
headers["content-length"] = size
else:
headers["transfer-encoding"] = "chunked"
headers = [
(b":status", str(response.status).encode()),
*response.processed_headers,
]
return headers
def send_headers(self) -> None:
logger.debug( # no cov
f"{Colors.BLUE}[send]: {Colors.GREEN}HEADERS{Colors.END}",
extra={"verbosity": 2},
)
if not self.response:
raise RuntimeError("no response")
response = self.response
headers = self._prepare_headers(response)
self.protocol.connection.send_headers(
stream_id=self.request.stream_id,
headers=headers,
)
self.headers_sent = True
self.stage = Stage.RESPONSE
if self.response.body and not self.head_only:
self._send(self.response.body, False)
elif self.head_only:
self.future.cancel()
def respond(self, response: BaseHTTPResponse) -> BaseHTTPResponse:
logger.debug( # no cov
f"{Colors.BLUE}[respond]:{Colors.END} {response}",
extra={"verbosity": 2},
)
if self.stage is not Stage.HANDLER:
self.stage = Stage.FAILED
raise RuntimeError("Response already started")
# Disconnect any earlier but unused response object
if self.response is not None:
self.response.stream = None
self.response, response.stream = response, self
return response
def receive_body(self, data: bytes) -> None:
self.request_bytes += len(data)
if self.request_bytes > self.request_max_size:
raise PayloadTooLarge("Request body exceeds the size limit")
self.request.body += data
async def send(self, data: bytes, end_stream: bool) -> None:
logger.debug( # no cov
f"{Colors.BLUE}[send]: {Colors.GREEN}data={data.decode()} "
f"end_stream={end_stream}{Colors.END}",
extra={"verbosity": 2},
)
self._send(data, end_stream)
def _send(self, data: bytes, end_stream: bool) -> None:
if not self.headers_sent:
self.send_headers()
if self.stage is not Stage.RESPONSE:
raise ServerError(f"not ready to send: {self.stage}")
# Chunked
if (
self.response
and self.response.headers.get("transfer-encoding") == "chunked"
):
size = len(data)
if end_stream:
data = (
b"%x\r\n%b\r\n0\r\n\r\n" % (size, data)
if size
else b"0\r\n\r\n"
)
elif size:
data = b"%x\r\n%b\r\n" % (size, data)
logger.debug( # no cov
f"{Colors.BLUE}[transmitting]{Colors.END}",
extra={"verbosity": 2},
)
self.protocol.connection.send_data(
stream_id=self.request.stream_id,
data=data,
end_stream=end_stream,
)
self.transmit()
if end_stream:
self.stage = Stage.IDLE
class WebsocketReceiver(Receiver): # noqa
async def run(self):
...
class WebTransportReceiver(Receiver): # noqa
async def run(self):
...
class Http3:
"""
Internal helper for managing the HTTP/3 request/response cycle
"""
if HTTP3_AVAILABLE:
HANDLER_PROPERTY_MAPPING = {
DataReceived: "stream_id",
HeadersReceived: "stream_id",
DatagramReceived: "flow_id",
WebTransportStreamDataReceived: "session_id",
}
def __init__(
self,
protocol: Http3Protocol,
transmit: Callable[[], None],
) -> None:
self.protocol = protocol
self.transmit = transmit
self.receivers: Dict[int, Receiver] = {}
def http_event_received(self, event: H3Event) -> None:
logger.debug( # no cov
f"{Colors.BLUE}[http_event_received]: "
f"{Colors.YELLOW}{event}{Colors.END}",
extra={"verbosity": 2},
)
receiver, created_new = self.get_or_make_receiver(event)
receiver = cast(HTTPReceiver, receiver)
if isinstance(event, HeadersReceived) and created_new:
receiver.future = asyncio.ensure_future(receiver.run())
elif isinstance(event, DataReceived):
try:
receiver.receive_body(event.data)
except Exception as e:
receiver.future.cancel()
receiver.future = asyncio.ensure_future(receiver.run(e))
else:
... # Intentionally here to help out Touchup
logger.debug( # no cov
f"{Colors.RED}DOING NOTHING{Colors.END}",
extra={"verbosity": 2},
)
def get_or_make_receiver(self, event: H3Event) -> Tuple[Receiver, bool]:
if (
isinstance(event, HeadersReceived)
and event.stream_id not in self.receivers
):
request = self._make_request(event)
receiver = HTTPReceiver(self.transmit, self.protocol, request)
request.stream = receiver
self.receivers[event.stream_id] = receiver
return receiver, True
else:
ident = getattr(event, self.HANDLER_PROPERTY_MAPPING[type(event)])
return self.receivers[ident], False
def get_receiver_by_stream_id(self, stream_id: int) -> Receiver:
return self.receivers[stream_id]
def _make_request(self, event: HeadersReceived) -> Request:
headers = Header(((k.decode(), v.decode()) for k, v in event.headers))
method = headers[":method"]
path = headers[":path"]
scheme = headers.pop(":scheme", "")
authority = headers.pop(":authority", "")
if authority:
headers["host"] = authority
transport = HTTP3Transport(self.protocol)
request = self.protocol.request_class(
path.encode(),
headers,
"3",
method,
transport,
self.protocol.app,
b"",
)
request.conn_info = ConnInfo(transport)
request._stream_id = event.stream_id
request._scheme = scheme
return request
class SessionTicketStore:
"""
Simple in-memory store for session tickets.
"""
def __init__(self) -> None:
self.tickets: Dict[bytes, SessionTicket] = {}
def add(self, ticket: SessionTicket) -> None:
self.tickets[ticket.ticket] = ticket
def pop(self, label: bytes) -> Optional[SessionTicket]:
return self.tickets.pop(label, None)
def get_config(
app: Sanic, ssl: Union[SanicSSLContext, CertSelector, SSLContext]
):
# TODO:
# - proper selection needed if service with multiple certs insted of
# just taking the first
if isinstance(ssl, CertSelector):
ssl = cast(SanicSSLContext, ssl.sanic_select[0])
if app.config.LOCAL_CERT_CREATOR is LocalCertCreator.TRUSTME:
raise SanicException(
"Sorry, you cannot currently use trustme as a local certificate "
"generator for an HTTP/3 server. This is not yet supported. You "
"should be able to use mkcert instead. For more information, see: "
"https://github.com/aiortc/aioquic/issues/295."
)
if not isinstance(ssl, SanicSSLContext):
raise SanicException("SSLContext is not SanicSSLContext")
config = QuicConfiguration(
alpn_protocols=H3_ALPN + H0_ALPN + ["siduck"],
is_client=False,
max_datagram_frame_size=65536,
)
password = app.config.TLS_CERT_PASSWORD or None
config.load_cert_chain(
ssl.sanic["cert"], ssl.sanic["key"], password=password
)
return config

View File

@@ -1,27 +0,0 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Optional, Tuple, Union
from sanic.http.constants import Stage
if TYPE_CHECKING:
from sanic.response import BaseHTTPResponse
from sanic.server.protocols.http_protocol import HttpProtocol
class Stream:
stage: Stage
response: Optional[BaseHTTPResponse]
protocol: HttpProtocol
url: Optional[str]
request_body: Optional[bytes]
request_max_size: Union[int, float]
__touchup__: Tuple[str, ...] = tuple()
__slots__ = ()
def respond(
self, response: BaseHTTPResponse
) -> BaseHTTPResponse: # no cov
raise NotImplementedError("Not implemented")

View File

@@ -1,5 +0,0 @@
from .context import process_to_context
from .creators import get_ssl_context
__all__ = ("get_ssl_context", "process_to_context")

View File

@@ -1,290 +0,0 @@
from __future__ import annotations
import ssl
import subprocess
import sys
from abc import ABC, abstractmethod
from contextlib import suppress
from pathlib import Path
from tempfile import mkdtemp
from types import ModuleType
from typing import TYPE_CHECKING, Optional, Tuple, Type, Union, cast
from sanic.application.constants import Mode
from sanic.application.spinner import loading
from sanic.constants import (
DEFAULT_LOCAL_TLS_CERT,
DEFAULT_LOCAL_TLS_KEY,
LocalCertCreator,
)
from sanic.exceptions import SanicException
from sanic.helpers import Default
from sanic.http.tls.context import CertSimple, SanicSSLContext
try:
import trustme
TRUSTME_INSTALLED = True
except (ImportError, ModuleNotFoundError):
trustme = ModuleType("trustme")
TRUSTME_INSTALLED = False
if TYPE_CHECKING:
from sanic import Sanic
# Only allow secure ciphers, notably leaving out AES-CBC mode
# OpenSSL chooses ECDSA or RSA depending on the cert in use
CIPHERS_TLS12 = [
"ECDHE-ECDSA-CHACHA20-POLY1305",
"ECDHE-ECDSA-AES256-GCM-SHA384",
"ECDHE-ECDSA-AES128-GCM-SHA256",
"ECDHE-RSA-CHACHA20-POLY1305",
"ECDHE-RSA-AES256-GCM-SHA384",
"ECDHE-RSA-AES128-GCM-SHA256",
]
def _make_path(maybe_path: Union[Path, str], tmpdir: Optional[Path]) -> Path:
if isinstance(maybe_path, Path):
return maybe_path
else:
path = Path(maybe_path)
if not path.exists():
if not tmpdir:
raise RuntimeError("Reached an unknown state. No tmpdir.")
return tmpdir / maybe_path
return path
def get_ssl_context(
app: Sanic, ssl: Optional[ssl.SSLContext]
) -> ssl.SSLContext:
if ssl:
return ssl
if app.state.mode is Mode.PRODUCTION:
raise SanicException(
"Cannot run Sanic as an HTTPS server in PRODUCTION mode "
"without passing a TLS certificate. If you are developing "
"locally, please enable DEVELOPMENT mode and Sanic will "
"generate a localhost TLS certificate. For more information "
"please see: https://sanic.dev/en/guide/deployment/development."
"html#automatic-tls-certificate."
)
creator = CertCreator.select(
app,
cast(LocalCertCreator, app.config.LOCAL_CERT_CREATOR),
app.config.LOCAL_TLS_KEY,
app.config.LOCAL_TLS_CERT,
)
context = creator.generate_cert(app.config.LOCALHOST)
return context
class CertCreator(ABC):
def __init__(self, app, key, cert) -> None:
self.app = app
self.key = key
self.cert = cert
self.tmpdir = None
if isinstance(self.key, Default) or isinstance(self.cert, Default):
self.tmpdir = Path(mkdtemp())
key = (
DEFAULT_LOCAL_TLS_KEY
if isinstance(self.key, Default)
else self.key
)
cert = (
DEFAULT_LOCAL_TLS_CERT
if isinstance(self.cert, Default)
else self.cert
)
self.key_path = _make_path(key, self.tmpdir)
self.cert_path = _make_path(cert, self.tmpdir)
@abstractmethod
def check_supported(self) -> None: # no cov
...
@abstractmethod
def generate_cert(self, localhost: str) -> ssl.SSLContext: # no cov
...
@classmethod
def select(
cls,
app: Sanic,
cert_creator: LocalCertCreator,
local_tls_key,
local_tls_cert,
) -> CertCreator:
creator: Optional[CertCreator] = None
cert_creator_options: Tuple[
Tuple[Type[CertCreator], LocalCertCreator], ...
] = (
(MkcertCreator, LocalCertCreator.MKCERT),
(TrustmeCreator, LocalCertCreator.TRUSTME),
)
for creator_class, local_creator in cert_creator_options:
creator = cls._try_select(
app,
creator,
creator_class,
local_creator,
cert_creator,
local_tls_key,
local_tls_cert,
)
if creator:
break
if not creator:
raise SanicException(
"Sanic could not find package to create a TLS certificate. "
"You must have either mkcert or trustme installed. See "
"https://sanic.dev/en/guide/deployment/development.html"
"#automatic-tls-certificate for more details."
)
return creator
@staticmethod
def _try_select(
app: Sanic,
creator: Optional[CertCreator],
creator_class: Type[CertCreator],
creator_requirement: LocalCertCreator,
creator_requested: LocalCertCreator,
local_tls_key,
local_tls_cert,
):
if creator or (
creator_requested is not LocalCertCreator.AUTO
and creator_requested is not creator_requirement
):
return creator
instance = creator_class(app, local_tls_key, local_tls_cert)
try:
instance.check_supported()
except SanicException:
if creator_requested is creator_requirement:
raise
else:
return None
return instance
class MkcertCreator(CertCreator):
def check_supported(self) -> None:
try:
subprocess.run( # nosec B603 B607
["mkcert", "-help"],
check=True,
stderr=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
)
except Exception as e:
raise SanicException(
"Sanic is attempting to use mkcert to generate local TLS "
"certificates since you did not supply a certificate, but "
"one is required. Sanic cannot proceed since mkcert does not "
"appear to be installed. Alternatively, you can use trustme. "
"Please install mkcert, trustme, or supply TLS certificates "
"to proceed. Installation instructions can be found here: "
"https://github.com/FiloSottile/mkcert.\n"
"Find out more information about your options here: "
"https://sanic.dev/en/guide/deployment/development.html#"
"automatic-tls-certificate"
) from e
def generate_cert(self, localhost: str) -> ssl.SSLContext:
try:
if not self.cert_path.exists():
message = "Generating TLS certificate"
# TODO: Validate input for security
with loading(message):
cmd = [
"mkcert",
"-key-file",
str(self.key_path),
"-cert-file",
str(self.cert_path),
localhost,
]
resp = subprocess.run( # nosec B603
cmd,
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
)
sys.stdout.write("\r" + " " * (len(message) + 4))
sys.stdout.flush()
sys.stdout.write(resp.stdout)
finally:
@self.app.main_process_stop
async def cleanup(*_): # no cov
if self.tmpdir:
with suppress(FileNotFoundError):
self.key_path.unlink()
self.cert_path.unlink()
self.tmpdir.rmdir()
context = CertSimple(self.cert_path, self.key_path)
context.sanic["creator"] = "mkcert"
context.sanic["localhost"] = localhost
SanicSSLContext.create_from_ssl_context(context)
return context
class TrustmeCreator(CertCreator):
def check_supported(self) -> None:
if not TRUSTME_INSTALLED:
raise SanicException(
"Sanic is attempting to use trustme to generate local TLS "
"certificates since you did not supply a certificate, but "
"one is required. Sanic cannot proceed since trustme does not "
"appear to be installed. Alternatively, you can use mkcert. "
"Please install mkcert, trustme, or supply TLS certificates "
"to proceed. Installation instructions can be found here: "
"https://github.com/python-trio/trustme.\n"
"Find out more information about your options here: "
"https://sanic.dev/en/guide/deployment/development.html#"
"automatic-tls-certificate"
)
def generate_cert(self, localhost: str) -> ssl.SSLContext:
context = SanicSSLContext.create_from_ssl_context(
ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
)
context.sanic = {
"cert": self.cert_path.absolute(),
"key": self.key_path.absolute(),
}
ca = trustme.CA()
server_cert = ca.issue_cert(localhost)
server_cert.configure_cert(context)
ca.configure_trust(context)
ca.cert_pem.write_to_path(str(self.cert_path.absolute()))
server_cert.private_key_and_cert_chain_pem.write_to_path(
str(self.key_path.absolute())
)
context.sanic["creator"] = "trustme"
context.sanic["localhost"] = localhost
return context

View File

@@ -1,11 +1,10 @@
import logging
import sys
from enum import Enum
from typing import Any, Dict
from warnings import warn
from sanic.compat import StrEnum, is_atty
LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict( # no cov
version=1,
@@ -24,12 +23,6 @@ LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict( # no cov
"propagate": True,
"qualname": "sanic.access",
},
"sanic.server": {
"level": "INFO",
"handlers": ["console"],
"propagate": True,
"qualname": "sanic.server",
},
},
handlers={
"console": {
@@ -62,60 +55,35 @@ LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict( # no cov
},
},
)
"""
Defult logging configuration
"""
class Colors(StrEnum): # no cov
class Colors(str, Enum): # no cov
END = "\033[0m"
BOLD = "\033[1m"
BLUE = "\033[34m"
GREEN = "\033[32m"
PURPLE = "\033[35m"
RED = "\033[31m"
SANIC = "\033[38;2;255;13;104m"
BLUE = "\033[01;34m"
GREEN = "\033[01;32m"
YELLOW = "\033[01;33m"
RED = "\033[01;31m"
class VerbosityFilter(logging.Filter):
verbosity: int = 0
def filter(self, record: logging.LogRecord) -> bool:
verbosity = getattr(record, "verbosity", 0)
return verbosity <= self.verbosity
_verbosity_filter = VerbosityFilter()
logger = logging.getLogger("sanic.root") # no cov
"""
General Sanic logger
"""
logger.addFilter(_verbosity_filter)
error_logger = logging.getLogger("sanic.error") # no cov
"""
Logger used by Sanic for error logging
"""
error_logger.addFilter(_verbosity_filter)
access_logger = logging.getLogger("sanic.access") # no cov
"""
Logger used by Sanic for access logging
"""
access_logger.addFilter(_verbosity_filter)
server_logger = logging.getLogger("sanic.server") # no cov
"""
Logger used by Sanic for server related messages
"""
logger.addFilter(_verbosity_filter)
def deprecation(message: str, version: float): # no cov
version_info = f"[DEPRECATION v{version}] "
if is_atty():
if sys.stdout.isatty():
version_info = f"{Colors.RED}{version_info}"
message = f"{Colors.YELLOW}{message}{Colors.END}"
warn(version_info + message, DeprecationWarning)

View File

@@ -1,66 +0,0 @@
from __future__ import annotations
from collections import deque
from enum import IntEnum, auto
from itertools import count
from typing import Deque, Sequence, Union
from sanic.models.handler_types import MiddlewareType
class MiddlewareLocation(IntEnum):
REQUEST = auto()
RESPONSE = auto()
class Middleware:
_counter = count()
__slots__ = ("func", "priority", "location", "definition")
def __init__(
self,
func: MiddlewareType,
location: MiddlewareLocation,
priority: int = 0,
) -> None:
self.func = func
self.priority = priority
self.location = location
self.definition = next(Middleware._counter)
def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs)
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}("
f"func=<function {self.func.__name__}>, "
f"priority={self.priority}, "
f"location={self.location.name})"
)
@property
def order(self):
return (self.priority, -self.definition)
@classmethod
def convert(
cls,
*middleware_collections: Sequence[Union[Middleware, MiddlewareType]],
location: MiddlewareLocation,
) -> Deque[Middleware]:
return deque(
[
middleware
if isinstance(middleware, Middleware)
else Middleware(middleware, location)
for collection in middleware_collections
for middleware in collection
]
)
@classmethod
def reset_count(cls):
cls._counter = count()
cls.count = next(cls._counter)

View File

@@ -3,7 +3,7 @@ from functools import partial
from typing import Callable, List, Optional, Union, overload
from sanic.base.meta import SanicMeta
from sanic.exceptions import BadRequest
from sanic.exceptions import InvalidUsage
from sanic.models.futures import FutureListener
from sanic.models.handler_types import ListenerType, Sanic
@@ -17,12 +17,9 @@ class ListenerEvent(str, Enum):
BEFORE_SERVER_STOP = "server.shutdown.before"
AFTER_SERVER_STOP = "server.shutdown.after"
MAIN_PROCESS_START = auto()
MAIN_PROCESS_READY = auto()
MAIN_PROCESS_STOP = auto()
RELOAD_PROCESS_START = auto()
RELOAD_PROCESS_STOP = auto()
BEFORE_RELOAD_TRIGGER = auto()
AFTER_RELOAD_TRIGGER = auto()
class ListenerMixin(metaclass=SanicMeta):
@@ -89,7 +86,7 @@ class ListenerMixin(metaclass=SanicMeta):
if callable(listener_or_event):
if event_or_none is None:
raise BadRequest(
raise InvalidUsage(
"Invalid event registration: Missing event name."
)
return register_listener(listener_or_event, event_or_none)
@@ -101,11 +98,6 @@ class ListenerMixin(metaclass=SanicMeta):
) -> ListenerType[Sanic]:
return self.listener(listener, "main_process_start")
def main_process_ready(
self, listener: ListenerType[Sanic]
) -> ListenerType[Sanic]:
return self.listener(listener, "main_process_ready")
def main_process_stop(
self, listener: ListenerType[Sanic]
) -> ListenerType[Sanic]:
@@ -121,16 +113,6 @@ class ListenerMixin(metaclass=SanicMeta):
) -> ListenerType[Sanic]:
return self.listener(listener, "reload_process_stop")
def before_reload_trigger(
self, listener: ListenerType[Sanic]
) -> ListenerType[Sanic]:
return self.listener(listener, "before_reload_trigger")
def after_reload_trigger(
self, listener: ListenerType[Sanic]
) -> ListenerType[Sanic]:
return self.listener(listener, "after_reload_trigger")
def before_server_start(
self, listener: ListenerType[Sanic]
) -> ListenerType[Sanic]:

View File

@@ -1,17 +1,11 @@
from collections import deque
from functools import partial
from operator import attrgetter
from typing import List
from sanic.base.meta import SanicMeta
from sanic.middleware import Middleware, MiddlewareLocation
from sanic.models.futures import FutureMiddleware
from sanic.router import Router
class MiddlewareMixin(metaclass=SanicMeta):
router: Router
def __init__(self, *args, **kwargs) -> None:
self._future_middleware: List[FutureMiddleware] = []
@@ -19,12 +13,7 @@ class MiddlewareMixin(metaclass=SanicMeta):
raise NotImplementedError # noqa
def middleware(
self,
middleware_or_request,
attach_to="request",
apply=True,
*,
priority=0
self, middleware_or_request, attach_to="request", apply=True
):
"""
Decorate and register middleware to be called before a request
@@ -41,12 +30,6 @@ class MiddlewareMixin(metaclass=SanicMeta):
def register_middleware(middleware, attach_to="request"):
nonlocal apply
location = (
MiddlewareLocation.REQUEST
if attach_to == "request"
else MiddlewareLocation.RESPONSE
)
middleware = Middleware(middleware, location, priority=priority)
future_middleware = FutureMiddleware(middleware, attach_to)
self._future_middleware.append(future_middleware)
if apply:
@@ -63,7 +46,7 @@ class MiddlewareMixin(metaclass=SanicMeta):
register_middleware, attach_to=middleware_or_request
)
def on_request(self, middleware=None, *, priority=0):
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')*.
@@ -71,13 +54,11 @@ class MiddlewareMixin(metaclass=SanicMeta):
:param: middleware: A callable that takes in request.
"""
if callable(middleware):
return self.middleware(middleware, "request", priority=priority)
return self.middleware(middleware, "request")
else:
return partial(
self.middleware, attach_to="request", priority=priority
)
return partial(self.middleware, attach_to="request")
def on_response(self, middleware=None, *, priority=0):
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')*.
@@ -86,57 +67,6 @@ class MiddlewareMixin(metaclass=SanicMeta):
A callable that takes in a request and its response.
"""
if callable(middleware):
return self.middleware(middleware, "response", priority=priority)
return self.middleware(middleware, "response")
else:
return partial(
self.middleware, attach_to="response", priority=priority
)
def finalize_middleware(self):
for route in self.router.routes:
request_middleware = Middleware.convert(
self.request_middleware,
self.named_request_middleware.get(route.name, deque()),
location=MiddlewareLocation.REQUEST,
)
response_middleware = Middleware.convert(
self.response_middleware,
self.named_response_middleware.get(route.name, deque()),
location=MiddlewareLocation.RESPONSE,
)
route.extra.request_middleware = deque(
sorted(
request_middleware,
key=attrgetter("order"),
reverse=True,
)
)
route.extra.response_middleware = deque(
sorted(
response_middleware,
key=attrgetter("order"),
reverse=True,
)[::-1]
)
request_middleware = Middleware.convert(
self.request_middleware,
location=MiddlewareLocation.REQUEST,
)
response_middleware = Middleware.convert(
self.response_middleware,
location=MiddlewareLocation.RESPONSE,
)
self.request_middleware = deque(
sorted(
request_middleware,
key=attrgetter("order"),
reverse=True,
)
)
self.response_middleware = deque(
sorted(
response_middleware,
key=attrgetter("order"),
reverse=True,
)[::-1]
)
return partial(self.middleware, attach_to="response")

View File

@@ -1,44 +1,47 @@
from ast import NodeVisitor, Return, parse
from contextlib import suppress
from email.utils import formatdate
from functools import partial, wraps
from inspect import getsource, signature
from mimetypes import guess_type
from os import path
from pathlib import Path, PurePath
from pathlib import PurePath
from re import sub
from textwrap import dedent
from typing import (
Any,
Callable,
Dict,
Iterable,
List,
Optional,
Set,
Tuple,
Union,
cast,
)
from time import gmtime, strftime
from typing import Any, Callable, Iterable, List, Optional, Set, Tuple, Union
from urllib.parse import unquote
from sanic_routing.route import Route
from sanic_routing.route import Route # type: ignore
from sanic.base.meta import SanicMeta
from sanic.compat import stat_async
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE, HTTP_METHODS
from sanic.errorpages import RESPONSE_MAPPING
from sanic.exceptions import FileNotFound, HeaderNotFound, RangeNotSatisfiable
from sanic.exceptions import (
ContentRangeError,
FileNotFound,
HeaderNotFound,
InvalidUsage,
)
from sanic.handlers import ContentRangeHandler
from sanic.log import error_logger
from sanic.log import deprecation, error_logger
from sanic.models.futures import FutureRoute, FutureStatic
from sanic.models.handler_types import RouteHandler
from sanic.response import HTTPResponse, file, file_stream, validate_file
from sanic.response import HTTPResponse, file, file_stream
from sanic.types import HashableDict
RouteWrapper = Callable[
[RouteHandler], Union[RouteHandler, Tuple[Route, RouteHandler]]
]
RESTRICTED_ROUTE_CONTEXT = (
"ignore_body",
"stream",
"hosts",
"static",
"error_format",
"websocket",
)
class RouteMixin(metaclass=SanicMeta):
@@ -218,7 +221,7 @@ class RouteMixin(metaclass=SanicMeta):
stream: bool = False,
version_prefix: str = "/v",
error_format: Optional[str] = None,
**ctx_kwargs: Any,
**ctx_kwargs,
) -> RouteHandler:
"""A helper method to register class instance or
functions as a handler to the application url
@@ -279,8 +282,8 @@ class RouteMixin(metaclass=SanicMeta):
ignore_body: bool = True,
version_prefix: str = "/v",
error_format: Optional[str] = None,
**ctx_kwargs: Any,
) -> RouteHandler:
**ctx_kwargs,
) -> RouteWrapper:
"""
Add an API URL under the **GET** *HTTP* method
@@ -296,20 +299,17 @@ class RouteMixin(metaclass=SanicMeta):
will be appended to the route context (``route.ctx``)
:return: Object decorated with :func:`route` method
"""
return cast(
RouteHandler,
self.route(
uri,
methods=frozenset({"GET"}),
host=host,
strict_slashes=strict_slashes,
version=version,
name=name,
ignore_body=ignore_body,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
),
return self.route(
uri,
methods=frozenset({"GET"}),
host=host,
strict_slashes=strict_slashes,
version=version,
name=name,
ignore_body=ignore_body,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)
def post(
@@ -322,8 +322,8 @@ class RouteMixin(metaclass=SanicMeta):
name: Optional[str] = None,
version_prefix: str = "/v",
error_format: Optional[str] = None,
**ctx_kwargs: Any,
) -> RouteHandler:
**ctx_kwargs,
) -> RouteWrapper:
"""
Add an API URL under the **POST** *HTTP* method
@@ -339,20 +339,17 @@ class RouteMixin(metaclass=SanicMeta):
will be appended to the route context (``route.ctx``)
:return: Object decorated with :func:`route` method
"""
return cast(
RouteHandler,
self.route(
uri,
methods=frozenset({"POST"}),
host=host,
strict_slashes=strict_slashes,
stream=stream,
version=version,
name=name,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
),
return self.route(
uri,
methods=frozenset({"POST"}),
host=host,
strict_slashes=strict_slashes,
stream=stream,
version=version,
name=name,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)
def put(
@@ -365,8 +362,8 @@ class RouteMixin(metaclass=SanicMeta):
name: Optional[str] = None,
version_prefix: str = "/v",
error_format: Optional[str] = None,
**ctx_kwargs: Any,
) -> RouteHandler:
**ctx_kwargs,
) -> RouteWrapper:
"""
Add an API URL under the **PUT** *HTTP* method
@@ -382,20 +379,17 @@ class RouteMixin(metaclass=SanicMeta):
will be appended to the route context (``route.ctx``)
:return: Object decorated with :func:`route` method
"""
return cast(
RouteHandler,
self.route(
uri,
methods=frozenset({"PUT"}),
host=host,
strict_slashes=strict_slashes,
stream=stream,
version=version,
name=name,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
),
return self.route(
uri,
methods=frozenset({"PUT"}),
host=host,
strict_slashes=strict_slashes,
stream=stream,
version=version,
name=name,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)
def head(
@@ -408,8 +402,8 @@ class RouteMixin(metaclass=SanicMeta):
ignore_body: bool = True,
version_prefix: str = "/v",
error_format: Optional[str] = None,
**ctx_kwargs: Any,
) -> RouteHandler:
**ctx_kwargs,
) -> RouteWrapper:
"""
Add an API URL under the **HEAD** *HTTP* method
@@ -433,20 +427,17 @@ class RouteMixin(metaclass=SanicMeta):
will be appended to the route context (``route.ctx``)
:return: Object decorated with :func:`route` method
"""
return cast(
RouteHandler,
self.route(
uri,
methods=frozenset({"HEAD"}),
host=host,
strict_slashes=strict_slashes,
version=version,
name=name,
ignore_body=ignore_body,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
),
return self.route(
uri,
methods=frozenset({"HEAD"}),
host=host,
strict_slashes=strict_slashes,
version=version,
name=name,
ignore_body=ignore_body,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)
def options(
@@ -459,8 +450,8 @@ class RouteMixin(metaclass=SanicMeta):
ignore_body: bool = True,
version_prefix: str = "/v",
error_format: Optional[str] = None,
**ctx_kwargs: Any,
) -> RouteHandler:
**ctx_kwargs,
) -> RouteWrapper:
"""
Add an API URL under the **OPTIONS** *HTTP* method
@@ -484,20 +475,17 @@ class RouteMixin(metaclass=SanicMeta):
will be appended to the route context (``route.ctx``)
:return: Object decorated with :func:`route` method
"""
return cast(
RouteHandler,
self.route(
uri,
methods=frozenset({"OPTIONS"}),
host=host,
strict_slashes=strict_slashes,
version=version,
name=name,
ignore_body=ignore_body,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
),
return self.route(
uri,
methods=frozenset({"OPTIONS"}),
host=host,
strict_slashes=strict_slashes,
version=version,
name=name,
ignore_body=ignore_body,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)
def patch(
@@ -510,8 +498,8 @@ class RouteMixin(metaclass=SanicMeta):
name: Optional[str] = None,
version_prefix: str = "/v",
error_format: Optional[str] = None,
**ctx_kwargs: Any,
) -> RouteHandler:
**ctx_kwargs,
) -> RouteWrapper:
"""
Add an API URL under the **PATCH** *HTTP* method
@@ -537,20 +525,17 @@ class RouteMixin(metaclass=SanicMeta):
will be appended to the route context (``route.ctx``)
:return: Object decorated with :func:`route` method
"""
return cast(
RouteHandler,
self.route(
uri,
methods=frozenset({"PATCH"}),
host=host,
strict_slashes=strict_slashes,
stream=stream,
version=version,
name=name,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
),
return self.route(
uri,
methods=frozenset({"PATCH"}),
host=host,
strict_slashes=strict_slashes,
stream=stream,
version=version,
name=name,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)
def delete(
@@ -563,8 +548,8 @@ class RouteMixin(metaclass=SanicMeta):
ignore_body: bool = True,
version_prefix: str = "/v",
error_format: Optional[str] = None,
**ctx_kwargs: Any,
) -> RouteHandler:
**ctx_kwargs,
) -> RouteWrapper:
"""
Add an API URL under the **DELETE** *HTTP* method
@@ -580,20 +565,17 @@ class RouteMixin(metaclass=SanicMeta):
will be appended to the route context (``route.ctx``)
:return: Object decorated with :func:`route` method
"""
return cast(
RouteHandler,
self.route(
uri,
methods=frozenset({"DELETE"}),
host=host,
strict_slashes=strict_slashes,
version=version,
name=name,
ignore_body=ignore_body,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
),
return self.route(
uri,
methods=frozenset({"DELETE"}),
host=host,
strict_slashes=strict_slashes,
version=version,
name=name,
ignore_body=ignore_body,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)
def websocket(
@@ -607,7 +589,7 @@ class RouteMixin(metaclass=SanicMeta):
apply: bool = True,
version_prefix: str = "/v",
error_format: Optional[str] = None,
**ctx_kwargs: Any,
**ctx_kwargs,
):
"""
Decorate a function to be registered as a websocket route
@@ -651,7 +633,7 @@ class RouteMixin(metaclass=SanicMeta):
name: Optional[str] = None,
version_prefix: str = "/v",
error_format: Optional[str] = None,
**ctx_kwargs: Any,
**ctx_kwargs,
):
"""
A helper method to register a function as a websocket route.
@@ -688,18 +670,18 @@ class RouteMixin(metaclass=SanicMeta):
def static(
self,
uri: str,
uri,
file_or_directory: Union[str, bytes, PurePath],
pattern: str = r"/?.+",
use_modified_since: bool = True,
use_content_range: bool = False,
stream_large_files: bool = False,
name: str = "static",
host: Optional[str] = None,
strict_slashes: Optional[bool] = None,
content_type: Optional[bool] = None,
apply: bool = True,
resource_type: Optional[str] = None,
pattern=r"/?.+",
use_modified_since=True,
use_content_range=False,
stream_large_files=False,
name="static",
host=None,
strict_slashes=None,
content_type=None,
apply=True,
resource_type=None,
):
"""
Register a root to serve files from. The input can either be a
@@ -783,37 +765,6 @@ class RouteMixin(metaclass=SanicMeta):
return name
async def _get_file_path(self, file_or_directory, __file_uri__, not_found):
file_path_raw = Path(unquote(file_or_directory))
root_path = file_path = file_path_raw.resolve()
if __file_uri__:
# Strip all / that in the beginning of the URL to help prevent
# python from herping a derp and treating the uri as an
# 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:
file_path.relative_to(root_path)
except ValueError:
if not file_path_raw.is_symlink():
error_logger.exception(
f"File not found: path={file_or_directory}, "
f"relative_url={__file_uri__}"
)
raise not_found
return file_path
async def _static_request_handler(
self,
file_or_directory,
@@ -824,17 +775,32 @@ class RouteMixin(metaclass=SanicMeta):
content_type=None,
__file_uri__=None,
):
not_found = FileNotFound(
"File not found",
path=file_or_directory,
relative_url=__file_uri__,
)
# 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
file_path = await self._get_file_path(
file_or_directory, __file_uri__, not_found
)
# Strip all / that in the beginning of the URL to help prevent python
# from herping a derp and treating the uri as an absolute path
root_path = file_path = file_or_directory
if __file_uri__:
file_path = path.join(
file_or_directory, sub("^[/]*", "", __file_uri__)
)
# URL decode the path sent by the browser otherwise we won't be able to
# match filenames which got encoded (filenames with spaces etc)
file_path = path.abspath(unquote(file_path))
if not file_path.startswith(path.abspath(unquote(root_path))):
error_logger.exception(
f"File not found: path={file_or_directory}, "
f"relative_url={__file_uri__}"
)
raise FileNotFound(
"File not found",
path=file_or_directory,
relative_url=__file_uri__,
)
try:
headers = {}
# Check if the client has been sent this file before
@@ -842,13 +808,15 @@ class RouteMixin(metaclass=SanicMeta):
stats = None
if use_modified_since:
stats = await stat_async(file_path)
modified_since = stats.st_mtime
response = await validate_file(request.headers, modified_since)
if response:
return response
headers["Last-Modified"] = formatdate(
modified_since, usegmt=True
modified_since = strftime(
"%a, %d %b %Y %H:%M:%S GMT", gmtime(stats.st_mtime)
)
if (
request.headers.getone("if-modified-since", None)
== modified_since
):
return HTTPResponse(status=304)
headers["Last-Modified"] = modified_since
_range = None
if use_content_range:
_range = None
@@ -863,7 +831,8 @@ class RouteMixin(metaclass=SanicMeta):
pass
else:
del headers["Content-Length"]
headers.update(_range.headers)
for key, value in _range.headers.items():
headers[key] = value
if "content-type" not in headers:
content_type = (
@@ -896,10 +865,14 @@ class RouteMixin(metaclass=SanicMeta):
file_path, headers=headers, _range=_range
)
return await file(file_path, headers=headers, _range=_range)
except RangeNotSatisfiable:
except ContentRangeError:
raise
except FileNotFoundError:
raise not_found
raise FileNotFound(
"File not found",
path=file_or_directory,
relative_url=__file_uri__,
)
except Exception:
error_logger.exception(
f"Exception in static request handler: "
@@ -956,7 +929,6 @@ class RouteMixin(metaclass=SanicMeta):
# serve from the folder
if not static.resource_type:
if not path.isfile(file_or_directory):
uri = uri.rstrip("/")
uri += "/<__file_uri__:path>"
elif static.resource_type == "dir":
if path.isfile(file_or_directory):
@@ -964,7 +936,6 @@ class RouteMixin(metaclass=SanicMeta):
"Resource type improperly identified as directory. "
f"'{file_or_directory}'"
)
uri = uri.rstrip("/")
uri += "/<__file_uri__:path>"
elif static.resource_type == "file" and not path.isfile(
file_or_directory
@@ -1023,6 +994,17 @@ class RouteMixin(metaclass=SanicMeta):
nonlocal types
with suppress(AttributeError):
if node.value.func.id == "stream": # type: ignore
deprecation(
"The sanic.response.stream method has been "
"deprecated and will be removed in v22.6. Please "
"upgrade your application to use the new style "
"streaming pattern. See "
"https://sanicframework.org/en/guide/advanced/"
"streaming.html#response-streaming for more "
"information.",
22.6,
)
checks = [node.value.func.id] # type: ignore
if node.value.keywords: # type: ignore
checks += [
@@ -1039,12 +1021,24 @@ class RouteMixin(metaclass=SanicMeta):
return types
def _build_route_context(self, raw: Dict[str, Any]) -> HashableDict:
def _build_route_context(self, raw):
ctx_kwargs = {
key.replace("ctx_", ""): raw.pop(key)
for key in {**raw}.keys()
if key.startswith("ctx_")
}
restricted = [
key for key in ctx_kwargs.keys() if key in RESTRICTED_ROUTE_CONTEXT
]
if restricted:
restricted_arguments = ", ".join(restricted)
raise AttributeError(
"Cannot use restricted route context: "
f"{restricted_arguments}. This limitation is only in place "
"until v22.3 when the restricted names will no longer be in"
"conflict. See https://github.com/sanic-org/sanic/issues/2303 "
"for more information."
)
if raw:
unexpected_arguments = ", ".join(raw.keys())
raise TypeError(

View File

@@ -16,94 +16,44 @@ from asyncio import (
from contextlib import suppress
from functools import partial
from importlib import import_module
from multiprocessing import Manager, Pipe, get_context
from multiprocessing.context import BaseContext
from pathlib import Path
from socket import SHUT_RDWR, socket
from socket import socket
from ssl import SSLContext
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
List,
Optional,
Set,
Tuple,
Type,
Union,
cast,
)
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Type, Union
from sanic.application.ext import setup_ext
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, is_atty
from sanic.constants import RestartOrder
from sanic.exceptions import ServerKilled
from sanic.helpers import Default
from sanic.http.constants import HTTP
from sanic.http.tls import get_ssl_context, process_to_context
from sanic.http.tls.context import SanicSSLContext
from sanic.log import Colors, deprecation, error_logger, logger
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.legacy import watchdog
from sanic.server.loop import try_windows_loop
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.server.socket import configure_socket, remove_unix_socket
from sanic.worker.inspector import Inspector
from sanic.worker.loader import AppLoader
from sanic.worker.manager import WorkerManager
from sanic.worker.multiplexer import WorkerMultiplexer
from sanic.worker.reloader import Reloader
from sanic.worker.serve import worker_serve
from sanic.tls import process_to_context
if TYPE_CHECKING:
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")
if sys.version_info < (3, 8): # no cov
HTTPVersion = Union[HTTP, int]
else: # no cov
from typing import Literal
HTTPVersion = Union[HTTP, Literal[1], Literal[3]]
class StartupMixin(metaclass=SanicMeta):
class RunnerMixin(metaclass=SanicMeta):
_app_registry: Dict[str, Sanic]
config: Config
listeners: Dict[str, List[ListenerType[Any]]]
state: ApplicationState
websocket_enabled: bool
multiplexer: WorkerMultiplexer
def setup_loop(self):
if not self.asgi:
if self.config.USE_UVLOOP is True or (
isinstance(self.config.USE_UVLOOP, Default)
and not OS_IS_WINDOWS
):
try_use_uvloop()
elif OS_IS_WINDOWS:
try_windows_loop()
@property
def m(self) -> WorkerMultiplexer:
"""Interface for interacting with the worker processes"""
return self.multiplexer
def make_coffee(self, *args, **kwargs):
self.state.coffee = True
@@ -117,7 +67,6 @@ class StartupMixin(metaclass=SanicMeta):
dev: bool = False,
debug: bool = False,
auto_reload: Optional[bool] = None,
version: HTTPVersion = HTTP.VERSION_1,
ssl: Union[None, SSLContext, dict, str, list, tuple] = None,
sock: Optional[socket] = None,
workers: int = 1,
@@ -133,9 +82,6 @@ class StartupMixin(metaclass=SanicMeta):
fast: bool = False,
verbosity: int = 0,
motd_display: Optional[Dict[str, str]] = None,
auto_tls: bool = False,
single_process: bool = False,
legacy: bool = False,
) -> None:
"""
Run the HTTP Server and listen until keyboard interrupt or term
@@ -179,7 +125,6 @@ class StartupMixin(metaclass=SanicMeta):
dev=dev,
debug=debug,
auto_reload=auto_reload,
version=version,
ssl=ssl,
sock=sock,
workers=workers,
@@ -195,18 +140,9 @@ class StartupMixin(metaclass=SanicMeta):
fast=fast,
verbosity=verbosity,
motd_display=motd_display,
auto_tls=auto_tls,
single_process=single_process,
legacy=legacy,
)
if single_process:
serve = self.__class__.serve_single
elif legacy:
serve = self.__class__.serve_legacy
else:
serve = self.__class__.serve
serve(primary=self) # type: ignore
self.__class__.serve(primary=self) # type: ignore
def prepare(
self,
@@ -216,7 +152,6 @@ class StartupMixin(metaclass=SanicMeta):
dev: bool = False,
debug: bool = False,
auto_reload: Optional[bool] = None,
version: HTTPVersion = HTTP.VERSION_1,
ssl: Union[None, SSLContext, dict, str, list, tuple] = None,
sock: Optional[socket] = None,
workers: int = 1,
@@ -232,25 +167,11 @@ class StartupMixin(metaclass=SanicMeta):
fast: bool = False,
verbosity: int = 0,
motd_display: Optional[Dict[str, str]] = None,
coffee: bool = False,
auto_tls: bool = False,
single_process: bool = False,
legacy: bool = False,
) -> None:
if version == 3 and self.state.server_info:
raise RuntimeError(
"Serving HTTP/3 instances as a secondary server is "
"not supported. There can only be a single HTTP/3 worker "
"and it must be the first instance prepared."
)
if dev:
debug = True
auto_reload = True
if debug and access_log is None:
access_log = True
self.state.verbosity = verbosity
if not self.state.auto_reload:
self.state.auto_reload = bool(auto_reload)
@@ -258,21 +179,6 @@ class StartupMixin(metaclass=SanicMeta):
if fast and workers != 1:
raise RuntimeError("You cannot use both fast=True and workers=X")
if single_process and (fast or (workers > 1) or auto_reload):
raise RuntimeError(
"Single process cannot be run with multiple workers "
"or auto-reload"
)
if single_process and legacy:
raise RuntimeError("Cannot run single process and legacy mode")
if register_sys_signals is False and not (single_process or legacy):
raise RuntimeError(
"Cannot run Sanic.serve with register_sys_signals=False. "
"Use either Sanic.serve_single or Sanic.serve_legacy."
)
if motd_display:
self.config.MOTD_DISPLAY.update(motd_display)
@@ -296,8 +202,14 @@ class StartupMixin(metaclass=SanicMeta):
"#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 = self.get_address(host, port, version, auto_tls)
host, port = host or "127.0.0.1", port or 8000
if protocol is None:
protocol = (
@@ -321,14 +233,10 @@ class StartupMixin(metaclass=SanicMeta):
except AttributeError: # no cov
workers = os.cpu_count() or 1
if coffee:
self.state.coffee = True
server_settings = self._helper(
host=host,
port=port,
debug=debug,
version=version,
ssl=ssl,
sock=sock,
unix=unix,
@@ -336,16 +244,15 @@ class StartupMixin(metaclass=SanicMeta):
protocol=protocol,
backlog=backlog,
register_sys_signals=register_sys_signals,
auto_tls=auto_tls,
)
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()
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,
@@ -406,7 +313,7 @@ class StartupMixin(metaclass=SanicMeta):
"""
if sock is None:
host, port = host, port = self.get_address(host, port)
host, port = host or "127.0.0.1", port or 8000
if protocol is None:
protocol = (
@@ -434,7 +341,7 @@ class StartupMixin(metaclass=SanicMeta):
run_async=return_asyncio_server,
)
if not isinstance(self.config.USE_UVLOOP, Default):
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. "
@@ -454,29 +361,23 @@ class StartupMixin(metaclass=SanicMeta):
asyncio_server_kwargs=asyncio_server_kwargs, **server_settings
)
def stop(self, terminate: bool = True, unregister: bool = False):
def stop(self):
"""
This kills the Sanic
"""
if terminate and hasattr(self, "multiplexer"):
self.multiplexer.terminate()
if self.state.stage is not ServerStage.STOPPED:
self.shutdown_tasks(timeout=0) # type: ignore
self.shutdown_tasks(timeout=0)
for task in all_tasks():
with suppress(AttributeError):
if task.get_name() == "RunServer":
task.cancel()
get_event_loop().stop()
if unregister:
self.__class__.unregister_app(self) # type: ignore
def _helper(
self,
host: Optional[str] = None,
port: Optional[int] = None,
debug: bool = False,
version: HTTPVersion = HTTP.VERSION_1,
ssl: Union[None, SSLContext, dict, str, list, tuple] = None,
sock: Optional[socket] = None,
unix: Optional[str] = None,
@@ -486,7 +387,6 @@ class StartupMixin(metaclass=SanicMeta):
backlog: int = 100,
register_sys_signals: bool = True,
run_async: bool = False,
auto_tls: bool = False,
) -> Dict[str, Any]:
"""Helper function used by `run` and `create_server`."""
if self.config.PROXIES_COUNT and self.config.PROXIES_COUNT < 0:
@@ -496,18 +396,11 @@ class StartupMixin(metaclass=SanicMeta):
"#proxy-configuration"
)
ssl = process_to_context(ssl)
if not self.state.is_debug:
self.state.mode = Mode.DEBUG if debug else Mode.PRODUCTION
if isinstance(version, int):
version = HTTP(version)
ssl = process_to_context(ssl)
if version is HTTP.VERSION_3 or auto_tls:
if TYPE_CHECKING:
self = cast(Sanic, self)
ssl = get_ssl_context(self, ssl)
self.state.host = host or ""
self.state.port = port or 0
self.state.workers = workers
@@ -519,7 +412,6 @@ class StartupMixin(metaclass=SanicMeta):
"protocol": protocol,
"host": host,
"port": port,
"version": version,
"sock": sock,
"unix": unix,
"ssl": ssl,
@@ -530,13 +422,9 @@ class StartupMixin(metaclass=SanicMeta):
"backlog": backlog,
}
self.motd(server_settings=server_settings)
self.motd(self.serve_location)
if (
is_atty()
and not self.state.is_debug
and not os.environ.get("SANIC_IGNORE_PRODUCTION_WARNING")
):
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 "
@@ -560,394 +448,90 @@ class StartupMixin(metaclass=SanicMeta):
return server_settings
def motd(
self,
server_settings: Optional[Dict[str, Any]] = None,
):
if (
os.environ.get("SANIC_WORKER_NAME")
or os.environ.get("SANIC_MOTD_OUTPUT")
or os.environ.get("SANIC_WORKER_PROCESS")
or os.environ.get("SANIC_SERVER_RUNNING")
):
return
serve_location = self.get_server_location(server_settings)
def motd(self, serve_location):
if self.config.MOTD:
logo = get_logo(coffee=self.state.coffee)
display, extra = self.get_motd_data(server_settings)
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)
def get_motd_data(
self, server_settings: Optional[Dict[str, Any]] = None
) -> Tuple[Dict[str, Any], Dict[str, Any]]:
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")
if server_settings:
server = ", ".join(
(
self.state.server,
server_settings["version"].display(), # type: ignore
)
)
else:
server = "ASGI" if self.asgi else "unknown" # type: ignore
display = {
"mode": " ".join(mode),
"server": 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__}" # type: ignore
)
except ImportError: # no cov
...
if packages:
display["packages"] = ", ".join(packages)
if self.config.MOTD_DISPLAY:
extra.update(self.config.MOTD_DISPLAY)
return display, extra
@property
def serve_location(self) -> str:
try:
server_settings = self.state.server_info[0].settings
return self.get_server_location(server_settings)
except IndexError:
location = "ASGI" if self.asgi else "unknown" # type: ignore
return f"http://<{location}>"
@staticmethod
def get_server_location(
server_settings: Optional[Dict[str, Any]] = None
) -> str:
serve_location = ""
proto = "http"
if not server_settings:
return serve_location
host = server_settings["host"]
port = server_settings["port"]
if server_settings.get("ssl") is not None:
if self.state.ssl is not None:
proto = "https"
if server_settings.get("unix"):
serve_location = f'{server_settings["unix"]} {proto}://...'
elif server_settings.get("sock"):
host, port, *_ = server_settings["sock"].getsockname()
if not serve_location and host and port:
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"[{host}]" if ":" in host else host
serve_location = f"{proto}://{display_host}:{port}"
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
@staticmethod
def get_address(
host: Optional[str],
port: Optional[int],
version: HTTPVersion = HTTP.VERSION_1,
auto_tls: bool = False,
) -> Tuple[str, int]:
host = host or "127.0.0.1"
port = port or (8443 if (version == 3 or auto_tls) else 8000)
return host, port
@classmethod
def should_auto_reload(cls) -> bool:
return any(app.state.auto_reload for app in cls._app_registry.values())
@classmethod
def _get_context(cls) -> BaseContext:
method = (
"spawn"
if "linux" not in sys.platform or cls.should_auto_reload()
else "fork"
)
return get_context(method)
@classmethod
def serve(
cls,
primary: Optional[Sanic] = None,
*,
app_loader: Optional[AppLoader] = None,
factory: Optional[Callable[[], Sanic]] = None,
) -> None:
os.environ["SANIC_MOTD_OUTPUT"] = "true"
apps = list(cls._app_registry.values())
if factory:
primary = factory()
else:
if not primary:
if app_loader:
primary = app_loader.load()
if not primary:
try:
primary = apps[0]
except IndexError:
raise RuntimeError(
"Did not find any applications."
) from None
# This exists primarily for unit testing
if not primary.state.server_info: # no cov
for app in apps:
app.state.server_info.clear()
return
try:
primary_server_info = primary.state.server_info[0]
except IndexError:
raise RuntimeError(
f"No server information found for {primary.name}. Perhaps you "
"need to run app.prepare(...)?"
) from None
socks = []
sync_manager = Manager()
setup_ext(primary)
exit_code = 0
try:
primary_server_info.settings.pop("main_start", None)
primary_server_info.settings.pop("main_stop", None)
main_start = primary.listeners.get("main_process_start")
main_stop = primary.listeners.get("main_process_stop")
app = primary_server_info.settings.pop("app")
app.setup_loop()
loop = new_event_loop()
trigger_events(main_start, loop, primary)
socks = [
sock
for sock in [
configure_socket(server_info.settings)
for app in apps
for server_info in app.state.server_info
]
if sock
]
primary_server_info.settings["run_multiple"] = True
monitor_sub, monitor_pub = Pipe(True)
worker_state: Dict[str, Any] = sync_manager.dict()
kwargs: Dict[str, Any] = {
**primary_server_info.settings,
"monitor_publisher": monitor_pub,
"worker_state": worker_state,
}
if not app_loader:
if factory:
app_loader = AppLoader(factory=factory)
else:
app_loader = AppLoader(
factory=partial(cls.get_app, app.name) # type: ignore
)
kwargs["app_name"] = app.name
kwargs["app_loader"] = app_loader
kwargs["server_info"] = {}
kwargs["passthru"] = {
"auto_reload": app.auto_reload,
"state": {
"verbosity": app.state.verbosity,
"mode": app.state.mode,
},
"config": {
"ACCESS_LOG": app.config.ACCESS_LOG,
"NOISY_EXCEPTIONS": app.config.NOISY_EXCEPTIONS,
},
"shared_ctx": app.shared_ctx.__dict__,
}
for app in apps:
kwargs["server_info"][app.name] = []
for server_info in app.state.server_info:
server_info.settings = {
k: v
for k, v in server_info.settings.items()
if k not in ("main_start", "main_stop", "app", "ssl")
}
kwargs["server_info"][app.name].append(server_info)
ssl = kwargs.get("ssl")
if isinstance(ssl, SanicSSLContext):
kwargs["ssl"] = kwargs["ssl"].sanic
manager = WorkerManager(
primary.state.workers,
worker_serve,
kwargs,
cls._get_context(),
(monitor_pub, monitor_sub),
worker_state,
cast(RestartOrder, primary.config.RESTART_ORDER),
)
if cls.should_auto_reload():
reload_dirs: Set[Path] = primary.state.reload_dirs.union(
*(app.state.reload_dirs for app in apps)
)
reloader = Reloader(monitor_pub, 1.0, reload_dirs, app_loader)
manager.manage("Reloader", reloader, {}, transient=False)
inspector = None
if primary.config.INSPECTOR:
display, extra = primary.get_motd_data()
packages = [
pkg.strip() for pkg in display["packages"].split(",")
]
module = import_module("sanic")
sanic_version = f"sanic=={module.__version__}" # type: ignore
app_info = {
**display,
"packages": [sanic_version, *packages],
"extra": extra,
}
inspector = Inspector(
monitor_pub,
app_info,
worker_state,
primary.config.INSPECTOR_HOST,
primary.config.INSPECTOR_PORT,
)
manager.manage("Inspector", inspector, {}, transient=False)
primary._inspector = inspector
primary._manager = manager
ready = primary.listeners["main_process_ready"]
trigger_events(ready, loop, primary)
manager.run()
except ServerKilled:
exit_code = 1
except BaseException:
kwargs = primary_server_info.settings
error_logger.exception(
"Experienced exception while trying to serve"
)
raise
finally:
logger.info("Server Stopped")
for app in apps:
app.state.server_info.clear()
app.router.reset()
app.signal_router.reset()
sync_manager.shutdown()
for sock in socks:
sock.shutdown(SHUT_RDWR)
sock.close()
socks = []
trigger_events(main_stop, loop, primary)
loop.close()
cls._cleanup_env_vars()
cls._cleanup_apps()
unix = kwargs.get("unix")
if unix:
remove_unix_socket(unix)
if exit_code:
os._exit(exit_code)
@classmethod
def serve_single(cls, primary: Optional[Sanic] = None) -> None:
os.environ["SANIC_MOTD_OUTPUT"] = "true"
apps = list(cls._app_registry.values())
if not primary:
try:
primary = apps[0]
except IndexError:
raise RuntimeError("Did not find any applications.")
# 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))
kwargs = {
k: v
for k, v in primary_server_info.settings.items()
if k
not in (
"main_start",
"main_stop",
"app",
)
}
kwargs["app_name"] = primary.name
kwargs["app_loader"] = None
sock = configure_socket(kwargs)
kwargs["server_info"] = {}
kwargs["server_info"][primary.name] = []
for server_info in primary.state.server_info:
server_info.settings = {
k: v
for k, v in server_info.settings.items()
if k not in ("main_start", "main_stop", "app")
}
kwargs["server_info"][primary.name].append(server_info)
try:
worker_serve(monitor_publisher=None, **kwargs)
except BaseException:
error_logger.exception(
"Experienced exception while trying to serve"
)
raise
finally:
logger.info("Server Stopped")
for app in apps:
app.state.server_info.clear()
app.router.reset()
app.signal_router.reset()
if sock:
sock.close()
cls._cleanup_env_vars()
cls._cleanup_apps()
@classmethod
def serve_legacy(cls, primary: Optional[Sanic] = None) -> None:
def serve(cls, primary: Optional[Sanic] = None) -> None:
apps = list(cls._app_registry.values())
if not primary:
@@ -968,7 +552,7 @@ class StartupMixin(metaclass=SanicMeta):
reload_dirs: Set[Path] = primary.state.reload_dirs.union(
*(app.state.reload_dirs for app in apps)
)
watchdog(1.0, reload_dirs)
reloader_helpers.watchdog(1.0, reload_dirs)
trigger_events(reloader_stop, loop, primary)
return
@@ -981,17 +565,11 @@ class StartupMixin(metaclass=SanicMeta):
primary_server_info = primary.state.server_info[0]
primary.before_server_start(partial(primary._start_servers, apps=apps))
deprecation(
f"{Colors.YELLOW}Running {Colors.SANIC}Sanic {Colors.YELLOW}w/ "
f"LEGACY manager.{Colors.END} Support for will be dropped in "
"version 23.3.",
23.3,
)
try:
primary_server_info.stage = ServerStage.SERVING
if primary.state.workers > 1 and os.name != "posix": # no cov
logger.warning(
logger.warn(
f"Multiprocessing is currently not supported on {os.name},"
" using workers=1 instead"
)
@@ -1012,9 +590,10 @@ class StartupMixin(metaclass=SanicMeta):
finally:
primary_server_info.stage = ServerStage.STOPPED
logger.info("Server Stopped")
cls._cleanup_env_vars()
cls._cleanup_apps()
for app in apps:
app.state.server_info.clear()
app.router.reset()
app.signal_router.reset()
async def _start_servers(
self,
@@ -1036,7 +615,7 @@ class StartupMixin(metaclass=SanicMeta):
f"{app.state.workers} worker(s), which will be ignored "
"in favor of the primary application."
)
if is_atty():
if sys.stdout.isatty():
message = "".join(
[
Colors.YELLOW,
@@ -1052,7 +631,7 @@ class StartupMixin(metaclass=SanicMeta):
*server_info.settings.pop("main_start", []),
*server_info.settings.pop("main_stop", []),
]
if handlers: # no cov
if handlers:
error_logger.warning(
f"Sanic found {len(handlers)} listener(s) on "
"secondary applications attached to the main "
@@ -1065,22 +644,19 @@ class StartupMixin(metaclass=SanicMeta):
if not server_info.settings["loop"]:
server_info.settings["loop"] = get_running_loop()
serve_args: Dict[str, Any] = {
**server_info.settings,
"run_async": True,
"reuse_port": bool(primary.state.workers - 1),
}
if "app" not in serve_args:
serve_args["app"] = app
try:
server_info.server = await serve(**serve_args)
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 is_atty():
if sys.stdout.isatty():
message_parts = [
Colors.YELLOW,
first_message,
@@ -1099,9 +675,9 @@ class StartupMixin(metaclass=SanicMeta):
async def _run_server(
self,
app: StartupMixin,
app: RunnerMixin,
server_info: ApplicationServerInfo,
) -> None: # no cov
) -> None:
try:
# We should never get to this point without a server
@@ -1125,26 +701,3 @@ class StartupMixin(metaclass=SanicMeta):
finally:
server_info.stage = ServerStage.STOPPED
server_info.server = None
@staticmethod
def _cleanup_env_vars():
variables = (
"SANIC_RELOADER_PROCESS",
"SANIC_IGNORE_PRODUCTION_WARNING",
"SANIC_WORKER_NAME",
"SANIC_MOTD_OUTPUT",
"SANIC_WORKER_PROCESS",
"SANIC_SERVER_RUNNING",
)
for var in variables:
try:
del os.environ[var]
except KeyError:
...
@classmethod
def _cleanup_apps(cls):
for app in cls._app_registry.values():
app.state.server_info.clear()
app.router.reset()
app.signal_router.reset()

View File

@@ -3,8 +3,7 @@ import sys
from typing import Any, Awaitable, Callable, MutableMapping, Optional, Union
from sanic.exceptions import BadRequest
from sanic.models.protocol_types import TransportProtocol
from sanic.exceptions import InvalidUsage
from sanic.server.websockets.connection import WebSocketConnection
@@ -57,7 +56,7 @@ class MockProtocol: # no cov
await self._not_paused.wait()
class MockTransport(TransportProtocol): # no cov
class MockTransport: # no cov
_protocol: Optional[MockProtocol]
def __init__(
@@ -69,25 +68,23 @@ class MockTransport(TransportProtocol): # no cov
self._protocol = None
self.loop = None
def get_protocol(self) -> MockProtocol: # type: ignore
def get_protocol(self) -> MockProtocol:
if not self._protocol:
self._protocol = MockProtocol(self, self.loop)
return self._protocol
def get_extra_info(
self, info: str, default=None
) -> Optional[Union[str, bool]]:
def get_extra_info(self, info: str) -> Union[str, bool, None]:
if info == "peername":
return self.scope.get("client")
elif info == "sslcontext":
return self.scope.get("scheme") in ["https", "wss"]
return default
return None
def get_websocket_connection(self) -> WebSocketConnection:
try:
return self._websocket_connection
except AttributeError:
raise BadRequest("Improper websocket connection.")
raise InvalidUsage("Improper websocket connection.")
def create_websocket_connection(
self, send: ASGISend, receive: ASGIReceive

View File

@@ -1,22 +1,28 @@
from __future__ import annotations
import sys
from asyncio import BaseTransport
from typing import TYPE_CHECKING, Any, AnyStr
if TYPE_CHECKING:
from sanic.models.asgi import ASGIScope
from typing import Any, AnyStr, TypeVar, Union
if sys.version_info < (3, 8):
from asyncio import BaseTransport
# from sanic.models.asgi import MockTransport
MockTransport = TypeVar("MockTransport")
TransportProtocol = Union[MockTransport, BaseTransport]
Range = Any
HTMLProtocol = Any
else:
# Protocol is a 3.8+ feature
from typing import Protocol
class TransportProtocol(Protocol):
def get_protocol(self):
...
def get_extra_info(self, info: str) -> Union[str, bool, None]:
...
class HTMLProtocol(Protocol):
def __html__(self) -> AnyStr:
...
@@ -36,8 +42,3 @@ else:
def total(self) -> int:
...
class TransportProtocol(BaseTransport):
scope: ASGIScope
__slots__ = ()

View File

@@ -1,8 +1,8 @@
from __future__ import annotations
from ssl import SSLContext, SSLObject
from ssl import SSLObject
from types import SimpleNamespace
from typing import Any, Dict, List, Optional
from typing import Any, Dict, Optional
from sanic.models.protocol_types import TransportProtocol
@@ -21,7 +21,6 @@ class ConnInfo:
"client",
"client_ip",
"ctx",
"lost",
"peername",
"server_port",
"server",
@@ -29,12 +28,10 @@ class ConnInfo:
"sockname",
"ssl",
"cert",
"network_paths",
)
def __init__(self, transport: TransportProtocol, unix=None):
self.ctx = SimpleNamespace()
self.lost = False
self.peername = None
self.server = self.client = ""
self.server_port = self.client_port = 0
@@ -43,22 +40,17 @@ class ConnInfo:
self.ssl = False
self.server_name = ""
self.cert: Dict[str, Any] = {}
self.network_paths: List[Any] = []
sslobj: Optional[SSLObject] = transport.get_extra_info(
"ssl_object"
) # type: ignore
sslctx: Optional[SSLContext] = transport.get_extra_info(
"ssl_context"
) # type: ignore
if sslobj:
self.ssl = True
self.server_name = getattr(sslobj, "sanic_server_name", None) or ""
self.cert = dict(getattr(sslobj.context, "sanic", {}))
if sslctx and not self.cert:
self.cert = dict(getattr(sslctx, "sanic", {}))
if isinstance(addr, str): # UNIX socket
self.server = unix or addr
return
# IPv4 (ip, port) or IPv6 (ip, port, flowinfo, scopeid)
if isinstance(addr, tuple):
self.server = addr[0] if len(addr) == 2 else f"[{addr[0]}]"
@@ -67,9 +59,6 @@ class ConnInfo:
if addr[1] != (443 if self.ssl else 80):
self.server = f"{self.server}:{addr[1]}"
self.peername = addr = transport.get_extra_info("peername")
self.network_paths = transport.get_extra_info( # type: ignore
"network_paths"
)
if isinstance(addr, tuple):
self.client = addr[0] if len(addr) == 2 else f"[{addr[0]}]"

View File

@@ -9,6 +9,7 @@ from time import sleep
def _iter_module_files():
"""This iterates over all relevant Python files.
It goes through all
loaded files from modules, all files in folders of already loaded modules
as well as all files reachable through a package.
@@ -51,7 +52,7 @@ def restart_with_reloader(changed=None):
this one.
"""
reloaded = ",".join(changed) if changed else ""
return subprocess.Popen( # nosec B603
return subprocess.Popen(
_get_args_for_reloading(),
env={
**os.environ,
@@ -78,6 +79,7 @@ def _check_file(filename, mtimes):
def watchdog(sleep_interval, reload_dirs):
"""Watch project files, restart worker process if a change happened.
:param sleep_interval: interval in second.
:return: Nothing
"""

View File

@@ -1,7 +1,5 @@
from __future__ import annotations
from contextvars import ContextVar
from inspect import isawaitable
from typing import (
TYPE_CHECKING,
Any,
@@ -14,15 +12,12 @@ from typing import (
Union,
)
from sanic_routing.route import Route
from sanic_routing.route import Route # type: ignore
from sanic.http.constants import HTTP # type: ignore
from sanic.http.stream import Stream
from sanic.models.asgi import ASGIScope
from sanic.models.http_types import Credentials
if TYPE_CHECKING:
if TYPE_CHECKING: # no cov
from sanic.server import ConnInfo
from sanic.app import Sanic
@@ -34,17 +29,12 @@ from http.cookies import SimpleCookie
from types import SimpleNamespace
from urllib.parse import parse_qs, parse_qsl, unquote, urlunparse
from httptools import parse_url
from httptools.parser.errors import HttpParserInvalidURLError
from httptools import parse_url # type: ignore
from httptools.parser.errors import HttpParserInvalidURLError # type: ignore
from sanic.compat import CancelledErrors, Header
from sanic.constants import (
CACHEABLE_HTTP_METHODS,
DEFAULT_HTTP_CONTENT_TYPE,
IDEMPOTENT_HTTP_METHODS,
SAFE_HTTP_METHODS,
)
from sanic.exceptions import BadRequest, BadURL, ServerError
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE
from sanic.exceptions import BadURL, InvalidUsage, ServerError
from sanic.headers import (
AcceptContainer,
Options,
@@ -55,8 +45,8 @@ from sanic.headers import (
parse_host,
parse_xforwarded,
)
from sanic.http import Stage
from sanic.log import deprecation, error_logger, logger
from sanic.http import Http, Stage
from sanic.log import error_logger, logger
from sanic.models.protocol_types import TransportProtocol
from sanic.response import BaseHTTPResponse, HTTPResponse
@@ -91,9 +81,6 @@ class Request:
Properties of an HTTP request such as URL, headers, etc.
"""
_current: ContextVar[Request] = ContextVar("request")
_loads = json_loads
__slots__ = (
"__weakref__",
"_cookies",
@@ -103,11 +90,7 @@ class Request:
"_port",
"_protocol",
"_remote_addr",
"_request_middleware_started",
"_response_middleware_started",
"_scheme",
"_socket",
"_stream_id",
"_match_info",
"_name",
"app",
@@ -128,6 +111,7 @@ class Request:
"parsed_token",
"raw_url",
"responded",
"request_middleware_started",
"route",
"stream",
"transport",
@@ -143,7 +127,6 @@ class Request:
transport: TransportProtocol,
app: Sanic,
head: bytes = b"",
stream_id: int = 0,
):
self.raw_url = url_bytes
@@ -153,7 +136,6 @@ class Request:
raise BadURL(f"Bad URL: {url_bytes.decode()}")
self._id: Optional[Union[uuid.UUID, str, int]] = None
self._name: Optional[str] = None
self._stream_id = stream_id
self.app = app
self.headers = Header(headers)
@@ -170,8 +152,8 @@ class Request:
self.parsed_accept: Optional[AcceptContainer] = None
self.parsed_credentials: Optional[Credentials] = None
self.parsed_json = None
self.parsed_form: Optional[RequestParameters] = None
self.parsed_files: Optional[RequestParameters] = None
self.parsed_form = None
self.parsed_files = None
self.parsed_token: Optional[str] = None
self.parsed_args: DefaultDict[
Tuple[bool, bool, str, str], RequestParameters
@@ -179,72 +161,22 @@ class Request:
self.parsed_not_grouped_args: DefaultDict[
Tuple[bool, bool, str, str], List[Tuple[str, str]]
] = defaultdict(list)
self._request_middleware_started = False
self._response_middleware_started = False
self.responded: bool = False
self.route: Optional[Route] = None
self.stream: Optional[Stream] = None
self.request_middleware_started = False
self._cookies: Optional[Dict[str, str]] = None
self._match_info: Dict[str, Any] = {}
self.stream: Optional[Http] = None
self.route: Optional[Route] = None
self._protocol = None
self.responded: bool = False
def __repr__(self):
class_name = self.__class__.__name__
return f"<{class_name}: {self.method} {self.path}>"
@classmethod
def get_current(cls) -> Request:
"""
Retrieve the current request object
This implements `Context Variables
<https://docs.python.org/3/library/contextvars.html>`_
to allow for accessing the current request from anywhere.
Raises :exc:`sanic.exceptions.ServerError` if it is outside of
a request lifecycle.
.. code-block:: python
from sanic import Request
current_request = Request.get_current()
:return: the current :class:`sanic.request.Request`
"""
request = cls._current.get(None)
if not request:
raise ServerError("No current request")
return request
@classmethod
def generate_id(*_):
return uuid.uuid4()
@property
def request_middleware_started(self):
deprecation(
"Request.request_middleware_started has been deprecated and will"
"be removed. You should set a flag on the request context using"
"either middleware or signals if you need this feature.",
23.3,
)
return self._request_middleware_started
@property
def stream_id(self):
"""
Access the HTTP/3 stream ID.
Raises :exc:`sanic.exceptions.ServerError` if it is not an
HTTP/3 request.
"""
if self.protocol.version is not HTTP.VERSION_3:
raise ServerError(
"Stream ID is only a property of a HTTP/3 request"
)
return self._stream_id
def reset_response(self):
try:
if (
@@ -331,19 +263,11 @@ class Request:
# Connect the response
if isinstance(response, BaseHTTPResponse) and self.stream:
response = self.stream.respond(response)
if isawaitable(response):
response = await response # type: ignore
# Run response middleware
try:
middleware = (
self.route and self.route.extra.response_middleware
) or self.app.response_middleware
if middleware and not self._response_middleware_started:
self._response_middleware_started = True
response = await self.app._run_response_middleware(
self, response, middleware
)
response = await self.app._run_response_middleware(
self, response, request_name=self.name
)
except CancelledErrors:
raise
except Exception:
@@ -366,19 +290,7 @@ class Request:
self.body = b"".join([data async for data in self.stream])
@property
def name(self) -> Optional[str]:
"""
The route name
In the following pattern:
.. code-block::
<AppName>.[<BlueprintName>.]<HandlerName>
:return: Route name
:rtype: Optional[str]
"""
def name(self):
if self._name:
return self._name
elif self.route:
@@ -386,47 +298,26 @@ class Request:
return None
@property
def endpoint(self) -> Optional[str]:
"""
:return: Alias of :attr:`sanic.request.Request.name`
:rtype: Optional[str]
"""
def endpoint(self):
return self.name
@property
def uri_template(self) -> Optional[str]:
"""
:return: The defined URI template
:rtype: Optional[str]
"""
if self.route:
return f"/{self.route.path}"
return None
def uri_template(self):
return f"/{self.route.path}"
@property
def protocol(self):
"""
:return: The HTTP protocol instance
"""
if not self._protocol:
self._protocol = self.transport.get_protocol()
return self._protocol
@property
def raw_headers(self) -> bytes:
"""
:return: The unparsed HTTP headers
:rtype: bytes
"""
def raw_headers(self):
_, headers = self.head.split(b"\r\n", 1)
return bytes(headers)
@property
def request_line(self) -> bytes:
"""
:return: The first line of a HTTP request
:rtype: bytes
"""
def request_line(self):
reqline, _ = self.head.split(b"\r\n", 1)
return bytes(reqline)
@@ -475,35 +366,24 @@ class Request:
return self._id # type: ignore
@property
def json(self) -> Any:
"""
:return: The request body parsed as JSON
:rtype: Any
"""
def json(self):
if self.parsed_json is None:
self.load_json()
return self.parsed_json
def load_json(self, loads=None):
def load_json(self, loads=json_loads):
try:
if not loads:
loads = self.__class__._loads
self.parsed_json = loads(self.body)
except Exception:
if not self.body:
return None
raise BadRequest("Failed when parsing body as json")
raise InvalidUsage("Failed when parsing body as json")
return self.parsed_json
@property
def accept(self) -> AcceptContainer:
"""
:return: The ``Accept`` header parsed
:rtype: AcceptContainer
"""
if self.parsed_accept is None:
accept_header = self.headers.getone("accept", "")
self.parsed_accept = parse_accept(accept_header)
@@ -546,60 +426,33 @@ class Request:
pass
return self.parsed_credentials
def get_form(
self, keep_blank_values: bool = False
) -> Optional[RequestParameters]:
"""
Method to extract and parse the form data from a request.
:param keep_blank_values:
Whether to discard blank values from the form data
:type keep_blank_values: bool
:return: the parsed form data
:rtype: Optional[RequestParameters]
"""
self.parsed_form = RequestParameters()
self.parsed_files = RequestParameters()
content_type = self.headers.getone(
"content-type", DEFAULT_HTTP_CONTENT_TYPE
)
content_type, parameters = parse_content_header(content_type)
try:
if content_type == "application/x-www-form-urlencoded":
self.parsed_form = RequestParameters(
parse_qs(
self.body.decode("utf-8"),
keep_blank_values=keep_blank_values,
)
)
elif content_type == "multipart/form-data":
# TODO: Stream this instead of reading to/from memory
boundary = parameters["boundary"].encode( # type: ignore
"utf-8"
) # type: ignore
self.parsed_form, self.parsed_files = parse_multipart_form(
self.body, boundary
)
except Exception:
error_logger.exception("Failed when parsing form")
return self.parsed_form
@property
def form(self):
"""
:return: The request body parsed as form data
"""
if self.parsed_form is None:
self.get_form()
self.parsed_form = RequestParameters()
self.parsed_files = RequestParameters()
content_type = self.headers.getone(
"content-type", DEFAULT_HTTP_CONTENT_TYPE
)
content_type, parameters = parse_content_header(content_type)
try:
if content_type == "application/x-www-form-urlencoded":
self.parsed_form = RequestParameters(
parse_qs(self.body.decode("utf-8"))
)
elif content_type == "multipart/form-data":
# TODO: Stream this instead of reading to/from memory
boundary = parameters["boundary"].encode("utf-8")
self.parsed_form, self.parsed_files = parse_multipart_form(
self.body, boundary
)
except Exception:
error_logger.exception("Failed when parsing form")
return self.parsed_form
@property
def files(self):
"""
:return: The request body parsed as uploaded files
"""
if self.parsed_files is None:
self.form # compute form to get files
@@ -613,8 +466,8 @@ class Request:
errors: str = "replace",
) -> RequestParameters:
"""
Method to parse ``query_string`` using ``urllib.parse.parse_qs``.
This methods is used by ``args`` property.
Method to parse `query_string` using `urllib.parse.parse_qs`.
This methods is used by `args` property.
Can be used directly if you need to change default parameters.
:param keep_blank_values:
@@ -663,10 +516,6 @@ class Request:
]
args = property(get_args)
"""
Convenience property to access :meth:`Request.get_args` with
default values.
"""
def get_query_args(
self,
@@ -786,9 +635,6 @@ class Request:
@property
def socket(self):
"""
:return: Information about the connected socket if available
"""
return self.conn_info.peername if self.conn_info else (None, None)
@property
@@ -799,13 +645,6 @@ class Request:
"""
return self._parsed_url.path.decode("utf-8")
@property
def network_paths(self):
"""
Access the network paths if available
"""
return self.conn_info.network_paths
# Proxy properties (using SERVER_NAME/forwarded/request/transport info)
@property
@@ -859,25 +698,23 @@ class Request:
:return: http|https|ws|wss or arbitrary value given by the headers.
:rtype: str
"""
if not hasattr(self, "_scheme"):
if "//" in self.app.config.get("SERVER_NAME", ""):
return self.app.config.SERVER_NAME.split("//")[0]
if "proto" in self.forwarded:
return str(self.forwarded["proto"])
if "//" in self.app.config.get("SERVER_NAME", ""):
return self.app.config.SERVER_NAME.split("//")[0]
if "proto" in self.forwarded:
return str(self.forwarded["proto"])
if (
self.app.websocket_enabled
and self.headers.getone("upgrade", "").lower() == "websocket"
):
scheme = "ws"
else:
scheme = "http"
if (
self.app.websocket_enabled
and self.headers.getone("upgrade", "").lower() == "websocket"
):
scheme = "ws"
else:
scheme = "http"
if self.transport.get_extra_info("sslcontext"):
scheme += "s"
self._scheme = scheme
if self.transport.get_extra_info("sslcontext"):
scheme += "s"
return self._scheme
return scheme
@property
def host(self) -> str:
@@ -982,48 +819,6 @@ class Request:
view_name, _external=True, _scheme=scheme, _server=netloc, **kwargs
)
@property
def scope(self) -> ASGIScope:
"""
:return: The ASGI scope of the request.
If the app isn't an ASGI app, then raises an exception.
:rtype: Optional[ASGIScope]
"""
if not self.app.asgi:
raise NotImplementedError(
"App isn't running in ASGI mode. "
"Scope is only available for ASGI apps."
)
return self.transport.scope
@property
def is_safe(self) -> bool:
"""
:return: Whether the HTTP method is safe.
See https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.1
:rtype: bool
"""
return self.method in SAFE_HTTP_METHODS
@property
def is_idempotent(self) -> bool:
"""
:return: Whether the HTTP method is iempotent.
See https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.2
:rtype: bool
"""
return self.method in IDEMPOTENT_HTTP_METHODS
@property
def is_cacheable(self) -> bool:
"""
:return: Whether the HTTP method is cacheable.
See https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.3
:rtype: bool
"""
return self.method in CACHEABLE_HTTP_METHODS
class File(NamedTuple):
"""

543
sanic/response.py Normal file
View File

@@ -0,0 +1,543 @@
from __future__ import annotations
from functools import partial
from mimetypes import guess_type
from os import path
from pathlib import PurePath
from typing import (
TYPE_CHECKING,
Any,
AnyStr,
Callable,
Coroutine,
Dict,
Iterator,
Optional,
Tuple,
TypeVar,
Union,
)
from urllib.parse import quote_plus
from sanic.compat import Header, open_async
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE
from sanic.cookies import CookieJar
from sanic.exceptions import SanicException, ServerError
from sanic.helpers import has_message_body, remove_entity_headers
from sanic.http import Http
from sanic.models.protocol_types import HTMLProtocol, Range
if TYPE_CHECKING:
from sanic.asgi import ASGIApp
from sanic.request import Request
else:
Request = TypeVar("Request")
try:
from ujson import dumps as json_dumps
except ImportError:
# This is done in order to ensure that the JSON response is
# kept consistent across both ujson and inbuilt json usage.
from json import dumps
json_dumps = partial(dumps, separators=(",", ":"))
class BaseHTTPResponse:
"""
The base class for all HTTP Responses
"""
__slots__ = (
"asgi",
"body",
"content_type",
"stream",
"status",
"headers",
"_cookies",
)
_dumps = json_dumps
def __init__(self):
self.asgi: bool = False
self.body: Optional[bytes] = None
self.content_type: Optional[str] = None
self.stream: Optional[Union[Http, ASGIApp]] = None
self.status: int = None
self.headers = Header({})
self._cookies: Optional[CookieJar] = None
def _encode_body(self, data: Optional[AnyStr]):
if data is None:
return b""
return (
data.encode() if hasattr(data, "encode") else data # type: ignore
)
@property
def cookies(self) -> CookieJar:
"""
The response cookies. Cookies should be set and written as follows:
.. code-block:: python
response.cookies["test"] = "It worked!"
response.cookies["test"]["domain"] = ".yummy-yummy-cookie.com"
response.cookies["test"]["httponly"] = True
`See user guide re: cookies
<https://sanicframework.org/guide/basics/cookies.html>`__
:return: the cookie jar
:rtype: CookieJar
"""
if self._cookies is None:
self._cookies = CookieJar(self.headers)
return self._cookies
@property
def processed_headers(self) -> Iterator[Tuple[bytes, bytes]]:
"""
Obtain a list of header tuples encoded in bytes for sending.
Add and remove headers based on status and content_type.
:return: response headers
:rtype: Tuple[Tuple[bytes, bytes], ...]
"""
# TODO: Make a blacklist set of header names and then filter with that
if self.status in (304, 412): # Not Modified, Precondition Failed
self.headers = remove_entity_headers(self.headers)
if has_message_body(self.status):
self.headers.setdefault("content-type", self.content_type)
# Encode headers into bytes
return (
(name.encode("ascii"), f"{value}".encode(errors="surrogateescape"))
for name, value in self.headers.items()
)
async def send(
self,
data: Optional[AnyStr] = None,
end_stream: Optional[bool] = None,
) -> None:
"""
Send any pending response headers and the given data as body.
:param data: str or bytes to be written
:param end_stream: whether to close the stream after this block
"""
if data is None and end_stream is None:
end_stream = True
if self.stream is None:
raise SanicException(
"No stream is connected to the response object instance."
)
if self.stream.send is None:
if end_stream and not data:
return
raise ServerError(
"Response stream was ended, no more response data is "
"allowed to be sent."
)
data = (
data.encode() # type: ignore
if hasattr(data, "encode")
else data or b""
)
await self.stream.send(data, end_stream=end_stream)
class HTTPResponse(BaseHTTPResponse):
"""
HTTP response to be sent back to the client.
:param body: the body content to be returned
:type body: Optional[bytes]
:param status: HTTP response number. **Default=200**
:type status: int
:param headers: headers to be returned
:type headers: Optional;
:param content_type: content type to be returned (as a header)
:type content_type: Optional[str]
"""
__slots__ = ()
def __init__(
self,
body: Optional[AnyStr] = None,
status: int = 200,
headers: Optional[Union[Header, Dict[str, str]]] = None,
content_type: Optional[str] = None,
):
super().__init__()
self.content_type: Optional[str] = content_type
self.body = self._encode_body(body)
self.status = status
self.headers = Header(headers or {})
self._cookies = None
async def eof(self):
await self.send("", True)
async def __aenter__(self):
return self.send
async def __aexit__(self, *_):
await self.eof()
def empty(
status=204, headers: Optional[Dict[str, str]] = None
) -> HTTPResponse:
"""
Returns an empty response to the client.
:param status Response code.
:param headers Custom Headers.
"""
return HTTPResponse(body=b"", status=status, headers=headers)
def json(
body: Any,
status: int = 200,
headers: Optional[Dict[str, str]] = None,
content_type: str = "application/json",
dumps: Optional[Callable[..., str]] = None,
**kwargs,
) -> HTTPResponse:
"""
Returns response object with body in json format.
:param body: Response data to be serialized.
:param status: Response code.
:param headers: Custom Headers.
:param kwargs: Remaining arguments that are passed to the json encoder.
"""
if not dumps:
dumps = BaseHTTPResponse._dumps
return HTTPResponse(
dumps(body, **kwargs),
headers=headers,
status=status,
content_type=content_type,
)
def text(
body: str,
status: int = 200,
headers: Optional[Dict[str, str]] = None,
content_type: str = "text/plain; charset=utf-8",
) -> HTTPResponse:
"""
Returns response object with body in text format.
:param body: Response data to be encoded.
:param status: Response code.
:param headers: Custom Headers.
:param content_type: the content type (string) of the response
"""
if not isinstance(body, str):
raise TypeError(
f"Bad body type. Expected str, got {type(body).__name__})"
)
return HTTPResponse(
body, status=status, headers=headers, content_type=content_type
)
def raw(
body: Optional[AnyStr],
status: int = 200,
headers: Optional[Dict[str, str]] = None,
content_type: str = DEFAULT_HTTP_CONTENT_TYPE,
) -> HTTPResponse:
"""
Returns response object without encoding the body.
:param body: Response data.
:param status: Response code.
:param headers: Custom Headers.
:param content_type: the content type (string) of the response.
"""
return HTTPResponse(
body=body,
status=status,
headers=headers,
content_type=content_type,
)
def html(
body: Union[str, bytes, HTMLProtocol],
status: int = 200,
headers: Optional[Dict[str, str]] = None,
) -> HTTPResponse:
"""
Returns response object with body in html format.
:param body: str or bytes-ish, or an object with __html__ or _repr_html_.
:param status: Response code.
:param headers: Custom Headers.
"""
if not isinstance(body, (str, bytes)):
if hasattr(body, "__html__"):
body = body.__html__()
elif hasattr(body, "_repr_html_"):
body = body._repr_html_()
return HTTPResponse( # type: ignore
body,
status=status,
headers=headers,
content_type="text/html; charset=utf-8",
)
async def file(
location: Union[str, PurePath],
status: int = 200,
mime_type: Optional[str] = None,
headers: Optional[Dict[str, str]] = None,
filename: Optional[str] = None,
_range: Optional[Range] = None,
) -> HTTPResponse:
"""Return a response object with file data.
:param location: Location of file on system.
:param mime_type: Specific mime_type.
:param headers: Custom Headers.
:param filename: Override filename.
:param _range:
"""
headers = headers or {}
if filename:
headers.setdefault(
"Content-Disposition", f'attachment; filename="{filename}"'
)
filename = filename or path.split(location)[-1]
async with await open_async(location, mode="rb") as f:
if _range:
await f.seek(_range.start)
out_stream = await f.read(_range.size)
headers[
"Content-Range"
] = f"bytes {_range.start}-{_range.end}/{_range.total}"
status = 206
else:
out_stream = await f.read()
mime_type = mime_type or guess_type(filename)[0] or "text/plain"
return HTTPResponse(
body=out_stream,
status=status,
headers=headers,
content_type=mime_type,
)
def redirect(
to: str,
headers: Optional[Dict[str, str]] = None,
status: int = 302,
content_type: str = "text/html; charset=utf-8",
) -> HTTPResponse:
"""
Abort execution and cause a 302 redirect (by default) by setting a
Location header.
:param to: path or fully qualified URL to redirect to
:param headers: optional dict of headers to include in the new request
:param status: status code (int) of the new request, defaults to 302
:param content_type: the content type (string) of the response
"""
headers = headers or {}
# URL Quote the URL before redirecting
safe_to = quote_plus(to, safe=":/%#?&=@[]!$&'()*+,;")
# According to RFC 7231, a relative URI is now permitted.
headers["Location"] = safe_to
return HTTPResponse(
status=status, headers=headers, content_type=content_type
)
class ResponseStream:
"""
ResponseStream is a compat layer to bridge the gap after the deprecation
of StreamingHTTPResponse. In v22.6 it will be removed when:
- stream is removed
- file_stream is moved to new style streaming
- file and file_stream are combined into a single API
"""
__slots__ = (
"_cookies",
"content_type",
"headers",
"request",
"response",
"status",
"streaming_fn",
)
def __init__(
self,
streaming_fn: Callable[
[Union[BaseHTTPResponse, ResponseStream]],
Coroutine[Any, Any, None],
],
status: int = 200,
headers: Optional[Union[Header, Dict[str, str]]] = None,
content_type: Optional[str] = None,
):
self.streaming_fn = streaming_fn
self.status = status
self.headers = headers or Header()
self.content_type = content_type
self.request: Optional[Request] = None
self._cookies: Optional[CookieJar] = None
async def write(self, message: str):
await self.response.send(message)
async def stream(self) -> HTTPResponse:
if not self.request:
raise ServerError("Attempted response to unknown request")
self.response = await self.request.respond(
headers=self.headers,
status=self.status,
content_type=self.content_type,
)
await self.streaming_fn(self)
return self.response
async def eof(self) -> None:
await self.response.eof()
@property
def cookies(self) -> CookieJar:
if self._cookies is None:
self._cookies = CookieJar(self.headers)
return self._cookies
@property
def processed_headers(self):
return self.response.processed_headers
@property
def body(self):
return self.response.body
def __call__(self, request: Request) -> ResponseStream:
self.request = request
return self
def __await__(self):
return self.stream().__await__()
async def file_stream(
location: Union[str, PurePath],
status: int = 200,
chunk_size: int = 4096,
mime_type: Optional[str] = None,
headers: Optional[Dict[str, str]] = None,
filename: Optional[str] = None,
_range: Optional[Range] = None,
) -> ResponseStream:
"""Return a streaming response object with file data.
:param location: Location of file on system.
:param chunk_size: The size of each chunk in the stream (in bytes)
:param mime_type: Specific mime_type.
:param headers: Custom Headers.
:param filename: Override filename.
:param _range:
"""
headers = headers or {}
if filename:
headers.setdefault(
"Content-Disposition", f'attachment; filename="{filename}"'
)
filename = filename or path.split(location)[-1]
mime_type = mime_type or guess_type(filename)[0] or "text/plain"
if _range:
start = _range.start
end = _range.end
total = _range.total
headers["Content-Range"] = f"bytes {start}-{end}/{total}"
status = 206
async def _streaming_fn(response):
async with await open_async(location, mode="rb") as f:
if _range:
await f.seek(_range.start)
to_send = _range.size
while to_send > 0:
content = await f.read(min((_range.size, chunk_size)))
if len(content) < 1:
break
to_send -= len(content)
await response.write(content)
else:
while True:
content = await f.read(chunk_size)
if len(content) < 1:
break
await response.write(content)
return ResponseStream(
streaming_fn=_streaming_fn,
status=status,
headers=headers,
content_type=mime_type,
)
def stream(
streaming_fn: Callable[
[Union[BaseHTTPResponse, ResponseStream]], Coroutine[Any, Any, None]
],
status: int = 200,
headers: Optional[Dict[str, str]] = None,
content_type: str = "text/plain; charset=utf-8",
) -> ResponseStream:
"""Accepts a coroutine `streaming_fn` which can be used to
write chunks to a streaming response. Returns a `ResponseStream`.
Example usage::
@app.route("/")
async def index(request):
async def streaming_fn(response):
await response.write('foo')
await response.write('bar')
return stream(streaming_fn, content_type='text/plain')
:param streaming_fn: A coroutine accepts a response and
writes content to that response.
:param status: HTTP status.
:param content_type: Specific content_type.
:param headers: Custom Headers.
"""
return ResponseStream(
streaming_fn,
headers=headers,
content_type=content_type,
status=status,
)

View File

@@ -1,36 +0,0 @@
from .convenience import (
empty,
file,
file_stream,
html,
json,
raw,
redirect,
text,
validate_file,
)
from .types import (
BaseHTTPResponse,
HTTPResponse,
JSONResponse,
ResponseStream,
json_dumps,
)
__all__ = (
"BaseHTTPResponse",
"HTTPResponse",
"JSONResponse",
"ResponseStream",
"empty",
"json",
"text",
"raw",
"html",
"validate_file",
"file",
"redirect",
"file_stream",
"json_dumps",
)

View File

@@ -1,333 +0,0 @@
from __future__ import annotations
from datetime import datetime, timezone
from email.utils import formatdate, parsedate_to_datetime
from mimetypes import guess_type
from os import path
from pathlib import PurePath
from time import time
from typing import Any, AnyStr, Callable, Dict, Optional, Union
from urllib.parse import quote_plus
from sanic.compat import Header, open_async, stat_async
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE
from sanic.helpers import Default, _default
from sanic.log import logger
from sanic.models.protocol_types import HTMLProtocol, Range
from .types import HTTPResponse, JSONResponse, ResponseStream
def empty(
status: int = 204, headers: Optional[Dict[str, str]] = None
) -> HTTPResponse:
"""
Returns an empty response to the client.
:param status Response code.
:param headers Custom Headers.
"""
return HTTPResponse(body=b"", status=status, headers=headers)
def json(
body: Any,
status: int = 200,
headers: Optional[Dict[str, str]] = None,
content_type: str = "application/json",
dumps: Optional[Callable[..., str]] = None,
**kwargs: Any,
) -> JSONResponse:
"""
Returns response object with body in json format.
:param body: Response data to be serialized.
:param status: Response code.
:param headers: Custom Headers.
:param kwargs: Remaining arguments that are passed to the json encoder.
"""
return JSONResponse(
body,
status=status,
headers=headers,
content_type=content_type,
dumps=dumps,
**kwargs,
)
def text(
body: str,
status: int = 200,
headers: Optional[Dict[str, str]] = None,
content_type: str = "text/plain; charset=utf-8",
) -> HTTPResponse:
"""
Returns response object with body in text format.
:param body: Response data to be encoded.
:param status: Response code.
:param headers: Custom Headers.
:param content_type: the content type (string) of the response
"""
if not isinstance(body, str):
raise TypeError(
f"Bad body type. Expected str, got {type(body).__name__})"
)
return HTTPResponse(
body, status=status, headers=headers, content_type=content_type
)
def raw(
body: Optional[AnyStr],
status: int = 200,
headers: Optional[Dict[str, str]] = None,
content_type: str = DEFAULT_HTTP_CONTENT_TYPE,
) -> HTTPResponse:
"""
Returns response object without encoding the body.
:param body: Response data.
:param status: Response code.
:param headers: Custom Headers.
:param content_type: the content type (string) of the response.
"""
return HTTPResponse(
body=body,
status=status,
headers=headers,
content_type=content_type,
)
def html(
body: Union[str, bytes, HTMLProtocol],
status: int = 200,
headers: Optional[Dict[str, str]] = None,
) -> HTTPResponse:
"""
Returns response object with body in html format.
:param body: str or bytes-ish, or an object with __html__ or _repr_html_.
:param status: Response code.
:param headers: Custom Headers.
"""
if not isinstance(body, (str, bytes)):
if hasattr(body, "__html__"):
body = body.__html__()
elif hasattr(body, "_repr_html_"):
body = body._repr_html_()
return HTTPResponse( # type: ignore
body,
status=status,
headers=headers,
content_type="text/html; charset=utf-8",
)
async def validate_file(
request_headers: Header, last_modified: Union[datetime, float, int]
):
try:
if_modified_since = request_headers.getone("If-Modified-Since")
except KeyError:
return
try:
if_modified_since = parsedate_to_datetime(if_modified_since)
except (TypeError, ValueError):
logger.warning(
"Ignorning invalid If-Modified-Since header received: " "'%s'",
if_modified_since,
)
return
if not isinstance(last_modified, datetime):
last_modified = datetime.fromtimestamp(
float(last_modified), tz=timezone.utc
).replace(microsecond=0)
if last_modified <= if_modified_since:
return HTTPResponse(status=304)
async def file(
location: Union[str, PurePath],
status: int = 200,
request_headers: Optional[Header] = None,
validate_when_requested: bool = True,
mime_type: Optional[str] = None,
headers: Optional[Dict[str, str]] = None,
filename: Optional[str] = None,
last_modified: Optional[Union[datetime, float, int, Default]] = _default,
max_age: Optional[Union[float, int]] = None,
no_store: Optional[bool] = None,
_range: Optional[Range] = None,
) -> HTTPResponse:
"""Return a response object with file data.
:param status: HTTP response code. Won't enforce the passed in
status if only a part of the content will be sent (206)
or file is being validated (304).
:param request_headers: The request headers.
:param validate_when_requested: If True, will validate the
file when requested.
:param location: Location of file on system.
:param mime_type: Specific mime_type.
:param headers: Custom Headers.
:param filename: Override filename.
:param last_modified: The last modified date and time of the file.
:param max_age: Max age for cache control.
:param no_store: Any cache should not store this response.
:param _range:
"""
if isinstance(last_modified, datetime):
last_modified = last_modified.replace(microsecond=0).timestamp()
elif isinstance(last_modified, Default):
stat = await stat_async(location)
last_modified = stat.st_mtime
if (
validate_when_requested
and request_headers is not None
and last_modified
):
response = await validate_file(request_headers, last_modified)
if response:
return response
headers = headers or {}
if last_modified:
headers.setdefault(
"Last-Modified", formatdate(last_modified, usegmt=True)
)
if filename:
headers.setdefault(
"Content-Disposition", f'attachment; filename="{filename}"'
)
if no_store:
cache_control = "no-store"
elif max_age:
cache_control = f"public, max-age={max_age}"
headers.setdefault(
"expires",
formatdate(
time() + max_age,
usegmt=True,
),
)
else:
cache_control = "no-cache"
headers.setdefault("cache-control", cache_control)
filename = filename or path.split(location)[-1]
async with await open_async(location, mode="rb") as f:
if _range:
await f.seek(_range.start)
out_stream = await f.read(_range.size)
headers[
"Content-Range"
] = f"bytes {_range.start}-{_range.end}/{_range.total}"
status = 206
else:
out_stream = await f.read()
mime_type = mime_type or guess_type(filename)[0] or "text/plain"
return HTTPResponse(
body=out_stream,
status=status,
headers=headers,
content_type=mime_type,
)
def redirect(
to: str,
headers: Optional[Dict[str, str]] = None,
status: int = 302,
content_type: str = "text/html; charset=utf-8",
) -> HTTPResponse:
"""
Abort execution and cause a 302 redirect (by default) by setting a
Location header.
:param to: path or fully qualified URL to redirect to
:param headers: optional dict of headers to include in the new request
:param status: status code (int) of the new request, defaults to 302
:param content_type: the content type (string) of the response
"""
headers = headers or {}
# URL Quote the URL before redirecting
safe_to = quote_plus(to, safe=":/%#?&=@[]!$&'()*+,;")
# According to RFC 7231, a relative URI is now permitted.
headers["Location"] = safe_to
return HTTPResponse(
status=status, headers=headers, content_type=content_type
)
async def file_stream(
location: Union[str, PurePath],
status: int = 200,
chunk_size: int = 4096,
mime_type: Optional[str] = None,
headers: Optional[Dict[str, str]] = None,
filename: Optional[str] = None,
_range: Optional[Range] = None,
) -> ResponseStream:
"""Return a streaming response object with file data.
:param location: Location of file on system.
:param chunk_size: The size of each chunk in the stream (in bytes)
:param mime_type: Specific mime_type.
:param headers: Custom Headers.
:param filename: Override filename.
:param _range:
"""
headers = headers or {}
if filename:
headers.setdefault(
"Content-Disposition", f'attachment; filename="{filename}"'
)
filename = filename or path.split(location)[-1]
mime_type = mime_type or guess_type(filename)[0] or "text/plain"
if _range:
start = _range.start
end = _range.end
total = _range.total
headers["Content-Range"] = f"bytes {start}-{end}/{total}"
status = 206
async def _streaming_fn(response):
async with await open_async(location, mode="rb") as f:
if _range:
await f.seek(_range.start)
to_send = _range.size
while to_send > 0:
content = await f.read(min((_range.size, chunk_size)))
if len(content) < 1:
break
to_send -= len(content)
await response.write(content)
else:
while True:
content = await f.read(chunk_size)
if len(content) < 1:
break
await response.write(content)
return ResponseStream(
streaming_fn=_streaming_fn,
status=status,
headers=headers,
content_type=mime_type,
)

View File

@@ -1,453 +0,0 @@
from __future__ import annotations
from functools import partial
from typing import (
TYPE_CHECKING,
Any,
AnyStr,
Callable,
Coroutine,
Dict,
Iterator,
Optional,
Tuple,
TypeVar,
Union,
)
from sanic.compat import Header
from sanic.cookies import CookieJar
from sanic.exceptions import SanicException, ServerError
from sanic.helpers import (
Default,
_default,
has_message_body,
remove_entity_headers,
)
from sanic.http import Http
if TYPE_CHECKING:
from sanic.asgi import ASGIApp
from sanic.http.http3 import HTTPReceiver
from sanic.request import Request
else:
Request = TypeVar("Request")
try:
from ujson import dumps as json_dumps
except ImportError:
# This is done in order to ensure that the JSON response is
# kept consistent across both ujson and inbuilt json usage.
from json import dumps
json_dumps = partial(dumps, separators=(",", ":"))
class BaseHTTPResponse:
"""
The base class for all HTTP Responses
"""
__slots__ = (
"asgi",
"body",
"content_type",
"stream",
"status",
"headers",
"_cookies",
)
_dumps = json_dumps
def __init__(self):
self.asgi: bool = False
self.body: Optional[bytes] = None
self.content_type: Optional[str] = None
self.stream: Optional[Union[Http, ASGIApp, HTTPReceiver]] = None
self.status: int = None
self.headers = Header({})
self._cookies: Optional[CookieJar] = None
def __repr__(self):
class_name = self.__class__.__name__
return f"<{class_name}: {self.status} {self.content_type}>"
def _encode_body(self, data: Optional[AnyStr]):
if data is None:
return b""
return (
data.encode() if hasattr(data, "encode") else data # type: ignore
)
@property
def cookies(self) -> CookieJar:
"""
The response cookies. Cookies should be set and written as follows:
.. code-block:: python
response.cookies["test"] = "It worked!"
response.cookies["test"]["domain"] = ".yummy-yummy-cookie.com"
response.cookies["test"]["httponly"] = True
`See user guide re: cookies
<https://sanic.dev/en/guide/basics/cookies.html>`
:return: the cookie jar
:rtype: CookieJar
"""
if self._cookies is None:
self._cookies = CookieJar(self.headers)
return self._cookies
@property
def processed_headers(self) -> Iterator[Tuple[bytes, bytes]]:
"""
Obtain a list of header tuples encoded in bytes for sending.
Add and remove headers based on status and content_type.
:return: response headers
:rtype: Tuple[Tuple[bytes, bytes], ...]
"""
# TODO: Make a blacklist set of header names and then filter with that
if self.status in (304, 412): # Not Modified, Precondition Failed
self.headers = remove_entity_headers(self.headers)
if has_message_body(self.status):
self.headers.setdefault("content-type", self.content_type)
# Encode headers into bytes
return (
(name.encode("ascii"), f"{value}".encode(errors="surrogateescape"))
for name, value in self.headers.items()
)
async def send(
self,
data: Optional[AnyStr] = None,
end_stream: Optional[bool] = None,
) -> None:
"""
Send any pending response headers and the given data as body.
:param data: str or bytes to be written
:param end_stream: whether to close the stream after this block
"""
if data is None and end_stream is None:
end_stream = True
if self.stream is None:
raise SanicException(
"No stream is connected to the response object instance."
)
if self.stream.send is None:
if end_stream and not data:
return
raise ServerError(
"Response stream was ended, no more response data is "
"allowed to be sent."
)
data = (
data.encode() # type: ignore
if hasattr(data, "encode")
else data or b""
)
await self.stream.send(
data, # type: ignore
end_stream=end_stream or False,
)
class HTTPResponse(BaseHTTPResponse):
"""
HTTP response to be sent back to the client.
:param body: the body content to be returned
:type body: Optional[bytes]
:param status: HTTP response number. **Default=200**
:type status: int
:param headers: headers to be returned
:type headers: Optional;
:param content_type: content type to be returned (as a header)
:type content_type: Optional[str]
"""
__slots__ = ()
def __init__(
self,
body: Optional[Any] = None,
status: int = 200,
headers: Optional[Union[Header, Dict[str, str]]] = None,
content_type: Optional[str] = None,
):
super().__init__()
self.content_type: Optional[str] = content_type
self.body = self._encode_body(body)
self.status = status
self.headers = Header(headers or {})
self._cookies = None
async def eof(self):
await self.send("", True)
async def __aenter__(self):
return self.send
async def __aexit__(self, *_):
await self.eof()
class JSONResponse(HTTPResponse):
"""
HTTP response to be sent back to the client, when the response
is of json type. Offers several utilities to manipulate common
json data types.
:param body: the body content to be returned
:type body: Optional[Any]
:param status: HTTP response number. **Default=200**
:type status: int
:param headers: headers to be returned
:type headers: Optional
:param content_type: content type to be returned (as a header)
:type content_type: Optional[str]
:param dumps: json.dumps function to use
:type dumps: Optional[Callable]
"""
__slots__ = (
"_body",
"_body_manually_set",
"_initialized",
"_raw_body",
"_use_dumps",
"_use_dumps_kwargs",
)
def __init__(
self,
body: Optional[Any] = None,
status: int = 200,
headers: Optional[Union[Header, Dict[str, str]]] = None,
content_type: Optional[str] = None,
dumps: Optional[Callable[..., str]] = None,
**kwargs: Any,
):
self._initialized = False
self._body_manually_set = False
self._use_dumps = dumps or BaseHTTPResponse._dumps
self._use_dumps_kwargs = kwargs
self._raw_body = body
super().__init__(
self._encode_body(self._use_dumps(body, **self._use_dumps_kwargs)),
headers=headers,
status=status,
content_type=content_type,
)
self._initialized = True
def _check_body_not_manually_set(self):
if self._body_manually_set:
raise SanicException(
"Cannot use raw_body after body has been manually set."
)
@property
def raw_body(self) -> Optional[Any]:
"""Returns the raw body, as long as body has not been manually
set previously.
NOTE: This object should not be mutated, as it will not be
reflected in the response body. If you need to mutate the
response body, consider using one of the provided methods in
this class or alternatively call set_body() with the mutated
object afterwards or set the raw_body property to it.
"""
self._check_body_not_manually_set()
return self._raw_body
@raw_body.setter
def raw_body(self, value: Any):
self._body_manually_set = False
self._body = self._encode_body(
self._use_dumps(value, **self._use_dumps_kwargs)
)
self._raw_body = value
@property # type: ignore
def body(self) -> Optional[bytes]: # type: ignore
return self._body
@body.setter
def body(self, value: Optional[bytes]):
self._body = value
if not self._initialized:
return
self._body_manually_set = True
def set_body(
self,
body: Any,
dumps: Optional[Callable[..., str]] = None,
**dumps_kwargs: Any,
) -> None:
"""Sets a new response body using the given dumps function
and kwargs, or falling back to the defaults given when
creating the object if none are specified.
"""
self._body_manually_set = False
self._raw_body = body
use_dumps = dumps or self._use_dumps
use_dumps_kwargs = dumps_kwargs if dumps else self._use_dumps_kwargs
self._body = self._encode_body(use_dumps(body, **use_dumps_kwargs))
def append(self, value: Any) -> None:
"""Appends a value to the response raw_body, ensuring that
body is kept up to date. This can only be used if raw_body
is a list.
"""
self._check_body_not_manually_set()
if not isinstance(self._raw_body, list):
raise SanicException("Cannot append to a non-list object.")
self._raw_body.append(value)
self.raw_body = self._raw_body
def extend(self, value: Any) -> None:
"""Extends the response's raw_body with the given values, ensuring
that body is kept up to date. This can only be used if raw_body is
a list.
"""
self._check_body_not_manually_set()
if not isinstance(self._raw_body, list):
raise SanicException("Cannot extend a non-list object.")
self._raw_body.extend(value)
self.raw_body = self._raw_body
def update(self, *args, **kwargs) -> None:
"""Updates the response's raw_body with the given values, ensuring
that body is kept up to date. This can only be used if raw_body is
a dict.
"""
self._check_body_not_manually_set()
if not isinstance(self._raw_body, dict):
raise SanicException("Cannot update a non-dict object.")
self._raw_body.update(*args, **kwargs)
self.raw_body = self._raw_body
def pop(self, key: Any, default: Any = _default) -> Any:
"""Pops a key from the response's raw_body, ensuring that body is
kept up to date. This can only be used if raw_body is a dict or a
list.
"""
self._check_body_not_manually_set()
if not isinstance(self._raw_body, (list, dict)):
raise SanicException(
"Cannot pop from a non-list and non-dict object."
)
if isinstance(default, Default):
value = self._raw_body.pop(key)
elif isinstance(self._raw_body, list):
raise TypeError("pop doesn't accept a default argument for lists")
else:
value = self._raw_body.pop(key, default)
self.raw_body = self._raw_body
return value
class ResponseStream:
"""
ResponseStream is a compat layer to bridge the gap after the deprecation
of StreamingHTTPResponse. It will be removed when:
- file_stream is moved to new style streaming
- file and file_stream are combined into a single API
"""
__slots__ = (
"_cookies",
"content_type",
"headers",
"request",
"response",
"status",
"streaming_fn",
)
def __init__(
self,
streaming_fn: Callable[
[Union[BaseHTTPResponse, ResponseStream]],
Coroutine[Any, Any, None],
],
status: int = 200,
headers: Optional[Union[Header, Dict[str, str]]] = None,
content_type: Optional[str] = None,
):
self.streaming_fn = streaming_fn
self.status = status
self.headers = headers or Header()
self.content_type = content_type
self.request: Optional[Request] = None
self._cookies: Optional[CookieJar] = None
async def write(self, message: str):
await self.response.send(message)
async def stream(self) -> HTTPResponse:
if not self.request:
raise ServerError("Attempted response to unknown request")
self.response = await self.request.respond(
headers=self.headers,
status=self.status,
content_type=self.content_type,
)
await self.streaming_fn(self)
return self.response
async def eof(self) -> None:
await self.response.eof()
@property
def cookies(self) -> CookieJar:
if self._cookies is None:
self._cookies = CookieJar(self.headers)
return self._cookies
@property
def processed_headers(self):
return self.response.processed_headers
@property
def body(self):
return self.response.body
def __call__(self, request: Request) -> ResponseStream:
self.request = request
return self
def __await__(self):
return self.stream().__await__()

View File

@@ -5,14 +5,16 @@ from inspect import signature
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
from uuid import UUID
from sanic_routing import BaseRouter
from sanic_routing.exceptions import NoMethod
from sanic_routing.exceptions import NotFound as RoutingNotFound
from sanic_routing.route import Route
from sanic_routing import BaseRouter # type: ignore
from sanic_routing.exceptions import NoMethod # type: ignore
from sanic_routing.exceptions import (
NotFound as RoutingNotFound, # type: ignore
)
from sanic_routing.route import Route # type: ignore
from sanic.constants import HTTP_METHODS
from sanic.errorpages import check_error_format
from sanic.exceptions import MethodNotAllowed, NotFound, SanicException
from sanic.exceptions import MethodNotSupported, NotFound, SanicException
from sanic.models.handler_types import RouteHandler
@@ -41,7 +43,7 @@ class Router(BaseRouter):
except RoutingNotFound as e:
raise NotFound("Requested URL {} not found".format(e.path))
except NoMethod as e:
raise MethodNotAllowed(
raise MethodNotSupported(
"Method {} not allowed for URL {}".format(method, path),
method=method,
allowed_methods=e.allowed_methods,
@@ -133,14 +135,14 @@ class Router(BaseRouter):
params.update({"requirements": {"host": host}})
route = super().add(**params) # type: ignore
route.extra.ignore_body = ignore_body
route.extra.stream = stream
route.extra.hosts = hosts
route.extra.static = static
route.extra.error_format = error_format
route.ctx.ignore_body = ignore_body
route.ctx.stream = stream
route.ctx.hosts = hosts
route.ctx.static = static
route.ctx.error_format = error_format
if error_format:
check_error_format(route.extra.error_format)
check_error_format(route.ctx.error_format)
routes.append(route)

View File

@@ -5,6 +5,7 @@ import asyncio
from typing import TYPE_CHECKING
from sanic.exceptions import SanicException
from sanic.log import deprecation
if TYPE_CHECKING:
@@ -34,6 +35,15 @@ class AsyncioServer:
self.serve_coro = serve_coro
self.server = None
@property
def init(self):
deprecation(
"AsyncioServer.init has been deprecated and will be removed "
"in v22.6. Use Sanic.state.is_started instead.",
22.6,
)
return self.app.state.is_started
def startup(self):
"""
Trigger "before_server_start" events

View File

@@ -4,7 +4,7 @@ from inspect import isawaitable
from typing import TYPE_CHECKING, Any, Callable, Iterable, Optional
if TYPE_CHECKING:
if TYPE_CHECKING: # no cov
from sanic import Sanic

View File

@@ -1,5 +1,4 @@
import asyncio
import sys
from distutils.util import strtobool
from os import getenv
@@ -48,19 +47,3 @@ def try_use_uvloop() -> None:
if not isinstance(asyncio.get_event_loop_policy(), uvloop.EventLoopPolicy):
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
def try_windows_loop():
if not OS_IS_WINDOWS:
error_logger.warning(
"You are trying to use an event loop policy that is not "
"compatible with your system. You can simply let Sanic handle "
"selecting the best loop for you. Sanic will now continue to run "
"using the default event loop."
)
return
if sys.version_info >= (3, 8) and not isinstance(
asyncio.get_event_loop_policy(), asyncio.WindowsSelectorEventLoopPolicy
):
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())

View File

@@ -2,14 +2,13 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Optional
from sanic.exceptions import RequestCancelled
if TYPE_CHECKING:
if TYPE_CHECKING: # no cov
from sanic.app import Sanic
import asyncio
from asyncio import CancelledError
from asyncio.transports import Transport
from time import monotonic as current_time
@@ -70,7 +69,7 @@ class SanicProtocol(asyncio.Protocol):
"""
await self._can_write.wait()
if self.transport.is_closing():
raise RequestCancelled
raise CancelledError
self.transport.write(data)
self._time = current_time()
@@ -121,7 +120,6 @@ class SanicProtocol(asyncio.Protocol):
try:
self.connections.discard(self)
self.resume_writing()
self.conn_info.lost = True
if self._task:
self._task.cancel()
except BaseException:

View File

@@ -2,93 +2,33 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Optional
from sanic.http.constants import HTTP
from sanic.http.http3 import Http3
from sanic.touchup.meta import TouchUpMeta
if TYPE_CHECKING:
if TYPE_CHECKING: # no cov
from sanic.app import Sanic
import sys
from asyncio import CancelledError
from time import monotonic as current_time
from sanic.exceptions import (
RequestCancelled,
RequestTimeout,
ServiceUnavailable,
)
from sanic.exceptions import RequestTimeout, ServiceUnavailable
from sanic.http import Http, Stage
from sanic.log import Colors, error_logger, logger
from sanic.log import error_logger, logger
from sanic.models.server_types import ConnInfo
from sanic.request import Request
from sanic.server.protocols.base_protocol import SanicProtocol
ConnectionProtocol = type("ConnectionProtocol", (), {})
try:
from aioquic.asyncio import QuicConnectionProtocol
from aioquic.h3.connection import H3_ALPN, H3Connection
from aioquic.quic.events import (
DatagramFrameReceived,
ProtocolNegotiated,
QuicEvent,
)
ConnectionProtocol = QuicConnectionProtocol
except ModuleNotFoundError: # no cov
...
class HttpProtocolMixin:
__slots__ = ()
__version__: HTTP
def _setup_connection(self, *args, **kwargs):
self._http = self.HTTP_CLASS(self, *args, **kwargs)
self._time = current_time()
try:
self.check_timeouts()
except AttributeError:
...
def _setup(self):
self.request: Optional[Request] = None
self.access_log = self.app.config.ACCESS_LOG
self.request_handler = self.app.handle_request
self.error_handler = self.app.error_handler
self.request_timeout = self.app.config.REQUEST_TIMEOUT
self.response_timeout = self.app.config.RESPONSE_TIMEOUT
self.keep_alive_timeout = self.app.config.KEEP_ALIVE_TIMEOUT
self.request_max_size = self.app.config.REQUEST_MAX_SIZE
self.request_class = self.app.request_class or Request
@property
def http(self):
if not hasattr(self, "_http"):
return None
return self._http
@property
def version(self):
return self.__class__.__version__
class HttpProtocol(HttpProtocolMixin, SanicProtocol, metaclass=TouchUpMeta):
class HttpProtocol(SanicProtocol, metaclass=TouchUpMeta):
"""
This class provides implements the HTTP 1.1 protocol on top of our
Sanic Server transport
"""
HTTP_CLASS = Http
__touchup__ = (
"send",
"connection_task",
)
__version__ = HTTP.VERSION_1
__slots__ = (
# request params
"request",
@@ -130,12 +70,25 @@ class HttpProtocol(HttpProtocolMixin, SanicProtocol, metaclass=TouchUpMeta):
unix=unix,
)
self.url = None
self.request: Optional[Request] = None
self.access_log = self.app.config.ACCESS_LOG
self.request_handler = self.app.handle_request
self.error_handler = self.app.error_handler
self.request_timeout = self.app.config.REQUEST_TIMEOUT
self.response_timeout = self.app.config.RESPONSE_TIMEOUT
self.keep_alive_timeout = self.app.config.KEEP_ALIVE_TIMEOUT
self.request_max_size = self.app.config.REQUEST_MAX_SIZE
self.request_class = self.app.request_class or Request
self.state = state if state else {}
self._setup()
if "requests_count" not in self.state:
self.state["requests_count"] = 0
self._exception = None
def _setup_connection(self):
self._http = Http(self)
self._time = current_time()
self.check_timeouts()
async def connection_task(self): # no cov
"""
Run a HTTP connection.
@@ -216,10 +169,7 @@ class HttpProtocol(HttpProtocolMixin, SanicProtocol, metaclass=TouchUpMeta):
)
self.loop.call_later(max(0.1, interval), self.check_timeouts)
return
cancel_msg_args = ()
if sys.version_info >= (3, 9):
cancel_msg_args = ("Cancel connection task with a timeout",)
self._task.cancel(*cancel_msg_args)
self._task.cancel()
except Exception:
error_logger.exception("protocol.check_timeouts")
@@ -229,7 +179,7 @@ class HttpProtocol(HttpProtocolMixin, SanicProtocol, metaclass=TouchUpMeta):
"""
await self._can_write.wait()
if self.transport.is_closing():
raise RequestCancelled
raise CancelledError
await self.app.dispatch(
"http.lifecycle.send",
inline=True,
@@ -269,6 +219,7 @@ class HttpProtocol(HttpProtocolMixin, SanicProtocol, metaclass=TouchUpMeta):
error_logger.exception("protocol.connect_made")
def data_received(self, data: bytes):
try:
self._time = current_time()
if not data:
@@ -285,39 +236,3 @@ class HttpProtocol(HttpProtocolMixin, SanicProtocol, metaclass=TouchUpMeta):
self._data_received.set()
except Exception:
error_logger.exception("protocol.data_received")
class Http3Protocol(HttpProtocolMixin, ConnectionProtocol): # type: ignore
HTTP_CLASS = Http3
__version__ = HTTP.VERSION_3
def __init__(self, *args, app: Sanic, **kwargs) -> None:
self.app = app
super().__init__(*args, **kwargs)
self._setup()
self._connection: Optional[H3Connection] = None
def quic_event_received(self, event: QuicEvent) -> None:
logger.debug(
f"{Colors.BLUE}[quic_event_received]: "
f"{Colors.PURPLE}{event}{Colors.END}",
extra={"verbosity": 2},
)
if isinstance(event, ProtocolNegotiated):
self._setup_connection(transmit=self.transmit)
if event.alpn_protocol in H3_ALPN:
self._connection = H3Connection(
self._quic, enable_webtransport=True
)
elif isinstance(event, DatagramFrameReceived):
if event.data == b"quack":
self._quic.send_datagram_frame(b"quack-ack")
# pass event to the HTTP layer
if self._connection is not None:
for http_event in self._connection.handle_event(event):
self._http.http_event_received(http_event)
@property
def connection(self) -> Optional[H3Connection]:
return self._connection

View File

@@ -1,13 +1,7 @@
from typing import TYPE_CHECKING, Optional, Sequence, cast
try: # websockets < 11.0
from websockets.connection import State
from websockets.server import ServerConnection as ServerProtocol
except ImportError: # websockets >= 11.0
from websockets.protocol import State # type: ignore
from websockets.server import ServerProtocol # type: ignore
from websockets.connection import CLOSED, CLOSING, OPEN
from websockets.server import ServerConnection
from websockets.typing import Subprotocol
from sanic.exceptions import ServerError
@@ -17,15 +11,10 @@ from sanic.server import HttpProtocol
from ..websockets.impl import WebsocketImplProtocol
if TYPE_CHECKING:
if TYPE_CHECKING: # no cov
from websockets import http11
OPEN = State.OPEN
CLOSING = State.CLOSING
CLOSED = State.CLOSED
class WebSocketProtocol(HttpProtocol):
__slots__ = (
"websocket",
@@ -85,7 +74,7 @@ class WebSocketProtocol(HttpProtocol):
# Called by Sanic Server when shutting down
# If we've upgraded to websocket, shut it down
if self.websocket is not None:
if self.websocket.ws_proto.state in (CLOSING, CLOSED):
if self.websocket.connection.state in (CLOSING, CLOSED):
return True
elif self.websocket.loop is not None:
self.websocket.loop.create_task(self.websocket.close(1001))
@@ -101,7 +90,7 @@ class WebSocketProtocol(HttpProtocol):
try:
if subprotocols is not None:
# subprotocols can be a set or frozenset,
# but ServerProtocol needs a list
# but ServerConnection needs a list
subprotocols = cast(
Optional[Sequence[Subprotocol]],
list(
@@ -111,13 +100,13 @@ class WebSocketProtocol(HttpProtocol):
]
),
)
ws_proto = ServerProtocol(
ws_conn = ServerConnection(
max_size=self.websocket_max_size,
subprotocols=subprotocols,
state=OPEN,
logger=logger,
)
resp: "http11.Response" = ws_proto.accept(request)
resp: "http11.Response" = ws_conn.accept(request)
except Exception:
msg = (
"Failed to open a WebSocket connection.\n"
@@ -140,7 +129,7 @@ class WebSocketProtocol(HttpProtocol):
else:
raise ServerError(resp.body, resp.status_code)
self.websocket = WebsocketImplProtocol(
ws_proto,
ws_conn,
ping_interval=self.websocket_ping_interval,
ping_timeout=self.websocket_ping_timeout,
close_timeout=self.websocket_timeout,

View File

@@ -6,9 +6,6 @@ from ssl import SSLContext
from typing import TYPE_CHECKING, Dict, Optional, Type, Union
from sanic.config import Config
from sanic.exceptions import ServerError
from sanic.http.constants import HTTP
from sanic.http.tls import get_ssl_context
from sanic.server.events import trigger_events
@@ -26,11 +23,10 @@ from signal import signal as signal_func
from sanic.application.ext import setup_ext
from sanic.compat import OS_IS_WINDOWS, ctrlc_workaround_for_windows
from sanic.http.http3 import SessionTicketStore, get_config
from sanic.log import error_logger, server_logger
from sanic.log import error_logger, logger
from sanic.models.server_types import Signal
from sanic.server.async_server import AsyncioServer
from sanic.server.protocols.http_protocol import Http3Protocol, HttpProtocol
from sanic.server.protocols.http_protocol import HttpProtocol
from sanic.server.socket import (
bind_socket,
bind_unix_socket,
@@ -38,14 +34,6 @@ from sanic.server.socket import (
)
try:
from aioquic.asyncio import serve as quic_serve
HTTP3_AVAILABLE = True
except ModuleNotFoundError: # no cov
HTTP3_AVAILABLE = False
def serve(
host,
port,
@@ -64,7 +52,6 @@ def serve(
signal=Signal(),
state=None,
asyncio_server_kwargs=None,
version=HTTP.VERSION_1,
):
"""Start asynchronous HTTP Server on an individual process.
@@ -101,90 +88,6 @@ def serve(
app.asgi = False
if version is HTTP.VERSION_3:
return _serve_http_3(host, port, app, loop, ssl)
return _serve_http_1(
host,
port,
app,
ssl,
sock,
unix,
reuse_port,
loop,
protocol,
backlog,
register_sys_signals,
run_multiple,
run_async,
connections,
signal,
state,
asyncio_server_kwargs,
)
def _setup_system_signals(
app: Sanic,
run_multiple: bool,
register_sys_signals: bool,
loop: asyncio.AbstractEventLoop,
) -> None: # no cov
# Ignore SIGINT when run_multiple
if run_multiple:
signal_func(SIGINT, SIG_IGN)
os.environ["SANIC_WORKER_PROCESS"] = "true"
# Register signals for graceful termination
if register_sys_signals:
if OS_IS_WINDOWS:
ctrlc_workaround_for_windows(app)
else:
for _signal in [SIGTERM] if run_multiple else [SIGINT, SIGTERM]:
loop.add_signal_handler(
_signal, partial(app.stop, terminate=False)
)
def _run_server_forever(loop, before_stop, after_stop, cleanup, unix):
pid = os.getpid()
try:
server_logger.info("Starting worker [%s]", pid)
loop.run_forever()
except KeyboardInterrupt:
pass
finally:
server_logger.info("Stopping worker [%s]", pid)
loop.run_until_complete(before_stop())
if cleanup:
cleanup()
loop.run_until_complete(after_stop())
remove_unix_socket(unix)
loop.close()
def _serve_http_1(
host,
port,
app,
ssl,
sock,
unix,
reuse_port,
loop,
protocol,
backlog,
register_sys_signals,
run_multiple,
run_async,
connections,
signal,
state,
asyncio_server_kwargs,
):
connections = connections if connections is not None else set()
protocol_kwargs = _build_protocol_kwargs(protocol, app.config)
server = partial(
@@ -200,12 +103,8 @@ def _serve_http_1(
asyncio_server_kwargs = (
asyncio_server_kwargs if asyncio_server_kwargs else {}
)
if OS_IS_WINDOWS:
pid = os.getpid()
sock = sock.share(pid)
sock = socket.fromshare(sock)
# UNIX sockets are always bound by us (to preserve semantics between modes)
elif unix:
if unix:
sock = bind_unix_socket(unix, backlog=backlog)
server_coroutine = loop.create_server(
server,
@@ -236,7 +135,30 @@ def _serve_http_1(
error_logger.exception("Unable to start server", exc_info=True)
return
def _cleanup():
# Ignore SIGINT when run_multiple
if run_multiple:
signal_func(SIGINT, SIG_IGN)
os.environ["SANIC_WORKER_PROCESS"] = "true"
# Register signals for graceful termination
if register_sys_signals:
if OS_IS_WINDOWS:
ctrlc_workaround_for_windows(app)
else:
for _signal in [SIGTERM] if run_multiple else [SIGINT, SIGTERM]:
loop.add_signal_handler(_signal, app.stop)
loop.run_until_complete(app._server_event("init", "after"))
pid = os.getpid()
try:
logger.info("Starting worker [%s]", pid)
loop.run_forever()
finally:
logger.info("Stopping worker [%s]", pid)
# Run the on_stop function if provided
loop.run_until_complete(app._server_event("shutdown", "before"))
# Wait for event loop to finish and all connections to drain
http_server.close()
loop.run_until_complete(http_server.wait_closed())
@@ -266,55 +188,8 @@ def _serve_http_1(
conn.websocket.fail_connection(code=1001)
else:
conn.abort()
_setup_system_signals(app, run_multiple, register_sys_signals, loop)
loop.run_until_complete(app._server_event("init", "after"))
_run_server_forever(
loop,
partial(app._server_event, "shutdown", "before"),
partial(app._server_event, "shutdown", "after"),
_cleanup,
unix,
)
def _serve_http_3(
host,
port,
app,
loop,
ssl,
register_sys_signals: bool = True,
run_multiple: bool = False,
):
if not HTTP3_AVAILABLE:
raise ServerError(
"Cannot run HTTP/3 server without aioquic installed. "
)
protocol = partial(Http3Protocol, app=app)
ticket_store = SessionTicketStore()
ssl_context = get_ssl_context(app, ssl)
config = get_config(app, ssl_context)
coro = quic_serve(
host,
port,
configuration=config,
create_protocol=protocol,
session_ticket_fetcher=ticket_store.pop,
session_ticket_handler=ticket_store.add,
)
server = AsyncioServer(app, loop, coro, [])
loop.run_until_complete(server.startup())
loop.run_until_complete(server.before_start())
loop.run_until_complete(server)
_setup_system_signals(app, run_multiple, register_sys_signals, loop)
loop.run_until_complete(server.after_start())
# TODO: Create connection cleanup and graceful shutdown
cleanup = None
_run_server_forever(
loop, server.before_stop, server.after_stop, cleanup, None
)
loop.run_until_complete(app._server_event("shutdown", "after"))
remove_unix_socket(unix)
def serve_single(server_settings):
@@ -372,9 +247,7 @@ def serve_multiple(server_settings, workers):
processes = []
def sig_handler(signal, frame):
server_logger.info(
"Received signal %s. Shutting down.", Signals(signal).name
)
logger.info("Received signal %s. Shutting down.", Signals(signal).name)
for process in processes:
os.kill(process.pid, SIGTERM)

View File

@@ -6,10 +6,7 @@ import socket
import stat
from ipaddress import ip_address
from typing import Any, Dict, Optional
from sanic.exceptions import ServerError
from sanic.http.constants import HTTP
from typing import Optional
def bind_socket(host: str, port: int, *, backlog=100) -> socket.socket:
@@ -19,8 +16,6 @@ def bind_socket(host: str, port: int, *, backlog=100) -> socket.socket:
:param backlog: Maximum number of connections to queue
:return: socket.socket object
"""
location = (host, port)
# socket.share, socket.fromshare
try: # IP address: family must be specified for IPv6 at least
ip = ip_address(host)
host = str(ip)
@@ -30,9 +25,8 @@ def bind_socket(host: str, port: int, *, backlog=100) -> socket.socket:
except ValueError: # Hostname, may become AF_INET or AF_INET6
sock = socket.socket()
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(location)
sock.bind((host, port))
sock.listen(backlog)
sock.set_inheritable(True)
return sock
@@ -42,7 +36,7 @@ def bind_unix_socket(path: str, *, mode=0o666, backlog=100) -> socket.socket:
:param backlog: Maximum number of connections to queue
:return: socket.socket object
"""
"""Open or atomically replace existing socket with zero downtime."""
# Sanitise and pre-verify socket path
path = os.path.abspath(path)
folder = os.path.dirname(path)
@@ -91,40 +85,3 @@ def remove_unix_socket(path: Optional[str]) -> None:
os.unlink(path)
except FileNotFoundError:
pass
def configure_socket(
server_settings: Dict[str, Any]
) -> Optional[socket.SocketType]:
# Create a listening socket or use the one in settings
if server_settings.get("version") is HTTP.VERSION_3:
return None
sock = server_settings.get("sock")
unix = server_settings["unix"]
backlog = server_settings["backlog"]
if unix:
sock = bind_unix_socket(unix, backlog=backlog)
server_settings["unix"] = unix
if sock is None:
try:
sock = bind_socket(
server_settings["host"],
server_settings["port"],
backlog=backlog,
)
except OSError as e: # no cov
error = ServerError(
f"Sanic server could not start: {e}.\n\n"
"This may have happened if you are running Sanic in the "
"global scope and not inside of a "
'`if __name__ == "__main__"` block.\n\nSee more information: '
"https://sanic.dev/en/guide/deployment/manager.html#"
"how-sanic-server-starts-processes\n"
)
error.quiet = True
raise error
sock.set_inheritable(True)
server_settings["sock"] = sock
server_settings["host"] = None
server_settings["port"] = None
return sock

View File

@@ -9,7 +9,7 @@ from websockets.typing import Data
from sanic.exceptions import ServerError
if TYPE_CHECKING:
if TYPE_CHECKING: # no cov
from .impl import WebsocketImplProtocol
UTF8Decoder = codecs.getincrementaldecoder("utf-8")
@@ -37,7 +37,7 @@ class WebsocketFrameAssembler:
"get_id",
"put_id",
)
if TYPE_CHECKING:
if TYPE_CHECKING: # no cov
protocol: "WebsocketImplProtocol"
read_mutex: asyncio.Lock
write_mutex: asyncio.Lock

View File

@@ -12,37 +12,21 @@ from typing import (
Union,
)
from websockets.exceptions import (
ConnectionClosed,
ConnectionClosedError,
ConnectionClosedOK,
)
from websockets.connection import CLOSED, CLOSING, OPEN, Event
from websockets.exceptions import ConnectionClosed, ConnectionClosedError
from websockets.frames import Frame, Opcode
try: # websockets < 11.0
from websockets.connection import Event, State
from websockets.server import ServerConnection as ServerProtocol
except ImportError: # websockets >= 11.0
from websockets.protocol import Event, State # type: ignore
from websockets.server import ServerProtocol # type: ignore
from websockets.server import ServerConnection
from websockets.typing import Data
from sanic.log import deprecation, error_logger, logger
from sanic.log import error_logger, logger
from sanic.server.protocols.base_protocol import SanicProtocol
from ...exceptions import ServerError, WebsocketClosed
from .frame import WebsocketFrameAssembler
OPEN = State.OPEN
CLOSING = State.CLOSING
CLOSED = State.CLOSED
class WebsocketImplProtocol:
ws_proto: ServerProtocol
connection: ServerConnection
io_proto: Optional[SanicProtocol]
loop: Optional[asyncio.AbstractEventLoop]
max_queue: int
@@ -68,14 +52,14 @@ class WebsocketImplProtocol:
def __init__(
self,
ws_proto,
connection,
max_queue=None,
ping_interval: Optional[float] = 20,
ping_timeout: Optional[float] = 20,
close_timeout: float = 10,
loop=None,
):
self.ws_proto = ws_proto
self.connection = connection
self.io_proto = None
self.loop = None
self.max_queue = max_queue
@@ -97,16 +81,7 @@ class WebsocketImplProtocol:
@property
def subprotocol(self):
return self.ws_proto.subprotocol
@property
def connection(self):
deprecation(
"The connection property has been deprecated and will be removed. "
"Please use the ws_proto property instead going forward.",
22.6,
)
return self.ws_proto
return self.connection.subprotocol
def pause_frames(self):
if not self.can_pause:
@@ -277,7 +252,7 @@ class WebsocketImplProtocol:
def _force_disconnect(self) -> bool:
"""
Internal method used by end_connection and fail_connection
Internal methdod used by end_connection and fail_connection
only when the graceful auto-closer cannot be used
"""
if self.auto_closer_task and not self.auto_closer_task.done():
@@ -320,15 +295,15 @@ class WebsocketImplProtocol:
# Not draining the write buffer is acceptable in this context.
# clear the send buffer
_ = self.ws_proto.data_to_send()
_ = self.connection.data_to_send()
# If we're not already CLOSED or CLOSING, then send the close.
if self.ws_proto.state is OPEN:
if self.connection.state is OPEN:
if code in (1000, 1001):
self.ws_proto.send_close(code, reason)
self.connection.send_close(code, reason)
else:
self.ws_proto.fail(code, reason)
self.connection.fail(code, reason)
try:
data_to_send = self.ws_proto.data_to_send()
data_to_send = self.connection.data_to_send()
while (
len(data_to_send)
and self.io_proto
@@ -342,7 +317,7 @@ class WebsocketImplProtocol:
...
if code == 1006:
# Special case: 1006 consider the transport already closed
self.ws_proto.state = CLOSED
self.connection.state = CLOSED
if self.data_finished_fut and not self.data_finished_fut.done():
# We have a graceful auto-closer. Use it to close the connection.
self.data_finished_fut.cancel()
@@ -363,10 +338,10 @@ class WebsocketImplProtocol:
# In Python Version 3.7: pause_reading is idempotent
# i.e. it can be called when the transport is already paused or closed.
self.io_proto.transport.pause_reading()
if self.ws_proto.state == OPEN:
data_to_send = self.ws_proto.data_to_send()
self.ws_proto.send_close(code, reason)
data_to_send.extend(self.ws_proto.data_to_send())
if self.connection.state == OPEN:
data_to_send = self.connection.data_to_send()
self.connection.send_close(code, reason)
data_to_send.extend(self.connection.data_to_send())
try:
while (
len(data_to_send)
@@ -475,7 +450,7 @@ class WebsocketImplProtocol:
Raise ConnectionClosed in pending keepalive pings.
They'll never receive a pong once the connection is closed.
"""
if self.ws_proto.state is not CLOSED:
if self.connection.state is not CLOSED:
raise ServerError(
"Webscoket about_pings should only be called "
"after connection state is changed to CLOSED"
@@ -504,9 +479,9 @@ class WebsocketImplProtocol:
self.fail_connection(code, reason)
return
async with self.conn_mutex:
if self.ws_proto.state is OPEN:
self.ws_proto.send_close(code, reason)
data_to_send = self.ws_proto.data_to_send()
if self.connection.state is OPEN:
self.connection.send_close(code, reason)
data_to_send = self.connection.data_to_send()
await self.send_data(data_to_send)
async def recv(self, timeout: Optional[float] = None) -> Optional[Data]:
@@ -536,7 +511,7 @@ class WebsocketImplProtocol:
"already waiting for the next message"
)
await self.recv_lock.acquire()
if self.ws_proto.state is CLOSED:
if self.connection.state is CLOSED:
self.recv_lock.release()
raise WebsocketClosed(
"Cannot receive from websocket interface after it is closed."
@@ -587,7 +562,7 @@ class WebsocketImplProtocol:
"for the next message"
)
await self.recv_lock.acquire()
if self.ws_proto.state is CLOSED:
if self.connection.state is CLOSED:
self.recv_lock.release()
raise WebsocketClosed(
"Cannot receive from websocket interface after it is closed."
@@ -646,7 +621,7 @@ class WebsocketImplProtocol:
"is already waiting for the next message"
)
await self.recv_lock.acquire()
if self.ws_proto.state is CLOSED:
if self.connection.state is CLOSED:
self.recv_lock.release()
raise WebsocketClosed(
"Cannot receive from websocket interface after it is closed."
@@ -687,7 +662,7 @@ class WebsocketImplProtocol:
"""
async with self.conn_mutex:
if self.ws_proto.state in (CLOSED, CLOSING):
if self.connection.state in (CLOSED, CLOSING):
raise WebsocketClosed(
"Cannot write to websocket interface after it is closed."
)
@@ -700,12 +675,12 @@ class WebsocketImplProtocol:
# strings and bytes-like objects are iterable.
if isinstance(message, str):
self.ws_proto.send_text(message.encode("utf-8"))
await self.send_data(self.ws_proto.data_to_send())
self.connection.send_text(message.encode("utf-8"))
await self.send_data(self.connection.data_to_send())
elif isinstance(message, (bytes, bytearray, memoryview)):
self.ws_proto.send_binary(message)
await self.send_data(self.ws_proto.data_to_send())
self.connection.send_binary(message)
await self.send_data(self.connection.data_to_send())
elif isinstance(message, Mapping):
# Catch a common mistake -- passing a dict to send().
@@ -734,7 +709,7 @@ class WebsocketImplProtocol:
(which will be encoded to UTF-8) or a bytes-like object.
"""
async with self.conn_mutex:
if self.ws_proto.state in (CLOSED, CLOSING):
if self.connection.state in (CLOSED, CLOSING):
raise WebsocketClosed(
"Cannot send a ping when the websocket interface "
"is closed."
@@ -762,8 +737,8 @@ class WebsocketImplProtocol:
self.pings[data] = self.io_proto.loop.create_future()
self.ws_proto.send_ping(data)
await self.send_data(self.ws_proto.data_to_send())
self.connection.send_ping(data)
await self.send_data(self.connection.data_to_send())
return asyncio.shield(self.pings[data])
@@ -775,15 +750,15 @@ class WebsocketImplProtocol:
be a string (which will be encoded to UTF-8) or a bytes-like object.
"""
async with self.conn_mutex:
if self.ws_proto.state in (CLOSED, CLOSING):
if self.connection.state in (CLOSED, CLOSING):
# Cannot send pong after transport is shutting down
return
if isinstance(data, str):
data = data.encode("utf-8")
elif isinstance(data, (bytearray, memoryview)):
data = bytes(data)
self.ws_proto.send_pong(data)
await self.send_data(self.ws_proto.data_to_send())
self.connection.send_pong(data)
await self.send_data(self.connection.data_to_send())
async def send_data(self, data_to_send):
for data in data_to_send:
@@ -805,7 +780,7 @@ class WebsocketImplProtocol:
SanicProtocol.close(self.io_proto, timeout=1.0)
async def async_data_received(self, data_to_send, events_to_process):
if self.ws_proto.state in (OPEN, CLOSING) and len(data_to_send) > 0:
if self.connection.state in (OPEN, CLOSING) and len(data_to_send) > 0:
# receiving data can generate data to send (eg, pong for a ping)
# send connection.data_to_send()
await self.send_data(data_to_send)
@@ -813,9 +788,9 @@ class WebsocketImplProtocol:
await self.process_events(events_to_process)
def data_received(self, data):
self.ws_proto.receive_data(data)
data_to_send = self.ws_proto.data_to_send()
events_to_process = self.ws_proto.events_received()
self.connection.receive_data(data)
data_to_send = self.connection.data_to_send()
events_to_process = self.connection.events_received()
if len(data_to_send) > 0 or len(events_to_process) > 0:
asyncio.create_task(
self.async_data_received(data_to_send, events_to_process)
@@ -824,7 +799,7 @@ class WebsocketImplProtocol:
async def async_eof_received(self, data_to_send, events_to_process):
# receiving EOF can generate data to send
# send connection.data_to_send()
if self.ws_proto.state in (OPEN, CLOSING) and len(data_to_send) > 0:
if self.connection.state in (OPEN, CLOSING) and len(data_to_send) > 0:
await self.send_data(data_to_send)
if len(events_to_process) > 0:
await self.process_events(events_to_process)
@@ -844,9 +819,9 @@ class WebsocketImplProtocol:
SanicProtocol.close(self.io_proto, timeout=1.0)
def eof_received(self) -> Optional[bool]:
self.ws_proto.receive_eof()
data_to_send = self.ws_proto.data_to_send()
events_to_process = self.ws_proto.events_received()
self.connection.receive_eof()
data_to_send = self.connection.data_to_send()
events_to_process = self.connection.events_received()
asyncio.create_task(
self.async_eof_received(data_to_send, events_to_process)
)
@@ -856,19 +831,12 @@ class WebsocketImplProtocol:
"""
The WebSocket Connection is Closed.
"""
if not self.ws_proto.state == CLOSED:
if not self.connection.state == CLOSED:
# signal to the websocket connection handler
# we've lost the connection
self.ws_proto.fail(code=1006)
self.ws_proto.state = CLOSED
self.connection.fail(code=1006)
self.connection.state = CLOSED
self.abort_pings()
if self.connection_lost_waiter:
self.connection_lost_waiter.set_result(None)
async def __aiter__(self):
try:
while True:
yield await self.recv()
except ConnectionClosedOK:
return

View File

@@ -6,9 +6,9 @@ from enum import Enum
from inspect import isawaitable
from typing import Any, Dict, List, Optional, Tuple, Union, cast
from sanic_routing import BaseRouter, Route, RouteGroup
from sanic_routing.exceptions import NotFound
from sanic_routing.utils import path_to_parts
from sanic_routing import BaseRouter, Route, RouteGroup # type: ignore
from sanic_routing.exceptions import NotFound # type: ignore
from sanic_routing.utils import path_to_parts # type: ignore
from sanic.exceptions import InvalidSignal
from sanic.log import error_logger, logger
@@ -30,8 +30,6 @@ class Event(Enum):
HTTP_LIFECYCLE_RESPONSE = "http.lifecycle.response"
HTTP_ROUTING_AFTER = "http.routing.after"
HTTP_ROUTING_BEFORE = "http.routing.before"
HTTP_HANDLER_AFTER = "http.handler.after"
HTTP_HANDLER_BEFORE = "http.handler.before"
HTTP_LIFECYCLE_SEND = "http.lifecycle.send"
HTTP_MIDDLEWARE_AFTER = "http.middleware.after"
HTTP_MIDDLEWARE_BEFORE = "http.middleware.before"
@@ -55,8 +53,6 @@ RESERVED_NAMESPACES = {
Event.HTTP_LIFECYCLE_RESPONSE.value,
Event.HTTP_ROUTING_AFTER.value,
Event.HTTP_ROUTING_BEFORE.value,
Event.HTTP_HANDLER_AFTER.value,
Event.HTTP_HANDLER_BEFORE.value,
Event.HTTP_LIFECYCLE_SEND.value,
Event.HTTP_MIDDLEWARE_AFTER.value,
Event.HTTP_MIDDLEWARE_BEFORE.value,
@@ -154,13 +150,13 @@ class SignalRouter(BaseRouter):
try:
for signal in signals:
params.pop("__trigger__", None)
requirements = getattr(
signal.handler, "__requirements__", None
)
if (
(condition is None and signal.ctx.exclusive is False)
or (condition is None and not requirements)
or (condition == requirements)
or (
condition is None
and not signal.handler.__requirements__
)
or (condition == signal.handler.__requirements__)
) and (signal.ctx.trigger or event == signal.ctx.definition):
maybe_coroutine = signal.handler(**params)
if isawaitable(maybe_coroutine):
@@ -191,7 +187,7 @@ class SignalRouter(BaseRouter):
fail_not_found=fail_not_found and inline,
reverse=reverse,
)
logger.debug(f"Dispatching signal: {event}", extra={"verbosity": 1})
logger.debug(f"Dispatching signal: {event}")
if inline:
return await dispatch

View File

@@ -1,9 +1,7 @@
from __future__ import annotations
import os
import ssl
from typing import Any, Dict, Iterable, Optional, Union
from typing import Iterable, Optional, Union
from sanic.log import logger
@@ -79,6 +77,65 @@ def load_cert_dir(p: str) -> ssl.SSLContext:
return CertSimple(certfile, keyfile)
class CertSimple(ssl.SSLContext):
"""A wrapper for creating SSLContext with a sanic attribute."""
def __new__(cls, cert, key, **kw):
# try common aliases, rename to cert/key
certfile = kw["cert"] = kw.pop("certificate", None) or cert
keyfile = kw["key"] = kw.pop("keyfile", None) or key
password = kw.pop("password", None)
if not certfile or not keyfile:
raise ValueError("SSL dict needs filenames for cert and key.")
subject = {}
if "names" not in kw:
cert = ssl._ssl._test_decode_cert(certfile) # type: ignore
kw["names"] = [
name
for t, name in cert["subjectAltName"]
if t in ["DNS", "IP Address"]
]
subject = {k: v for item in cert["subject"] for k, v in item}
self = create_context(certfile, keyfile, password)
self.__class__ = cls
self.sanic = {**subject, **kw}
return self
def __init__(self, cert, key, **kw):
pass # Do not call super().__init__ because it is already initialized
class CertSelector(ssl.SSLContext):
"""Automatically select SSL certificate based on the hostname that the
client is trying to access, via SSL SNI. Paths to certificate folders
with privkey.pem and fullchain.pem in them should be provided, and
will be matched in the order given whenever there is a new connection.
"""
def __new__(cls, ctxs):
return super().__new__(cls)
def __init__(self, ctxs: Iterable[Optional[ssl.SSLContext]]):
super().__init__()
self.sni_callback = selector_sni_callback # type: ignore
self.sanic_select = []
self.sanic_fallback = None
all_names = []
for i, ctx in enumerate(ctxs):
if not ctx:
continue
names = dict(getattr(ctx, "sanic", {})).get("names", [])
all_names += names
self.sanic_select.append(ctx)
if i == 0:
self.sanic_fallback = ctx
if not all_names:
raise ValueError(
"No certificates with SubjectAlternativeNames found."
)
logger.info(f"Certificate vhosts: {', '.join(all_names)}")
def find_cert(self: CertSelector, server_name: str):
"""Find the first certificate that matches the given SNI.
@@ -137,73 +194,3 @@ def server_name_callback(
) -> None:
"""Store the received SNI as sslobj.sanic_server_name."""
sslobj.sanic_server_name = server_name # type: ignore
class SanicSSLContext(ssl.SSLContext):
sanic: Dict[str, os.PathLike]
@classmethod
def create_from_ssl_context(cls, context: ssl.SSLContext):
context.__class__ = cls
return context
class CertSimple(SanicSSLContext):
"""A wrapper for creating SSLContext with a sanic attribute."""
sanic: Dict[str, Any]
def __new__(cls, cert, key, **kw):
# try common aliases, rename to cert/key
certfile = kw["cert"] = kw.pop("certificate", None) or cert
keyfile = kw["key"] = kw.pop("keyfile", None) or key
password = kw.pop("password", None)
if not certfile or not keyfile:
raise ValueError("SSL dict needs filenames for cert and key.")
subject = {}
if "names" not in kw:
cert = ssl._ssl._test_decode_cert(certfile) # type: ignore
kw["names"] = [
name
for t, name in cert["subjectAltName"]
if t in ["DNS", "IP Address"]
]
subject = {k: v for item in cert["subject"] for k, v in item}
self = create_context(certfile, keyfile, password)
self.__class__ = cls
self.sanic = {**subject, **kw}
return self
def __init__(self, cert, key, **kw):
pass # Do not call super().__init__ because it is already initialized
class CertSelector(ssl.SSLContext):
"""Automatically select SSL certificate based on the hostname that the
client is trying to access, via SSL SNI. Paths to certificate folders
with privkey.pem and fullchain.pem in them should be provided, and
will be matched in the order given whenever there is a new connection.
"""
def __new__(cls, ctxs):
return super().__new__(cls)
def __init__(self, ctxs: Iterable[Optional[ssl.SSLContext]]):
super().__init__()
self.sni_callback = selector_sni_callback # type: ignore
self.sanic_select = []
self.sanic_fallback = None
all_names = []
for i, ctx in enumerate(ctxs):
if not ctx:
continue
names = dict(getattr(ctx, "sanic", {})).get("names", [])
all_names += names
self.sanic_select.append(ctx)
if i == 0:
self.sanic_fallback = ctx
if not all_names:
raise ValueError(
"No certificates with SubjectAlternativeNames found."
)
logger.info(f"Certificate vhosts: {', '.join(all_names)}")

View File

@@ -1,4 +1,3 @@
from .altsvc import AltSvcCheck # noqa
from .base import BaseScheme
from .ode import OptionalDispatchEvent # noqa

View File

@@ -1,56 +0,0 @@
from __future__ import annotations
from ast import Assign, Constant, NodeTransformer, Subscript
from typing import TYPE_CHECKING, Any, List
from sanic.http.constants import HTTP
from .base import BaseScheme
if TYPE_CHECKING:
from sanic import Sanic
class AltSvcCheck(BaseScheme):
ident = "ALTSVC"
def visitors(self) -> List[NodeTransformer]:
return [RemoveAltSvc(self.app, self.app.state.verbosity)]
class RemoveAltSvc(NodeTransformer):
def __init__(self, app: Sanic, verbosity: int = 0) -> None:
self._app = app
self._verbosity = verbosity
self._versions = {
info.settings["version"] for info in app.state.server_info
}
def visit_Assign(self, node: Assign) -> Any:
if any(self._matches(target) for target in node.targets):
if self._should_remove():
return None
assert isinstance(node.value, Constant)
node.value.value = self.value()
return node
def _should_remove(self) -> bool:
return len(self._versions) == 1
@staticmethod
def _matches(node) -> bool:
return (
isinstance(node, Subscript)
and isinstance(node.slice, Constant)
and node.slice.value == "alt-svc"
)
def value(self):
values = []
for info in self._app.state.server_info:
port = info.settings["port"]
version = info.settings["version"]
if version is HTTP.VERSION_3:
values.append(f'h3=":{port}"')
return ", ".join(values)

View File

@@ -1,8 +1,5 @@
from abc import ABC, abstractmethod
from ast import NodeTransformer, parse
from inspect import getsource
from textwrap import dedent
from typing import Any, Dict, List, Set, Type
from typing import Set, Type
class BaseScheme(ABC):
@@ -13,26 +10,11 @@ class BaseScheme(ABC):
self.app = app
@abstractmethod
def visitors(self) -> List[NodeTransformer]:
def run(self, method, module_globals) -> None:
...
def __init_subclass__(cls):
BaseScheme._registry.add(cls)
def __call__(self):
return self.visitors()
@classmethod
def build(cls, method, module_globals, app):
raw_source = getsource(method)
src = dedent(raw_source)
node = parse(src)
for scheme in cls._registry:
for visitor in scheme(app)():
node = visitor.visit(node)
compiled_src = compile(node, method.__name__, "exec")
exec_locals: Dict[str, Any] = {}
exec(compiled_src, module_globals, exec_locals) # nosec
return exec_locals[method.__name__]
def __call__(self, method, module_globals):
return self.run(method, module_globals)

View File

@@ -1,5 +1,7 @@
from ast import Attribute, Await, Expr, NodeTransformer
from typing import Any, List
from ast import Attribute, Await, Dict, Expr, NodeTransformer, parse
from inspect import getsource
from textwrap import dedent
from typing import Any
from sanic.log import logger
@@ -18,8 +20,18 @@ class OptionalDispatchEvent(BaseScheme):
signal.name for signal in app.signal_router.routes
]
def visitors(self) -> List[NodeTransformer]:
return [RemoveDispatch(self._registered_events)]
def run(self, method, module_globals):
raw_source = getsource(method)
src = dedent(raw_source)
tree = parse(src)
node = RemoveDispatch(
self._registered_events, self.app.state.verbosity
).visit(tree)
compiled_src = compile(node, method.__name__, "exec")
exec_locals: Dict[str, Any] = {}
exec(compiled_src, module_globals, exec_locals) # nosec
return exec_locals[method.__name__]
def _sync_events(self):
all_events = set()
@@ -52,8 +64,9 @@ class OptionalDispatchEvent(BaseScheme):
class RemoveDispatch(NodeTransformer):
def __init__(self, registered_events) -> None:
def __init__(self, registered_events, verbosity: int = 0) -> None:
self._registered_events = registered_events
self._verbosity = verbosity
def visit_Expr(self, node: Expr) -> Any:
call = node.value
@@ -70,10 +83,8 @@ class RemoveDispatch(NodeTransformer):
if hasattr(event, "s"):
event_name = getattr(event, "value", event.s)
if self._not_registered(event_name):
logger.debug(
f"Disabling event: {event_name}",
extra={"verbosity": 2},
)
if self._verbosity >= 2:
logger.debug(f"Disabling event: {event_name}")
return None
return node

View File

@@ -21,8 +21,10 @@ class TouchUp:
module = getmodule(target)
module_globals = dict(getmembers(module))
modified = BaseScheme.build(method, module_globals, app)
setattr(target, method_name, modified)
for scheme in BaseScheme._registry:
modified = scheme(app)(method, module_globals)
setattr(target, method_name, modified)
target.__touched__ = True

View File

@@ -1,57 +0,0 @@
import os
from types import SimpleNamespace
from typing import Any, Iterable
from sanic.log import Colors, error_logger
class SharedContext(SimpleNamespace):
SAFE = ("_lock",)
def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)
self._lock = False
def __setattr__(self, name: str, value: Any) -> None:
if self.is_locked:
raise RuntimeError(
f"Cannot set {name} on locked SharedContext object"
)
if not os.environ.get("SANIC_WORKER_NAME"):
to_check: Iterable[Any]
if not isinstance(value, (tuple, frozenset)):
to_check = [value]
else:
to_check = value
for item in to_check:
self._check(name, item)
super().__setattr__(name, value)
def _check(self, name: str, value: Any) -> None:
if name in self.SAFE:
return
try:
module = value.__module__
except AttributeError:
module = ""
if not any(
module.startswith(prefix)
for prefix in ("multiprocessing", "ctypes")
):
error_logger.warning(
f"{Colors.YELLOW}Unsafe object {Colors.PURPLE}{name} "
f"{Colors.YELLOW}with type {Colors.PURPLE}{type(value)} "
f"{Colors.YELLOW}was added to shared_ctx. It may not "
"not function as intended. Consider using the regular "
f"ctx.\nFor more information, please see https://sanic.dev/en"
"/guide/deployment/manager.html#using-shared-context-between-"
f"worker-processes.{Colors.END}"
)
@property
def is_locked(self) -> bool:
return getattr(self, "_lock", False)
def lock(self) -> None:
self._lock = True

View File

@@ -13,7 +13,7 @@ from typing import (
from sanic.models.handler_types import RouteHandler
if TYPE_CHECKING:
if TYPE_CHECKING: # no cov
from sanic import Sanic
from sanic.blueprints import Blueprint

View File

@@ -1,142 +0,0 @@
import sys
from datetime import datetime
from multiprocessing.connection import Connection
from signal import SIGINT, SIGTERM
from signal import signal as signal_func
from socket import AF_INET, SOCK_STREAM, socket, timeout
from textwrap import indent
from typing import Any, Dict
from sanic.application.logo import get_logo
from sanic.application.motd import MOTDTTY
from sanic.log import Colors, error_logger, logger
from sanic.server.socket import configure_socket
try: # no cov
from ujson import dumps, loads
except ModuleNotFoundError: # no cov
from json import dumps, loads # type: ignore
class Inspector:
def __init__(
self,
publisher: Connection,
app_info: Dict[str, Any],
worker_state: Dict[str, Any],
host: str,
port: int,
):
self._publisher = publisher
self.run = True
self.app_info = app_info
self.worker_state = worker_state
self.host = host
self.port = port
def __call__(self) -> None:
sock = configure_socket(
{"host": self.host, "port": self.port, "unix": None, "backlog": 1}
)
assert sock
signal_func(SIGINT, self.stop)
signal_func(SIGTERM, self.stop)
logger.info(f"Inspector started on: {sock.getsockname()}")
sock.settimeout(0.5)
try:
while self.run:
try:
conn, _ = sock.accept()
except timeout:
continue
else:
action = conn.recv(64)
if action == b"reload":
conn.send(b"\n")
self.reload()
elif action == b"shutdown":
conn.send(b"\n")
self.shutdown()
else:
data = dumps(self.state_to_json())
conn.send(data.encode())
conn.close()
finally:
logger.debug("Inspector closing")
sock.close()
def stop(self, *_):
self.run = False
def state_to_json(self):
output = {"info": self.app_info}
output["workers"] = self.make_safe(dict(self.worker_state))
return output
def reload(self):
message = "__ALL_PROCESSES__:"
self._publisher.send(message)
def shutdown(self):
message = "__TERMINATE__"
self._publisher.send(message)
@staticmethod
def make_safe(obj: Dict[str, Any]) -> Dict[str, Any]:
for key, value in obj.items():
if isinstance(value, dict):
obj[key] = Inspector.make_safe(value)
elif isinstance(value, datetime):
obj[key] = value.isoformat()
return obj
def inspect(host: str, port: int, action: str):
out = sys.stdout.write
with socket(AF_INET, SOCK_STREAM) as sock:
try:
sock.connect((host, port))
except ConnectionRefusedError:
error_logger.error(
f"{Colors.RED}Could not connect to inspector at: "
f"{Colors.YELLOW}{(host, port)}{Colors.END}\n"
"Either the application is not running, or it did not start "
"an inspector instance."
)
sock.close()
sys.exit(1)
sock.sendall(action.encode())
data = sock.recv(4096)
if action == "raw":
out(data.decode())
elif action == "pretty":
loaded = loads(data)
display = loaded.pop("info")
extra = display.pop("extra", {})
display["packages"] = ", ".join(display["packages"])
MOTDTTY(get_logo(), f"{host}:{port}", display, extra).display(
version=False,
action="Inspecting",
out=out,
)
for name, info in loaded["workers"].items():
info = "\n".join(
f"\t{key}: {Colors.BLUE}{value}{Colors.END}"
for key, value in info.items()
)
out(
"\n"
+ indent(
"\n".join(
[
f"{Colors.BOLD}{Colors.SANIC}{name}{Colors.END}",
info,
]
),
" ",
)
+ "\n"
)

View File

@@ -1,127 +0,0 @@
from __future__ import annotations
import os
import sys
from importlib import import_module
from pathlib import Path
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Union, cast
from sanic.http.tls.context import process_to_context
from sanic.http.tls.creators import MkcertCreator, TrustmeCreator
if TYPE_CHECKING:
from sanic import Sanic as SanicApp
class AppLoader:
def __init__(
self,
module_input: str = "",
as_factory: bool = False,
as_simple: bool = False,
args: Any = None,
factory: Optional[Callable[[], SanicApp]] = None,
) -> None:
self.module_input = module_input
self.module_name = ""
self.app_name = ""
self.as_factory = as_factory
self.as_simple = as_simple
self.args = args
self.factory = factory
self.cwd = os.getcwd()
if module_input:
delimiter = ":" if ":" in module_input else "."
if module_input.count(delimiter):
module_name, app_name = module_input.rsplit(delimiter, 1)
self.module_name = module_name
self.app_name = app_name
if self.app_name.endswith("()"):
self.as_factory = True
self.app_name = self.app_name[:-2]
def load(self) -> SanicApp:
module_path = os.path.abspath(self.cwd)
if module_path not in sys.path:
sys.path.append(module_path)
if self.factory:
return self.factory()
else:
from sanic.app import Sanic
from sanic.simple import create_simple_server
if self.as_simple:
path = Path(self.module_input)
app = create_simple_server(path)
else:
if self.module_name == "" and os.path.isdir(self.module_input):
raise ValueError(
"App not found.\n"
" Please use --simple if you are passing a "
"directory to sanic.\n"
f" eg. sanic {self.module_input} --simple"
)
module = import_module(self.module_name)
app = getattr(module, self.app_name, None)
if self.as_factory:
try:
app = app(self.args)
except TypeError:
app = app()
app_type_name = type(app).__name__
if (
not isinstance(app, Sanic)
and self.args
and hasattr(self.args, "module")
):
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(
f"Module is not a Sanic app, it is a {app_type_name}\n"
f" Perhaps you meant {self.args.module}:app?"
)
return app
class CertLoader:
_creators = {
"mkcert": MkcertCreator,
"trustme": TrustmeCreator,
}
def __init__(self, ssl_data: Dict[str, Union[str, os.PathLike]]):
self._ssl_data = ssl_data
creator_name = cast(str, ssl_data.get("creator"))
self._creator_class = self._creators.get(creator_name)
if not creator_name:
return
if not self._creator_class:
raise RuntimeError(f"Unknown certificate creator: {creator_name}")
self._key = ssl_data["key"]
self._cert = ssl_data["cert"]
self._localhost = cast(str, ssl_data["localhost"])
def load(self, app: SanicApp):
if not self._creator_class:
return process_to_context(self._ssl_data)
creator = self._creator_class(app, self._key, self._cert)
return creator.generate_cert(self._localhost)

View File

@@ -1,223 +0,0 @@
import os
from signal import SIGINT, SIGTERM, Signals
from signal import signal as signal_func
from typing import List, Optional
from sanic.compat import OS_IS_WINDOWS
from sanic.constants import RestartOrder
from sanic.exceptions import ServerKilled
from sanic.log import error_logger, logger
from sanic.worker.process import ProcessState, Worker, WorkerProcess
if not OS_IS_WINDOWS:
from signal import SIGKILL
else:
SIGKILL = SIGINT
class WorkerManager:
THRESHOLD = 300 # == 30 seconds
MAIN_IDENT = "Sanic-Main"
def __init__(
self,
number: int,
serve,
server_settings,
context,
monitor_pubsub,
worker_state,
restart_order: RestartOrder = RestartOrder.SHUTDOWN_FIRST,
):
self.num_server = number
self.context = context
self.transient: List[Worker] = []
self.durable: List[Worker] = []
self.monitor_publisher, self.monitor_subscriber = monitor_pubsub
self.worker_state = worker_state
self.worker_state[self.MAIN_IDENT] = {"pid": self.pid}
self.terminated = False
self.restart_order = restart_order
if number == 0:
raise RuntimeError("Cannot serve with no workers")
for i in range(number):
self.manage(
f"{WorkerProcess.SERVER_LABEL}-{i}",
serve,
server_settings,
transient=True,
)
signal_func(SIGINT, self.shutdown_signal)
signal_func(SIGTERM, self.shutdown_signal)
def manage(self, ident, func, kwargs, transient=False):
container = self.transient if transient else self.durable
container.append(
Worker(
ident,
func,
kwargs,
self.context,
self.worker_state,
self.restart_order,
)
)
def run(self):
self.start()
self.monitor()
self.join()
self.terminate()
# self.kill()
def start(self):
for process in self.processes:
process.start()
def join(self):
logger.debug("Joining processes", extra={"verbosity": 1})
joined = set()
for process in self.processes:
logger.debug(
f"Found {process.pid} - {process.state.name}",
extra={"verbosity": 1},
)
if process.state < ProcessState.JOINED:
logger.debug(f"Joining {process.pid}", extra={"verbosity": 1})
joined.add(process.pid)
process.join()
if joined:
self.join()
def terminate(self):
if not self.terminated:
for process in self.processes:
process.terminate()
self.terminated = True
def restart(self, process_names: Optional[List[str]] = None, **kwargs):
for process in self.transient_processes:
if not process_names or process.name in process_names:
process.restart(**kwargs)
def monitor(self):
self.wait_for_ack()
while True:
try:
if self.monitor_subscriber.poll(0.1):
message = self.monitor_subscriber.recv()
logger.debug(
f"Monitor message: {message}", extra={"verbosity": 2}
)
if not message:
break
elif message == "__TERMINATE__":
self.shutdown()
break
split_message = message.split(":", 1)
processes = split_message[0]
reloaded_files = (
split_message[1] if len(split_message) > 1 else None
)
process_names = [
name.strip() for name in processes.split(",")
]
if "__ALL_PROCESSES__" in process_names:
process_names = None
self.restart(
process_names=process_names,
reloaded_files=reloaded_files,
)
self._sync_states()
except InterruptedError:
if not OS_IS_WINDOWS:
raise
break
def _sync_states(self):
for process in self.processes:
state = self.worker_state[process.name].get("state")
if state and process.state.name != state:
process.set_state(ProcessState[state], True)
def wait_for_ack(self): # no cov
misses = 0
message = (
"It seems that one or more of your workers failed to come "
"online in the allowed time. Sanic is shutting down to avoid a "
f"deadlock. The current threshold is {self.THRESHOLD / 10}s. "
"If this problem persists, please check out the documentation "
"___."
)
while not self._all_workers_ack():
if self.monitor_subscriber.poll(0.1):
monitor_msg = self.monitor_subscriber.recv()
if monitor_msg != "__TERMINATE_EARLY__":
self.monitor_publisher.send(monitor_msg)
continue
misses = self.THRESHOLD
message = (
"One of your worker processes terminated before startup "
"was completed. Please solve any errors experienced "
"during startup. If you do not see an exception traceback "
"in your error logs, try running Sanic in in a single "
"process using --single-process or single_process=True. "
"Once you are confident that the server is able to start "
"without errors you can switch back to multiprocess mode."
)
misses += 1
if misses > self.THRESHOLD:
error_logger.error(
"Not all workers acknowledged a successful startup. "
"Shutting down.\n\n" + message
)
self.kill()
@property
def workers(self):
return self.transient + self.durable
@property
def processes(self):
for worker in self.workers:
for process in worker.processes:
yield process
@property
def transient_processes(self):
for worker in self.transient:
for process in worker.processes:
yield process
def kill(self):
for process in self.processes:
logger.info("Killing %s [%s]", process.name, process.pid)
os.kill(process.pid, SIGKILL)
raise ServerKilled
def shutdown_signal(self, signal, frame):
logger.info("Received signal %s. Shutting down.", Signals(signal).name)
self.monitor_publisher.send(None)
self.shutdown()
def shutdown(self):
for process in self.processes:
if process.is_alive():
process.terminate()
@property
def pid(self):
return os.getpid()
def _all_workers_ack(self):
acked = [
worker_state.get("state") == ProcessState.ACKED.name
for worker_state in self.worker_state.values()
if worker_state.get("server")
]
return all(acked) and len(acked) == self.num_server

View File

@@ -1,54 +0,0 @@
from multiprocessing.connection import Connection
from os import environ, getpid
from typing import Any, Dict
from sanic.worker.process import ProcessState
from sanic.worker.state import WorkerState
class WorkerMultiplexer:
def __init__(
self,
monitor_publisher: Connection,
worker_state: Dict[str, Any],
):
self._monitor_publisher = monitor_publisher
self._state = WorkerState(worker_state, self.name)
def ack(self):
self._state._state[self.name] = {
**self._state._state[self.name],
"state": ProcessState.ACKED.name,
}
def restart(self, name: str = "", all_workers: bool = False):
if name and all_workers:
raise ValueError(
"Ambiguous restart with both a named process and"
" all_workers=True"
)
if not name:
name = "__ALL_PROCESSES__:" if all_workers else self.name
self._monitor_publisher.send(name)
reload = restart # no cov
def terminate(self, early: bool = False):
message = "__TERMINATE_EARLY__" if early else "__TERMINATE__"
self._monitor_publisher.send(message)
@property
def pid(self) -> int:
return getpid()
@property
def name(self) -> str:
return environ.get("SANIC_WORKER_NAME", "")
@property
def state(self):
return self._state
@property
def workers(self) -> Dict[str, Any]:
return self.state.full()

View File

@@ -1,228 +0,0 @@
import os
from datetime import datetime, timezone
from enum import IntEnum, auto
from multiprocessing.context import BaseContext
from signal import SIGINT
from threading import Thread
from typing import Any, Dict, Set
from sanic.constants import RestartOrder
from sanic.log import Colors, logger
def get_now():
now = datetime.now(tz=timezone.utc)
return now
class ProcessState(IntEnum):
IDLE = auto()
RESTARTING = auto()
STARTING = auto()
STARTED = auto()
ACKED = auto()
JOINED = auto()
TERMINATED = auto()
class WorkerProcess:
SERVER_LABEL = "Server"
def __init__(
self,
factory,
name,
target,
kwargs,
worker_state,
restart_order: RestartOrder,
):
self.state = ProcessState.IDLE
self.factory = factory
self.name = name
self.target = target
self.kwargs = kwargs
self.worker_state = worker_state
self.restart_order = restart_order
if self.name not in self.worker_state:
self.worker_state[self.name] = {
"server": self.SERVER_LABEL in self.name
}
self.spawn()
def set_state(self, state: ProcessState, force=False):
if not force and state < self.state:
raise Exception("...")
self.state = state
self.worker_state[self.name] = {
**self.worker_state[self.name],
"state": self.state.name,
}
def start(self):
os.environ["SANIC_WORKER_NAME"] = self.name
logger.debug(
f"{Colors.BLUE}Starting a process: {Colors.BOLD}"
f"{Colors.SANIC}%s{Colors.END}",
self.name,
)
self.set_state(ProcessState.STARTING)
self._current_process.start()
self.set_state(ProcessState.STARTED)
if not self.worker_state[self.name].get("starts"):
self.worker_state[self.name] = {
**self.worker_state[self.name],
"pid": self.pid,
"start_at": get_now(),
"starts": 1,
}
del os.environ["SANIC_WORKER_NAME"]
def join(self):
self.set_state(ProcessState.JOINED)
self._current_process.join()
def terminate(self):
if self.state is not ProcessState.TERMINATED:
logger.debug(
f"{Colors.BLUE}Terminating a process: "
f"{Colors.BOLD}{Colors.SANIC}"
f"%s {Colors.BLUE}[%s]{Colors.END}",
self.name,
self.pid,
)
self.set_state(ProcessState.TERMINATED, force=True)
try:
os.kill(self.pid, SIGINT)
del self.worker_state[self.name]
except (KeyError, AttributeError, ProcessLookupError):
...
def restart(self, **kwargs):
logger.debug(
f"{Colors.BLUE}Restarting a process: {Colors.BOLD}{Colors.SANIC}"
f"%s {Colors.BLUE}[%s]{Colors.END}",
self.name,
self.pid,
)
self.set_state(ProcessState.RESTARTING, force=True)
if self.restart_order is RestartOrder.SHUTDOWN_FIRST:
self._terminate_now()
else:
self._old_process = self._current_process
self.kwargs.update(
{"config": {k.upper(): v for k, v in kwargs.items()}}
)
try:
self.spawn()
self.start()
except AttributeError:
raise RuntimeError("Restart failed")
if self.restart_order is RestartOrder.STARTUP_FIRST:
self._terminate_soon()
self.worker_state[self.name] = {
**self.worker_state[self.name],
"pid": self.pid,
"starts": self.worker_state[self.name]["starts"] + 1,
"restart_at": get_now(),
}
def _terminate_now(self):
logger.debug(
f"{Colors.BLUE}Begin restart termination: "
f"{Colors.BOLD}{Colors.SANIC}"
f"%s {Colors.BLUE}[%s]{Colors.END}",
self.name,
self._current_process.pid,
)
self._current_process.terminate()
def _terminate_soon(self):
logger.debug(
f"{Colors.BLUE}Begin restart termination: "
f"{Colors.BOLD}{Colors.SANIC}"
f"%s {Colors.BLUE}[%s]{Colors.END}",
self.name,
self._current_process.pid,
)
termination_thread = Thread(target=self._wait_to_terminate)
termination_thread.start()
def _wait_to_terminate(self):
logger.debug(
f"{Colors.BLUE}Waiting for process to be acked: "
f"{Colors.BOLD}{Colors.SANIC}"
f"%s {Colors.BLUE}[%s]{Colors.END}",
self.name,
self._old_process.pid,
)
# TODO: Add a timeout?
while self.state is not ProcessState.ACKED:
...
else:
logger.debug(
f"{Colors.BLUE}Process acked. Terminating: "
f"{Colors.BOLD}{Colors.SANIC}"
f"%s {Colors.BLUE}[%s]{Colors.END}",
self.name,
self._old_process.pid,
)
self._old_process.terminate()
delattr(self, "_old_process")
def is_alive(self):
try:
return self._current_process.is_alive()
except AssertionError:
return False
def spawn(self):
if self.state not in (ProcessState.IDLE, ProcessState.RESTARTING):
raise Exception("Cannot spawn a worker process until it is idle.")
self._current_process = self.factory(
name=self.name,
target=self.target,
kwargs=self.kwargs,
daemon=True,
)
@property
def pid(self):
return self._current_process.pid
class Worker:
WORKER_PREFIX = "Sanic-"
def __init__(
self,
ident: str,
serve,
server_settings,
context: BaseContext,
worker_state: Dict[str, Any],
restart_order: RestartOrder,
):
self.ident = f"{self.WORKER_PREFIX}{ident}"
self.context = context
self.serve = serve
self.server_settings = server_settings
self.worker_state = worker_state
self.processes: Set[WorkerProcess] = set()
self.restart_order = restart_order
self.create_process()
def create_process(self) -> WorkerProcess:
process = WorkerProcess(
factory=self.context.Process,
name=f"{self.ident}-{len(self.processes)}",
target=self.serve,
kwargs={**self.server_settings},
worker_state=self.worker_state,
restart_order=self.restart_order,
)
self.processes.add(process)
return process

View File

@@ -1,121 +0,0 @@
from __future__ import annotations
import os
import sys
from asyncio import new_event_loop
from itertools import chain
from multiprocessing.connection import Connection
from pathlib import Path
from signal import SIGINT, SIGTERM
from signal import signal as signal_func
from time import sleep
from typing import Dict, Set
from sanic.server.events import trigger_events
from sanic.worker.loader import AppLoader
class Reloader:
def __init__(
self,
publisher: Connection,
interval: float,
reload_dirs: Set[Path],
app_loader: AppLoader,
):
self._publisher = publisher
self.interval = interval
self.reload_dirs = reload_dirs
self.run = True
self.app_loader = app_loader
def __call__(self) -> None:
app = self.app_loader.load()
signal_func(SIGINT, self.stop)
signal_func(SIGTERM, self.stop)
mtimes: Dict[str, float] = {}
reloader_start = app.listeners.get("reload_process_start")
reloader_stop = app.listeners.get("reload_process_stop")
before_trigger = app.listeners.get("before_reload_trigger")
after_trigger = app.listeners.get("after_reload_trigger")
loop = new_event_loop()
if reloader_start:
trigger_events(reloader_start, loop, app)
while self.run:
changed = set()
for filename in self.files():
try:
if self.check_file(filename, mtimes):
path = (
filename
if isinstance(filename, str)
else filename.resolve()
)
changed.add(str(path))
except OSError:
continue
if changed:
if before_trigger:
trigger_events(before_trigger, loop, app)
self.reload(",".join(changed) if changed else "unknown")
if after_trigger:
trigger_events(after_trigger, loop, app)
sleep(self.interval)
else:
if reloader_stop:
trigger_events(reloader_stop, loop, app)
def stop(self, *_):
self.run = False
def reload(self, reloaded_files):
message = f"__ALL_PROCESSES__:{reloaded_files}"
self._publisher.send(message)
def files(self):
return chain(
self.python_files(),
*(d.glob("**/*") for d in self.reload_dirs),
)
def python_files(self): # no cov
"""This iterates over all relevant Python files.
It goes through all
loaded files from modules, all files in folders of already loaded
modules as well as all files reachable through a package.
"""
# The list call is necessary on Python 3 in case the module
# dictionary modifies during iteration.
for module in list(sys.modules.values()):
if module is None:
continue
filename = getattr(module, "__file__", None)
if filename:
old = None
while not os.path.isfile(filename):
old = filename
filename = os.path.dirname(filename)
if filename == old:
break
else:
if filename[-4:] in (".pyc", ".pyo"):
filename = filename[:-1]
yield filename
@staticmethod
def check_file(filename, mtimes) -> bool:
need_reload = False
mtime = os.stat(filename).st_mtime
old_time = mtimes.get(filename)
if old_time is None:
mtimes[filename] = mtime
elif mtime > old_time:
mtimes[filename] = mtime
need_reload = True
return need_reload

View File

@@ -1,139 +0,0 @@
import asyncio
import os
import socket
import warnings
from functools import partial
from multiprocessing.connection import Connection
from ssl import SSLContext
from typing import Any, Dict, List, Optional, Type, Union
from sanic.application.constants import ServerStage
from sanic.application.state import ApplicationServerInfo
from sanic.http.constants import HTTP
from sanic.log import error_logger
from sanic.models.server_types import Signal
from sanic.server.protocols.http_protocol import HttpProtocol
from sanic.server.runners import _serve_http_1, _serve_http_3
from sanic.worker.loader import AppLoader, CertLoader
from sanic.worker.multiplexer import WorkerMultiplexer
def worker_serve(
host,
port,
app_name: str,
monitor_publisher: Optional[Connection],
app_loader: AppLoader,
worker_state: Optional[Dict[str, Any]] = None,
server_info: Optional[Dict[str, List[ApplicationServerInfo]]] = None,
ssl: Optional[
Union[SSLContext, Dict[str, Union[str, os.PathLike]]]
] = None,
sock: Optional[socket.socket] = None,
unix: Optional[str] = None,
reuse_port: bool = False,
loop=None,
protocol: Type[asyncio.Protocol] = HttpProtocol,
backlog: int = 100,
register_sys_signals: bool = True,
run_multiple: bool = False,
run_async: bool = False,
connections=None,
signal=Signal(),
state=None,
asyncio_server_kwargs=None,
version=HTTP.VERSION_1,
config=None,
passthru: Optional[Dict[str, Any]] = None,
):
try:
from sanic import Sanic
if app_loader:
app = app_loader.load()
else:
app = Sanic.get_app(app_name)
app.refresh(passthru)
app.setup_loop()
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
# Hydrate server info if needed
if server_info:
for app_name, server_info_objects in server_info.items():
a = Sanic.get_app(app_name)
if not a.state.server_info:
a.state.server_info = []
for info in server_info_objects:
if not info.settings.get("app"):
info.settings["app"] = a
a.state.server_info.append(info)
if isinstance(ssl, dict):
cert_loader = CertLoader(ssl)
ssl = cert_loader.load(app)
for info in app.state.server_info:
info.settings["ssl"] = ssl
# When in a worker process, do some init
if os.environ.get("SANIC_WORKER_NAME"):
# Hydrate apps with any passed server info
if monitor_publisher is None:
raise RuntimeError(
"No restart publisher found in worker process"
)
if worker_state is None:
raise RuntimeError("No worker state found in worker process")
# Run secondary servers
apps = list(Sanic._app_registry.values())
app.before_server_start(partial(app._start_servers, apps=apps))
for a in apps:
a.multiplexer = WorkerMultiplexer(
monitor_publisher, worker_state
)
if app.debug:
loop.set_debug(app.debug)
app.asgi = False
if app.state.server_info:
primary_server_info = app.state.server_info[0]
primary_server_info.stage = ServerStage.SERVING
if config:
app.update_config(config)
if version is HTTP.VERSION_3:
return _serve_http_3(host, port, app, loop, ssl)
return _serve_http_1(
host,
port,
app,
ssl,
sock,
unix,
reuse_port,
loop,
protocol,
backlog,
register_sys_signals,
run_multiple,
run_async,
connections,
signal,
state,
asyncio_server_kwargs,
)
except Exception as e:
warnings.simplefilter("ignore", category=RuntimeWarning)
if monitor_publisher:
error_logger.exception(e)
multiplexer = WorkerMultiplexer(monitor_publisher, {})
multiplexer.terminate(True)
else:
raise e

View File

@@ -1,85 +0,0 @@
from collections.abc import Mapping
from typing import Any, Dict, ItemsView, Iterator, KeysView, List
from typing import Mapping as MappingType
from typing import ValuesView
dict
class WorkerState(Mapping):
RESTRICTED = (
"health",
"pid",
"requests",
"restart_at",
"server",
"start_at",
"starts",
"state",
)
def __init__(self, state: Dict[str, Any], current: str) -> None:
self._name = current
self._state = state
def __getitem__(self, key: str) -> Any:
return self._state[self._name][key]
def __setitem__(self, key: str, value: Any) -> None:
if key in self.RESTRICTED:
self._write_error([key])
self._state[self._name] = {
**self._state[self._name],
key: value,
}
def __delitem__(self, key: str) -> None:
if key in self.RESTRICTED:
self._write_error([key])
self._state[self._name] = {
k: v for k, v in self._state[self._name].items() if k != key
}
def __iter__(self) -> Iterator[Any]:
return iter(self._state[self._name])
def __len__(self) -> int:
return len(self._state[self._name])
def __repr__(self) -> str:
return repr(self._state[self._name])
def __eq__(self, other: object) -> bool:
return self._state[self._name] == other
def keys(self) -> KeysView[str]:
return self._state[self._name].keys()
def values(self) -> ValuesView[Any]:
return self._state[self._name].values()
def items(self) -> ItemsView[str, Any]:
return self._state[self._name].items()
def update(self, mapping: MappingType[str, Any]) -> None:
if any(k in self.RESTRICTED for k in mapping.keys()):
self._write_error(
[k for k in mapping.keys() if k in self.RESTRICTED]
)
self._state[self._name] = {
**self._state[self._name],
**mapping,
}
def pop(self) -> None:
raise NotImplementedError
def full(self) -> Dict[str, Any]:
return dict(self._state)
def _write_error(self, keys: List[str]) -> None:
raise LookupError(
f"Cannot set restricted key{'s' if len(keys) > 1 else ''} on "
f"WorkerState: {', '.join(keys)}"
)

View File

@@ -1,2 +1,13 @@
[flake8]
ignore = E203, W503
[isort]
atomic = true
default_section = THIRDPARTY
include_trailing_comma = true
known_first_party = sanic
known_third_party = pytest
line_length = 79
lines_after_imports = 2
lines_between_types = 1
multi_line_output = 3

View File

@@ -61,7 +61,7 @@ setup_kwargs = {
"Build fast. Run fast."
),
"long_description": long_description,
"packages": find_packages(exclude=("tests", "tests.*")),
"packages": find_packages(),
"package_data": {"sanic": ["py.typed"]},
"platforms": "any",
"python_requires": ">=3.7",
@@ -81,10 +81,10 @@ env_dependency = (
'; sys_platform != "win32" ' 'and implementation_name == "cpython"'
)
ujson = "ujson>=1.35" + env_dependency
uvloop = "uvloop>=0.15.0" + env_dependency
uvloop = "uvloop>=0.5.3" + env_dependency
types_ujson = "types-ujson" + env_dependency
requirements = [
"sanic-routing>=22.8.0",
"sanic-routing>=22.3.0,<22.6.0",
"httptools>=0.0.10",
uvloop,
ujson,
@@ -94,11 +94,14 @@ requirements = [
]
tests_require = [
"sanic-testing>=22.9.0",
"pytest==7.1.*",
"coverage",
"sanic-testing>=22.3.0",
"pytest==6.2.5",
"coverage==5.3",
"gunicorn==20.0.4",
"pytest-cov",
"beautifulsoup4",
"pytest-sanic",
"pytest-sugar",
"pytest-benchmark",
"chardet==3.*",
"flake8",
@@ -119,7 +122,6 @@ docs_require = [
"docutils",
"pygments",
"m2r2",
"enum-tools[sphinx]",
"mistune<2.0.0",
]
@@ -147,7 +149,6 @@ extras_require = {
"docs": docs_require,
"all": all_require,
"ext": ["sanic-ext"],
"http3": ["aioquic"],
}
setup_kwargs["install_requires"] = requirements

Some files were not shown because too many files have changed in this diff Show More