Skip to content

Commit

Permalink
Merge pull request #2403 from sanic-org/http3-flow
Browse files Browse the repository at this point in the history
  • Loading branch information
ahopkins committed May 24, 2022
2 parents 067316a + 9045719 commit 92d2f41
Show file tree
Hide file tree
Showing 53 changed files with 1,145 additions and 276 deletions.
9 changes: 8 additions & 1 deletion CONTRIBUTING.rst
Expand Up @@ -140,6 +140,7 @@ To maintain the code consistency, Sanic uses following tools.
#. `isort <https://github.com/timothycrosley/isort>`_
#. `black <https://github.com/python/black>`_
#. `flake8 <https://github.com/PyCQA/flake8>`_
#. `slotscheck <https://github.com/ariebovenberg/slotscheck>`_

isort
*****
Expand Down Expand Up @@ -167,7 +168,13 @@ flake8
#. pycodestyle
#. Ned Batchelder's McCabe script

``isort``\ , ``black`` and ``flake8`` checks are performed during ``tox`` lint checks.
slotscheck
**********

``slotscheck`` ensures that there are no problems with ``__slots__``
(e.g. overlaps, or missing slots in base classes).

``isort``\ , ``black``\ , ``flake8`` and ``slotscheck`` checks are performed during ``tox`` lint checks.

The **easiest** way to make your code conform is to run the following before committing.

Expand Down
1 change: 1 addition & 0 deletions docs/sanic/changelog.rst
@@ -1,6 +1,7 @@
📜 Changelog
============

.. mdinclude:: ./releases/22/22.3.md
.. mdinclude:: ./releases/21/21.12.md
.. mdinclude:: ./releases/21/21.9.md
.. include:: ../../CHANGELOG.rst
52 changes: 52 additions & 0 deletions docs/sanic/releases/22/22.3.md
@@ -0,0 +1,52 @@
## Version 22.3.0

