diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 5108c247..1113fa80 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -2,9 +2,13 @@ name: "CodeQL" on: push: - branches: [ main ] + branches: + - main + - "*LTS" pull_request: - branches: [ main ] + branches: + - main + - "*LTS" types: [opened, synchronize, reopened, ready_for_review] schedule: - cron: '25 16 * * 0' diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 92d93aa7..9b5834fa 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -3,6 +3,7 @@ on: push: branches: - main + - "*LTS" tags: - "!*" # Do not execute on tags pull_request: diff --git a/.github/workflows/pr-bandit.yml b/.github/workflows/pr-bandit.yml index ca91312a..2bd70204 100644 --- a/.github/workflows/pr-bandit.yml +++ b/.github/workflows/pr-bandit.yml @@ -3,6 +3,7 @@ on: pull_request: branches: - main + - "*LTS" types: [opened, synchronize, reopened, ready_for_review] jobs: diff --git a/.github/workflows/pr-docs.yml b/.github/workflows/pr-docs.yml index 7b3c2f6e..8479aef5 100644 --- a/.github/workflows/pr-docs.yml +++ b/.github/workflows/pr-docs.yml @@ -3,6 +3,7 @@ on: pull_request: branches: - main + - "*LTS" types: [opened, synchronize, reopened, ready_for_review] jobs: diff --git a/.github/workflows/pr-linter.yml b/.github/workflows/pr-linter.yml index 9ed45d0a..11ad9d29 100644 --- a/.github/workflows/pr-linter.yml +++ b/.github/workflows/pr-linter.yml @@ -3,6 +3,7 @@ on: pull_request: branches: - main + - "*LTS" types: [opened, synchronize, reopened, ready_for_review] jobs: diff --git a/.github/workflows/pr-python310.yml b/.github/workflows/pr-python310.yml index f3f7c607..5e66deec 100644 --- a/.github/workflows/pr-python310.yml +++ b/.github/workflows/pr-python310.yml @@ -3,6 +3,7 @@ on: pull_request: branches: - main + - "*LTS" types: [opened, synchronize, reopened, ready_for_review] jobs: diff --git a/.github/workflows/pr-python37.yml b/.github/workflows/pr-python37.yml index 50f79c6e..c0051d33 100644 --- a/.github/workflows/pr-python37.yml +++ b/.github/workflows/pr-python37.yml @@ -3,6 +3,7 @@ on: pull_request: branches: - main + - "*LTS" types: [opened, synchronize, reopened, ready_for_review] jobs: diff --git a/.github/workflows/pr-python38.yml b/.github/workflows/pr-python38.yml index 1e0b8050..09e93f3f 100644 --- a/.github/workflows/pr-python38.yml +++ b/.github/workflows/pr-python38.yml @@ -3,6 +3,7 @@ on: pull_request: branches: - main + - "*LTS" types: [opened, synchronize, reopened, ready_for_review] jobs: diff --git a/.github/workflows/pr-python39.yml b/.github/workflows/pr-python39.yml index 1abd6bcb..ff479459 100644 --- a/.github/workflows/pr-python39.yml +++ b/.github/workflows/pr-python39.yml @@ -3,6 +3,7 @@ on: pull_request: branches: - main + - "*LTS" types: [opened, synchronize, reopened, ready_for_review] jobs: diff --git a/.github/workflows/pr-type-check.yml b/.github/workflows/pr-type-check.yml index 2fae03be..58a90ee3 100644 --- a/.github/workflows/pr-type-check.yml +++ b/.github/workflows/pr-type-check.yml @@ -3,6 +3,7 @@ on: pull_request: branches: - main + - "*LTS" types: [opened, synchronize, reopened, ready_for_review] jobs: @@ -15,7 +16,7 @@ jobs: matrix: os: [ubuntu-latest] 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.9, tox-env: type-checking} - { python-version: "3.10", tox-env: type-checking} diff --git a/.github/workflows/pr-windows.yml b/.github/workflows/pr-windows.yml index 9721b5b5..050ee2bb 100644 --- a/.github/workflows/pr-windows.yml +++ b/.github/workflows/pr-windows.yml @@ -3,6 +3,7 @@ on: pull_request: branches: - main + - "*LTS" types: [opened, synchronize, reopened, ready_for_review] jobs: diff --git a/docs/sanic/releases/21/21.12.md b/docs/sanic/releases/21/21.12.md index 6c4dc419..f8f0d954 100644 --- a/docs/sanic/releases/21/21.12.md +++ b/docs/sanic/releases/21/21.12.md @@ -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 ### Features diff --git a/sanic/app.py b/sanic/app.py index c393c715..97c7a177 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -1552,10 +1552,19 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): name: Optional[str] = None, register: bool = True, ) -> Task: - prepped = cls._prep_task(task, app, loop) - task = loop.create_task(prepped, name=name) + if not isinstance(task, Future): + 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 return task @@ -1617,10 +1626,12 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): def get_task( self, name: str, *, raise_exception: bool = True ) -> Optional[Task]: - if sys.version_info == (3, 7): - raise RuntimeError( - "This feature is only supported on using Python 3.8+." + if sys.version_info < (3, 8): + error_logger.warning( + "This feature (get_task) is only supported on using " + "Python 3.8+." ) + return try: return self._task_registry[name] except KeyError: @@ -1637,10 +1648,12 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): *, raise_exception: bool = True, ) -> None: - if sys.version_info == (3, 7): - raise RuntimeError( - "This feature is only supported on using Python 3.8+." + if sys.version_info < (3, 8): + error_logger.warning( + "This feature (cancel_task) is only supported on using " + "Python 3.8+." ) + return task = self.get_task(name, raise_exception=raise_exception) if task and not task.cancelled(): args: Tuple[str, ...] = () @@ -1659,10 +1672,12 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): ... def purge_tasks(self): - if sys.version_info == (3, 7): - raise RuntimeError( - "This feature is only supported on using Python 3.8+." + if sys.version_info < (3, 8): + error_logger.warning( + "This feature (purge_tasks) is only supported on using " + "Python 3.8+." ) + return for task in self.tasks: if task.done() or task.cancelled(): name = task.get_name() @@ -1675,10 +1690,12 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): def shutdown_tasks( self, timeout: Optional[float] = None, increment: float = 0.1 ): - if sys.version_info == (3, 7): - raise RuntimeError( - "This feature is only supported on using Python 3.8+." + if sys.version_info < (3, 8): + error_logger.warning( + "This feature (shutdown_tasks) is only supported on using " + "Python 3.8+." ) + return for task in self.tasks: task.cancel() @@ -1692,10 +1709,12 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): @property def tasks(self): - if sys.version_info == (3, 7): - raise RuntimeError( - "This feature is only supported on using Python 3.8+." + if sys.version_info < (3, 8): + error_logger.warning( + "This feature (tasks) is only supported on using " + "Python 3.8+." ) + return return iter(self._task_registry.values()) # -------------------------------------------------------------------- # @@ -1709,7 +1728,8 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): details: https://asgi.readthedocs.io/en/latest """ self.asgi = True - self.motd("") + if scope["type"] == "lifespan": + self.motd("") self._asgi_app = await ASGIApp.create(self, scope, receive, send) asgi_app = self._asgi_app await asgi_app() diff --git a/sanic/config.py b/sanic/config.py index 30c8627f..ac147ef3 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -124,22 +124,27 @@ class Config(dict, metaclass=DescriptorMeta): raise AttributeError(f"Config has no '{ke.args[0]}'") 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}) def __setitem__(self, attr, value) -> None: self.update({attr: value}) def update(self, *other, **kwargs) -> None: - other_mapping = {k: v for item in other for k, v in dict(item).items()} - super().update(*other, **kwargs) - for attr, value in {**other_mapping, **kwargs}.items(): + kwargs.update({k: v for item in other for k, v in dict(item).items()}) + setters: Dict[str, Any] = { + 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) def _post_set(self, attr, value) -> None: diff --git a/tests/test_config.py b/tests/test_config.py index 9237b55c..3cb33f68 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -5,7 +5,7 @@ from os import environ from pathlib import Path from tempfile import TemporaryDirectory from textwrap import dedent -from unittest.mock import Mock +from unittest.mock import Mock, call 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.reset_mock() - app.config.update_config({"FOO": 6}) - post_set.assert_called_once_with("FOO", 6) + app.config.update({"FOO": 6}, {"BAR": 7}) + 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) diff --git a/tests/test_create_task.py b/tests/test_create_task.py index c98666a9..a11bc302 100644 --- a/tests/test_create_task.py +++ b/tests/test_create_task.py @@ -2,6 +2,7 @@ import asyncio import sys from threading import Event +from unittest.mock import Mock import pytest @@ -77,6 +78,25 @@ def test_create_named_task(app): 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") def test_create_named_task_fails_outside_app(app): async def dummy(): diff --git a/tests/test_errorpages.py b/tests/test_errorpages.py index f8e425b0..f1e5fbd8 100644 --- a/tests/test_errorpages.py +++ b/tests/test_errorpages.py @@ -334,6 +334,22 @@ def test_config_fallback_before_and_after_startup(app): 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): message = "Unknown format: fake" with pytest.raises(SanicException, match=message): diff --git a/tests/test_pipelining.py b/tests/test_pipelining.py index 2bb29c52..6c998756 100644 --- a/tests/test_pipelining.py +++ b/tests/test_pipelining.py @@ -62,19 +62,15 @@ def test_streaming_body_requests(app): 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) + async def stream(data): + for value in data: + yield value.encode("utf-8") + with client: - _, response1 = client.post("/", data=Data(data)) - _, response2 = client.post("/", data=Data(data)) + _, response1 = client.post("/", data=stream(data)) + _, response2 = client.post("/", data=stream(data)) assert response1.status == response2.status == 200 assert response1.json["data"] == response2.json["data"] == data diff --git a/tests/test_server_loop.py b/tests/test_server_loop.py index fbd5cc2b..30077178 100644 --- a/tests/test_server_loop.py +++ b/tests/test_server_loop.py @@ -4,8 +4,8 @@ from unittest.mock import Mock, patch import pytest -from sanic.server import loop from sanic.compat import OS_IS_WINDOWS, UVLOOP_INSTALLED +from sanic.server import loop @pytest.mark.skipif(