Repository: sanic-org/sanic

Issue:
Support for Python 3.12
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Is your feature request related to a problem? Please describe.

Currently unable to use Sanic with Python 3.12

### Describe the solution you'd like

[uvloop 0.18.0 was just released](https://github.com/MagicStack/uvloop/releases/tag/v0.18.0) and supports Python 3.12

It'd be great to get support for Python 3.12 on the next release of Sanic along with the 2022.12 LTS release

### Additional context

_No response_


Discussion:
Making this the tracking issue for Python 3.12 compatibility. Please post any incompatibilities found here. We probably need to update some dependencies, and add 3.12 testing to our CI. Rarely have any Python updates (other than asyncio loop argument removal) prompted for changes in Sanic itself.


@ahopkins Is this feature something what is okay to pick up into progress by someone whom isn't part of the core maintainers group ?  
@iAndriy Feel free to hack it, PRs from anyone are welcome here! And core developers are quite overloaded already =)
A good start would be to just add to the CI, open a PR, mark it as draft and list out all the errors you encountered.

First and foremost add it to tox.ini and run the tests.

```
tox -e py312
> A good start would be to just add to the CI, open a PR, mark it as draft and list out all the errors you encountered.
> 
> First and foremost add it to tox.ini and run the tests.
> 
> ```
> tox -e py312
> ```

@ahopkins thanks for suggestion

Resolving patch:
diff --git a/sanic/__version__.py b/sanic/__version__.py
--- a/sanic/__version__.py
+++ b/sanic/__version__.py
@@ -1 +1 @@
-__version__ = "23.12.0"
+__version__ = "23.12.1"
diff --git a/sanic/compat.py b/sanic/compat.py
--- a/sanic/compat.py
+++ b/sanic/compat.py
@@ -126,11 +126,11 @@ def __getattr__(self, key: str) -> str:
         if key.startswith("_"):
             return self.__getattribute__(key)
         key = key.rstrip("_").replace("_", "-")
-        return ",".join(self.getall(key, default=[]))
+        return ",".join(self.getall(key, []))
 
     def get_all(self, key: str):
         """Convenience method mapped to ``getall()``."""
-        return self.getall(key, default=[])
+        return self.getall(key, [])
 
 
 use_trio = sys.argv[0].endswith("hypercorn") and "trio" in sys.argv


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -41,6 +41,7 @@ jobs:
           - { python-version: "3.10", tox-env: py310,         max-attempts: 3 }
           - { python-version: "3.10", tox-env: py310-no-ext,  max-attempts: 3 }
           - { python-version: "3.11", tox-env: py311,         max-attempts: 3 }
+          - { python-version: "3.12", tox-env: py312,         max-attempts: 3 }
           - { python-version: "3.11", tox-env: py311-no-ext,  max-attempts: 3 }
           - { python-version: "3.8",  tox-env: py38-no-ext,   platform: windows-latest, ignore-errors: true }
           - { python-version: "3.9",  tox-env: py39-no-ext,   platform: windows-latest, ignore-errors: true }
diff --git a/tests/test_app.py b/tests/test_app.py
--- a/tests/test_app.py
+++ b/tests/test_app.py
@@ -657,3 +657,15 @@ def test_stop_trigger_terminate(app: Sanic):
 
     app.stop(unregister=False)
     app.multiplexer.terminate.assert_called_once()
+
+
+def test_refresh_pass_passthru_data_to_new_instance(app: Sanic):
+    # arrange
+    passthru = {
+        '_inspector': 2,
+        'config': {'TOUCHUP': 23}
+    }
+    app = app.refresh(passthru)
+
+    assert app.inspector == 2
+    assert app.config.TOUCHUP == 23
diff --git a/tests/test_blueprint_group.py b/tests/test_blueprint_group.py
--- a/tests/test_blueprint_group.py
+++ b/tests/test_blueprint_group.py
@@ -25,6 +25,16 @@ def test_bp_group_indexing(app: Sanic):
     with raises(expected_exception=IndexError):
         _ = group[3]
 
+def test_bp_group_set_item_by_index(app: Sanic):
+    blueprint_1 = Blueprint("blueprint_1", url_prefix="/bp1")
+    blueprint_2 = Blueprint("blueprint_2", url_prefix="/bp2")
+
+    group = Blueprint.group(blueprint_1, blueprint_2)
+    group[0] = blueprint_2
+
+    assert group[0] == blueprint_2
+
+
 
 def test_bp_group_with_additional_route_params(app: Sanic):
     blueprint_1 = Blueprint("blueprint_1", url_prefix="/bp1")
diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py
--- a/tests/test_blueprints.py
+++ b/tests/test_blueprints.py
@@ -1112,3 +1112,43 @@ async def index(_):
     app.router.finalize()
 
     assert app.router.routes[0].path == "foo/"
+
+
+def test_blueprint_copy_returns_blueprint_with_the_name_of_original_blueprint(
+    app: Sanic,
+):
+    # arrange
+    bp = Blueprint("bp")
+
+    # act
+    actual = bp.copy("new_bp_name")
+
+    # assert
+    assert bp.name == actual.copied_from
+
+
+def test_blueprint_copy_returns_blueprint_with_overwritten_properties(
+    app: Sanic,
+):
+    # arrange
+    bp = Blueprint("bp")
+    to_override_attrs = expected = dict(
+        url_prefix="v2",
+        version="v2",
+        version_prefix="v2",
+        allow_route_overwrite=True,
+        strict_slashes=True,
+    )
+
+    # act
+    actual = bp.copy(
+        "new_bp_name",
+        **to_override_attrs,
+    )
+
+    # assert
+    assert all(
+        value == getattr(actual, key)
+        for key, value in expected.items()
+        if hasattr(actual, key)
+    )
diff --git a/tests/test_websockets.py b/tests/test_websockets.py
--- a/tests/test_websockets.py
+++ b/tests/test_websockets.py
@@ -5,7 +5,7 @@
 
 import pytest
 
-from websockets.frames import CTRL_OPCODES, DATA_OPCODES, Frame
+from websockets.frames import CTRL_OPCODES, DATA_OPCODES, OP_TEXT, Frame
 
 from sanic.exceptions import ServerError
 from sanic.server.websockets.frame import WebsocketFrameAssembler
@@ -210,17 +210,14 @@ async def test_ws_frame_put_message_complete(opcode):
 @pytest.mark.asyncio
 @pytest.mark.parametrize("opcode", DATA_OPCODES)
 async def test_ws_frame_put_message_into_queue(opcode):
+    foo = "foo" if (opcode == OP_TEXT) else b"foo"
     assembler = WebsocketFrameAssembler(Mock())
     assembler.chunks_queue = AsyncMock(spec=Queue)
     assembler.message_fetched = AsyncMock()
     assembler.message_fetched.is_set = Mock(return_value=False)
-
     await assembler.put(Frame(opcode, b"foo"))
 
-    assembler.chunks_queue.put.has_calls(
-        call(b"foo"),
-        call(None),
-    )
+    assert assembler.chunks_queue.put.call_args_list == [call(foo), call(None)]
 
 
 @pytest.mark.asyncio