### Features
- [#2347](https://github.com/sanic-org/sanic/pull/2347) API for multi-application server
- 🚨 *BREAKING CHANGE*: The old `sanic.worker.GunicornWorker` has been **removed**. To run Sanic with `gunicorn`, you should use it thru `uvicorn` [as described in their docs](https://www.uvicorn.org/#running-with-gunicorn).
- 🧁 *SIDE EFFECT*: Named background tasks are now supported, even in Python 3.7
- [#2357](https://github.com/sanic-org/sanic/pull/2357) Parse `Authorization` header as `Request.credentials`
- [#2361](https://github.com/sanic-org/sanic/pull/2361) Add config option to skip `Touchup` step in application startup
- [#2372](https://github.com/sanic-org/sanic/pull/2372) Updates to CLI help messaging
- [#2382](https://github.com/sanic-org/sanic/pull/2382) Downgrade warnings to backwater debug messages
- [#2396](https://github.com/sanic-org/sanic/pull/2396) Allow for `multidict` v0.6
- [#2401](https://github.com/sanic-org/sanic/pull/2401) Upgrade CLI catching for alternative application run types
- [#2402](https://github.com/sanic-org/sanic/pull/2402) Conditionally inject CLI arguments into factory
- [#2413](https://github.com/sanic-org/sanic/pull/2413) Add new start and stop event listeners to reloader process
- [#2414](https://github.com/sanic-org/sanic/pull/2414) Remove loop as required listener arg
- [#2415](https://github.com/sanic-org/sanic/pull/2415) Better exception for bad URL parsing
- [sanic-routing#47](https://github.com/sanic-org/sanic-routing/pull/47) Add a new extention parameter type: `<file:ext>`, `<file:ext=jpg>`, `<file:ext=jpg|png|gif|svg>`, `<file=int:ext>`, `<file=int:ext=jpg|png|gif|svg>`, `<file=float:ext=tar.gz>`
- 👶 *BETA FEATURE*: This feature will not work with `path` type matching, and is being released as a beta feature only.
- [sanic-routing#57](https://github.com/sanic-org/sanic-routing/pull/57) Change `register_pattern` to accept a `str` or `Pattern`
- [sanic-routing#58](https://github.com/sanic-org/sanic-routing/pull/58) Default matching on non-empty strings only, and new `strorempty` pattern type
- 🚨 *BREAKING CHANGE*: Previously a route with a dynamic string parameter (`/<foo>` or `/<foo:str>`) would match on any string, including empty strings. It will now **only** match a non-empty string. To retain the old behavior, you should use the new parameter type: `/<foo:strorempty>`.

### Bugfixes
- [#2373](https://github.com/sanic-org/sanic/pull/2373) Remove `error_logger` on websockets
- [#2381](https://github.com/sanic-org/sanic/pull/2381) Fix newly assigned `None` in task registry
- [sanic-routing#52](https://github.com/sanic-org/sanic-routing/pull/52) Add type casting to regex route matching
- [sanic-routing#60](https://github.com/sanic-org/sanic-routing/pull/60) Add requirements check on regex routes (this resolves, for example, multiple static directories with differing `host` values)

### Deprecations and Removals
- [#2362](https://github.com/sanic-org/sanic/pull/2362) 22.3 Deprecations and changes
1. `debug=True` and `--debug` do _NOT_ automatically run `auto_reload`
2. Default error render is with plain text (browsers still get HTML by default because `auto` looks at headers)
3. `config` is required for `ErrorHandler.finalize`
4. `ErrorHandler.lookup` requires two positional args
5. Unused websocket protocol args removed
- [#2344](https://github.com/sanic-org/sanic/pull/2344) Deprecate loading of lowercase environment variables

### Developer infrastructure
- [#2363](https://github.com/sanic-org/sanic/pull/2363) Revert code coverage back to Codecov
- [#2405](https://github.com/sanic-org/sanic/pull/2405) Upgrade tests for `sanic-routing` changes
- [sanic-testing#35](https://github.com/sanic-org/sanic-testing/pull/35) Allow for httpx v0.22

### Improved Documentation
- [#2350](https://github.com/sanic-org/sanic/pull/2350) Fix link in README for ASGI
- [#2398](https://github.com/sanic-org/sanic/pull/2398) Document middleware on_request and on_response
- [#2409](https://github.com/sanic-org/sanic/pull/2409) Add missing documentation for `Request.respond`

### Miscellaneous
- [#2376](https://github.com/sanic-org/sanic/pull/2376) Fix typing for `ListenerMixin.listener`
- [#2383](https://github.com/sanic-org/sanic/pull/2383) Clear deprecation warning in `asyncio.wait`
- [#2387](https://github.com/sanic-org/sanic/pull/2387) Cleanup `__slots__` implementations
- [#2390](https://github.com/sanic-org/sanic/pull/2390) Clear deprecation warning in `asyncio.get_event_loop`
3 changes: 2 additions & 1 deletion examples/delayed_response.py
Expand Up @@ -4,14 +4,15 @@


app = Sanic("DelayedResponseApp", strict_slashes=True)
app.config.AUTO_EXTEND = False


@app.get("/")
async def handler(request):
return response.redirect("/sleep/3")


@app.get("/sleep/<t:number>")
@app.get("/sleep/<t:float>")
async def handler2(request, t=0.3):
await sleep(t)
return response.text(f"Slept {t:.1f} seconds.\n")
Expand Down
2 changes: 2 additions & 0 deletions sanic/__init__.py
Expand Up @@ -4,6 +4,7 @@
from sanic.constants import HTTPMethod
from sanic.request import Request
from sanic.response import HTTPResponse, html, json, text
from sanic.server.websockets.impl import WebsocketImplProtocol as Websocket


__all__ = (
Expand All @@ -13,6 +14,7 @@
"HTTPMethod",
"HTTPResponse",
"Request",
"Websocket",
"html",
"json",
"text",
Expand Down
2 changes: 1 addition & 1 deletion sanic/__version__.py
@@ -1 +1 @@
__version__ = "22.3.0.dev1"
__version__ = "22.3.2"
43 changes: 30 additions & 13 deletions sanic/app.py
Expand Up @@ -11,7 +11,6 @@
CancelledError,
Task,
ensure_future,
get_event_loop,
get_running_loop,
wait_for,
)
Expand Down Expand Up @@ -59,12 +58,13 @@
from sanic.compat import OS_IS_WINDOWS, enable_windows_color_support
from sanic.config import SANIC_PREFIX, Config
from sanic.exceptions import (
InvalidUsage,
BadRequest,
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,
Expand Down Expand Up @@ -142,7 +142,6 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
"error_handler",
"go_fast",
"listeners",
"name",
"named_request_middleware",
"named_response_middleware",
"request_class",
Expand Down Expand Up @@ -254,7 +253,13 @@ def loop(self):
"Loop can only be retrieved after the app has started "
"running. Not supported with `create_server` function"
)
return get_event_loop()
try:
return get_running_loop()
except RuntimeError:
if sys.version_info > (3, 10):
return asyncio.get_event_loop_policy().get_event_loop()
else:
return asyncio.get_event_loop()

# -------------------------------------------------------------------- #
# Registration
Expand All @@ -277,7 +282,7 @@ def register_listener(
valid = ", ".join(
map(lambda x: x.lower(), ListenerEvent.__members__.keys())
)
raise InvalidUsage(f"Invalid event: {event}. Use one of: {valid}")
raise BadRequest(f"Invalid event: {event}. Use one of: {valid}")

if "." in _event:
self.signal(_event.value)(
Expand Down Expand Up @@ -945,6 +950,7 @@ async def handle_request(self, request: Request): # no cov
"response": response,
},
)
...
await response.send(end_stream=True)
elif isinstance(response, ResponseStream):
resp = await response(request)
Expand Down Expand Up @@ -988,10 +994,10 @@ async def _websocket_handler(
cancelled = False
try:
await fut
except Exception as e:
self.error_handler.log(request, e)
except (CancelledError, ConnectionClosed):
cancelled = True
except Exception as e:
self.error_handler.log(request, e)
finally:
self.websocket_tasks.remove(fut)
if cancelled:
Expand Down Expand Up @@ -1132,7 +1138,10 @@ def _cancel_websocket_tasks(cls, app, loop):
async def _listener(
app: Sanic, loop: AbstractEventLoop, listener: ListenerType
):
maybe_coro = listener(app, loop)
try:
maybe_coro = listener(app) # type: ignore
except TypeError:
maybe_coro = listener(app, loop) # type: ignore
if maybe_coro and isawaitable(maybe_coro):
await maybe_coro

Expand Down Expand Up @@ -1509,7 +1518,8 @@ def finalize(self):
if not Sanic.test_mode:
raise e

def signalize(self):
def signalize(self, allow_fail_builtin=True):
self.signal_router.allow_fail_builtin = allow_fail_builtin
try:
self.signal_router.finalize()
except FinalizationError as e:
Expand All @@ -1524,8 +1534,13 @@ async def _startup(self):
if hasattr(self, "_ext"):
self.ext._display()

if self.state.is_debug and self.config.TOUCHUP is not True:
self.config.TOUCHUP = False
elif self.config.TOUCHUP is _default:
self.config.TOUCHUP = True

# Setup routers
self.signalize()
self.signalize(self.config.TOUCHUP)
self.finalize()

# TODO: Replace in v22.6 to check against apps in app registry
Expand All @@ -1545,7 +1560,8 @@ async def _startup(self):
# TODO:
# - Raise warning if secondary apps have error handler config
ErrorHandler.finalize(self.error_handler, config=self.config)
TouchUp.run(self)
if self.config.TOUCHUP:
TouchUp.run(self)

self.state.is_started = True

Expand All @@ -1561,8 +1577,9 @@ async def _server_event(
"shutdown",
):
raise SanicException(f"Invalid server event: {event}")
if self.state.verbosity >= 1:
logger.debug(f"Triggering server events: {event}")
logger.debug(
f"Triggering server events: {event}", extra={"verbosity": 1}
)
reverse = concern == "shutdown"
if loop is None:
loop = self.loop
Expand Down
5 changes: 4 additions & 1 deletion sanic/application/state.py
Expand Up @@ -9,7 +9,7 @@
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Union

from sanic.application.constants import Mode, Server, ServerStage
from sanic.log import logger
from sanic.log import VerbosityFilter, logger
from sanic.server.async_server import AsyncioServer


Expand Down Expand Up @@ -69,6 +69,9 @@ def set_mode(self, value: Union[str, Mode]):
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
Expand Down
43 changes: 22 additions & 21 deletions sanic/asgi.py
Expand Up @@ -25,27 +25,28 @@ class Lifespan:
def __init__(self, asgi_app: ASGIApp) -> None:
self.asgi_app = asgi_app

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."
)
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},
)

async def startup(self) -> None:
"""
Expand Down
31 changes: 29 additions & 2 deletions sanic/cli/app.py
Expand Up @@ -71,6 +71,13 @@ def run(self):
legacy_version = len(sys.argv) == 2 and sys.argv[-1] == "-v"
parse_args = ["--version"] if legacy_version else None

if not parse_args:
parsed, unknown = self.parser.parse_known_args()
if unknown and parsed.factory:
for arg in unknown:
if arg.startswith("--"):
self.parser.add_argument(arg.split("=")[0])

self.args = self.parser.parse_args(args=parse_args)
self._precheck()

Expand Down Expand Up @@ -120,21 +127,41 @@ def _get_app(self):
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:
app = app()
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?"
f" Perhaps you meant {self.args.module}:app?"
)
except ImportError as e:
if module_name.startswith(e.name):
Expand Down
2 changes: 1 addition & 1 deletion sanic/compat.py
Expand Up @@ -72,7 +72,7 @@ async def stay_active(app):
"""Asyncio wakeups to allow receiving SIGINT in Python"""
while not die:
# If someone else stopped the app, just exit
if app.is_stopping:
if app.state.is_stopping:
return
# Windows Python blocks signal handlers while the event loop is
# waiting for I/O. Frequent wakeups keep interrupts flowing.
Expand Down

0 comments on commit 92d2f41

Please sign in to comment.