Mergeback of 21.12.1 (#2358)

Co-authored-by: Néstor Pérez <25409753+prryplatypus@users.noreply.github.com>
Co-authored-by: Ryu juheon <saidbysolo@gmail.com>
This commit is contained in:
Adam Hopkins 2022-01-06 12:40:52 +02:00 committed by GitHub
parent 9bf9067c99
commit 4669036f45
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 142 additions and 46 deletions

View File

@ -2,9 +2,13 @@ name: "CodeQL"
on: on:
push: push:
branches: [ main ] branches:
- main
- "*LTS"
pull_request: pull_request:
branches: [ main ] branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review] types: [opened, synchronize, reopened, ready_for_review]
schedule: schedule:
- cron: '25 16 * * 0' - cron: '25 16 * * 0'

View File

@ -3,6 +3,7 @@ on:
push: push:
branches: branches:
- main - main
- "*LTS"
tags: tags:
- "!*" # Do not execute on tags - "!*" # Do not execute on tags
pull_request: pull_request:

View File

@ -3,6 +3,7 @@ on:
pull_request: pull_request:
branches: branches:
- main - main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review] types: [opened, synchronize, reopened, ready_for_review]
jobs: jobs:

View File

@ -3,6 +3,7 @@ on:
pull_request: pull_request:
branches: branches:
- main - main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review] types: [opened, synchronize, reopened, ready_for_review]
jobs: jobs:

View File

@ -3,6 +3,7 @@ on:
pull_request: pull_request:
branches: branches:
- main - main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review] types: [opened, synchronize, reopened, ready_for_review]
jobs: jobs:

View File

@ -3,6 +3,7 @@ on:
pull_request: pull_request:
branches: branches:
- main - main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review] types: [opened, synchronize, reopened, ready_for_review]
jobs: jobs:

View File

@ -3,6 +3,7 @@ on:
pull_request: pull_request:
branches: branches:
- main - main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review] types: [opened, synchronize, reopened, ready_for_review]
jobs: jobs:

View File

@ -3,6 +3,7 @@ on:
pull_request: pull_request:
branches: branches:
- main - main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review] types: [opened, synchronize, reopened, ready_for_review]
jobs: jobs:

View File

@ -3,6 +3,7 @@ on:
pull_request: pull_request:
branches: branches:
- main - main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review] types: [opened, synchronize, reopened, ready_for_review]
jobs: jobs:

View File

@ -3,6 +3,7 @@ on:
pull_request: pull_request:
branches: branches:
- main - main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review] types: [opened, synchronize, reopened, ready_for_review]
jobs: jobs:
@ -15,7 +16,7 @@ jobs:
matrix: matrix:
os: [ubuntu-latest] os: [ubuntu-latest]
config: config:
- { python-version: 3.7, tox-env: type-checking} # - { python-version: 3.7, tox-env: type-checking}
- { python-version: 3.8, tox-env: type-checking} - { python-version: 3.8, tox-env: type-checking}
- { python-version: 3.9, tox-env: type-checking} - { python-version: 3.9, tox-env: type-checking}
- { python-version: "3.10", tox-env: type-checking} - { python-version: "3.10", tox-env: type-checking}

View File

@ -3,6 +3,7 @@ on:
pull_request: pull_request:
branches: branches:
- main - main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review] types: [opened, synchronize, reopened, ready_for_review]
jobs: jobs:

View File

@ -1,3 +1,9 @@
## 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 ### Features

View File

@ -1552,10 +1552,19 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
name: Optional[str] = None, name: Optional[str] = None,
register: bool = True, register: bool = True,
) -> Task: ) -> Task:
prepped = cls._prep_task(task, app, loop) if not isinstance(task, Future):
task = loop.create_task(prepped, name=name) prepped = cls._prep_task(task, app, loop)
if sys.version_info < (3, 8):
if name:
error_logger.warning(
"Cannot set a name for a task when using Python 3.7. "
"Your task will be created without a name."
)
task = loop.create_task(prepped)
else:
task = loop.create_task(prepped, name=name)
if name and register: if name and register and sys.version_info > (3, 7):
app._task_registry[name] = task app._task_registry[name] = task
return task return task
@ -1617,10 +1626,12 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
def get_task( def get_task(
self, name: str, *, raise_exception: bool = True self, name: str, *, raise_exception: bool = True
) -> Optional[Task]: ) -> Optional[Task]:
if sys.version_info == (3, 7): if sys.version_info < (3, 8):
raise RuntimeError( error_logger.warning(
"This feature is only supported on using Python 3.8+." "This feature (get_task) is only supported on using "
"Python 3.8+."
) )
return
try: try:
return self._task_registry[name] return self._task_registry[name]
except KeyError: except KeyError:
@ -1637,10 +1648,12 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
*, *,
raise_exception: bool = True, raise_exception: bool = True,
) -> None: ) -> None:
if sys.version_info == (3, 7): if sys.version_info < (3, 8):
raise RuntimeError( error_logger.warning(
"This feature is only supported on using Python 3.8+." "This feature (cancel_task) is only supported on using "
"Python 3.8+."
) )
return
task = self.get_task(name, raise_exception=raise_exception) task = self.get_task(name, raise_exception=raise_exception)
if task and not task.cancelled(): if task and not task.cancelled():
args: Tuple[str, ...] = () args: Tuple[str, ...] = ()
@ -1659,10 +1672,12 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
... ...
def purge_tasks(self): def purge_tasks(self):
if sys.version_info == (3, 7): if sys.version_info < (3, 8):
raise RuntimeError( error_logger.warning(
"This feature is only supported on using Python 3.8+." "This feature (purge_tasks) is only supported on using "
"Python 3.8+."
) )
return
for task in self.tasks: for task in self.tasks:
if task.done() or task.cancelled(): if task.done() or task.cancelled():
name = task.get_name() name = task.get_name()
@ -1675,10 +1690,12 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
def shutdown_tasks( def shutdown_tasks(
self, timeout: Optional[float] = None, increment: float = 0.1 self, timeout: Optional[float] = None, increment: float = 0.1
): ):
if sys.version_info == (3, 7): if sys.version_info < (3, 8):
raise RuntimeError( error_logger.warning(
"This feature is only supported on using Python 3.8+." "This feature (shutdown_tasks) is only supported on using "
"Python 3.8+."
) )
return
for task in self.tasks: for task in self.tasks:
task.cancel() task.cancel()
@ -1692,10 +1709,12 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
@property @property
def tasks(self): def tasks(self):
if sys.version_info == (3, 7): if sys.version_info < (3, 8):
raise RuntimeError( error_logger.warning(
"This feature is only supported on using Python 3.8+." "This feature (tasks) is only supported on using "
"Python 3.8+."
) )
return
return iter(self._task_registry.values()) return iter(self._task_registry.values())
# -------------------------------------------------------------------- # # -------------------------------------------------------------------- #
@ -1709,7 +1728,8 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
details: https://asgi.readthedocs.io/en/latest details: https://asgi.readthedocs.io/en/latest
""" """
self.asgi = True self.asgi = True
self.motd("") if scope["type"] == "lifespan":
self.motd("")
self._asgi_app = await ASGIApp.create(self, scope, receive, send) self._asgi_app = await ASGIApp.create(self, scope, receive, send)
asgi_app = self._asgi_app asgi_app = self._asgi_app
await asgi_app() await asgi_app()

View File

@ -124,22 +124,27 @@ class Config(dict, metaclass=DescriptorMeta):
raise AttributeError(f"Config has no '{ke.args[0]}'") raise AttributeError(f"Config has no '{ke.args[0]}'")
def __setattr__(self, attr, value) -> None: def __setattr__(self, attr, value) -> None:
if attr in self.__class__.__setters__:
try:
super().__setattr__(attr, value)
except AttributeError:
...
else:
return None
self.update({attr: value}) self.update({attr: value})
def __setitem__(self, attr, value) -> None: def __setitem__(self, attr, value) -> None:
self.update({attr: value}) self.update({attr: value})
def update(self, *other, **kwargs) -> None: def update(self, *other, **kwargs) -> None:
other_mapping = {k: v for item in other for k, v in dict(item).items()} kwargs.update({k: v for item in other for k, v in dict(item).items()})
super().update(*other, **kwargs) setters: Dict[str, Any] = {
for attr, value in {**other_mapping, **kwargs}.items(): k: kwargs.pop(k)
for k in {**kwargs}.keys()
if k in self.__class__.__setters__
}
for key, value in setters.items():
try:
super().__setattr__(key, value)
except AttributeError:
...
super().update(**kwargs)
for attr, value in {**setters, **kwargs}.items():
self._post_set(attr, value) self._post_set(attr, value)
def _post_set(self, attr, value) -> None: def _post_set(self, attr, value) -> None:

View File

@ -5,7 +5,7 @@ from os import environ
from pathlib import Path from pathlib import Path
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from textwrap import dedent from textwrap import dedent
from unittest.mock import Mock from unittest.mock import Mock, call
import pytest import pytest
@ -399,5 +399,24 @@ def test_config_set_methods(app: Sanic, monkeypatch: MonkeyPatch):
post_set.assert_called_once_with("FOO", 5) post_set.assert_called_once_with("FOO", 5)
post_set.reset_mock() post_set.reset_mock()
app.config.update_config({"FOO": 6}) app.config.update({"FOO": 6}, {"BAR": 7})
post_set.assert_called_once_with("FOO", 6) post_set.assert_has_calls(
calls=[
call("FOO", 6),
call("BAR", 7),
]
)
post_set.reset_mock()
app.config.update({"FOO": 8}, BAR=9)
post_set.assert_has_calls(
calls=[
call("FOO", 8),
call("BAR", 9),
],
any_order=True,
)
post_set.reset_mock()
app.config.update_config({"FOO": 10})
post_set.assert_called_once_with("FOO", 10)

View File

@ -2,6 +2,7 @@ import asyncio
import sys import sys
from threading import Event from threading import Event
from unittest.mock import Mock
import pytest import pytest
@ -77,6 +78,25 @@ def test_create_named_task(app):
app.run() app.run()
def test_named_task_called(app):
e = Event()
async def coro():
e.set()
@app.route("/")
async def isset(request):
await asyncio.sleep(0.05)
return text(str(e.is_set()))
@app.before_server_start
async def setup(app, _):
app.add_task(coro, name="dummy_task")
request, response = app.test_client.get("/")
assert response.body == b"True"
@pytest.mark.skipif(sys.version_info < (3, 8), reason="Not supported in 3.7") @pytest.mark.skipif(sys.version_info < (3, 8), reason="Not supported in 3.7")
def test_create_named_task_fails_outside_app(app): def test_create_named_task_fails_outside_app(app):
async def dummy(): async def dummy():

View File

@ -334,6 +334,22 @@ def test_config_fallback_before_and_after_startup(app):
assert response.content_type == "text/plain; charset=utf-8" assert response.content_type == "text/plain; charset=utf-8"
def test_config_fallback_using_update_dict(app):
app.config.update({"FALLBACK_ERROR_FORMAT": "text"})
_, response = app.test_client.get("/error")
assert response.status == 500
assert response.content_type == "text/plain; charset=utf-8"
def test_config_fallback_using_update_kwarg(app):
app.config.update(FALLBACK_ERROR_FORMAT="text")
_, response = app.test_client.get("/error")
assert response.status == 500
assert response.content_type == "text/plain; charset=utf-8"
def test_config_fallback_bad_value(app): def test_config_fallback_bad_value(app):
message = "Unknown format: fake" message = "Unknown format: fake"
with pytest.raises(SanicException, match=message): with pytest.raises(SanicException, match=message):

View File

@ -62,19 +62,15 @@ def test_streaming_body_requests(app):
data = ["hello", "world"] data = ["hello", "world"]
class Data(AsyncByteStream):
def __init__(self, data):
self.data = data
async def __aiter__(self):
for value in self.data:
yield value.encode("utf-8")
client = ReusableClient(app, port=1234) client = ReusableClient(app, port=1234)
async def stream(data):
for value in data:
yield value.encode("utf-8")
with client: with client:
_, response1 = client.post("/", data=Data(data)) _, response1 = client.post("/", data=stream(data))
_, response2 = client.post("/", data=Data(data)) _, response2 = client.post("/", data=stream(data))
assert response1.status == response2.status == 200 assert response1.status == response2.status == 200
assert response1.json["data"] == response2.json["data"] == data assert response1.json["data"] == response2.json["data"] == data

View File

@ -4,8 +4,8 @@ from unittest.mock import Mock, patch
import pytest import pytest
from sanic.server import loop
from sanic.compat import OS_IS_WINDOWS, UVLOOP_INSTALLED from sanic.compat import OS_IS_WINDOWS, UVLOOP_INSTALLED
from sanic.server import loop
@pytest.mark.skipif( @pytest.mark.skipif(