Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Copy Blueprints Implementation #2184

Merged
merged 29 commits into from
Aug 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
d6037fc
Initial version of Blueprint.copy, with a test case.
Jul 7, 2021
94f840d
Merge branch 'main' into zhiwei/bp-copy
Jul 7, 2021
46d9ebf
Merge branch 'main' into zhiwei/bp-copy
ChihweiLHBird Jul 8, 2021
69c1a8e
Improve copy method in blueprint, adding possibiliy to keep original …
Jul 13, 2021
47ebdda
Merge branch 'main' into zhiwei/bp-copy
Jul 13, 2021
be1c09e
Fix typing
Jul 13, 2021
0a8ee7b
Fix typing 2
Jul 13, 2021
176db5f
Merge branch 'zhiwei/bp-copy' of https://github.com/ChihweiLHBird/san…
Jul 13, 2021
762c25e
doc fix
Jul 13, 2021
d4cd897
Merge branch 'main' into zhiwei/bp-copy
ahopkins Jul 20, 2021
80029d6
Add reset method for init and reset bp instance; update copy method
Jul 21, 2021
84f9659
Merge branch 'main' into zhiwei/bp-copy
Aug 6, 2021
0e90d9c
Move reset method to above; try fix lint
Aug 6, 2021
45dd2a0
Update test bp copy
Aug 6, 2021
d072ae3
make pretty
Aug 6, 2021
9ae7158
Merge branch 'main' into zhiwei/bp-copy
Aug 6, 2021
54cee45
Merge branch 'main' into zhiwei/bp-copy
Aug 7, 2021
9c8f2c6
Fix if statement logic
ChihweiLHBird Aug 7, 2021
1022a46
Change Exception to SanicException
Aug 7, 2021
3238aec
Move reset to the front of copy method.
Aug 8, 2021
fb41b3e
call reset at the beginning
Aug 8, 2021
4ebb773
Update copy blueprint tests
Aug 9, 2021
9089c79
Merge branch 'main' into zhiwei/bp-copy
Aug 9, 2021
e04cd75
Add more attrs check in test cases
Aug 9, 2021
903de13
Add version_prefix override test
Aug 9, 2021
8588c77
Update sanic/blueprints.py
ahopkins Aug 9, 2021
0c2d3ec
Fix
Aug 9, 2021
cc81895
Merge branch 'zhiwei/bp-copy' of https://github.com/ChihweiLHBird/san…
Aug 9, 2021
f88e873
Merge branch 'main' into zhiwei/bp-copy
Aug 9, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 79 additions & 9 deletions sanic/blueprints.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import asyncio

from collections import defaultdict
from copy import deepcopy
from types import SimpleNamespace
from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Set, Union

Expand All @@ -12,6 +13,7 @@
from sanic.base import BaseSanic
from sanic.blueprint_group import BlueprintGroup
from sanic.exceptions import SanicException
from sanic.helpers import Default, _default
from sanic.models.futures import FutureRoute, FutureStatic
from sanic.models.handler_types import (
ListenerType,
Expand Down Expand Up @@ -40,7 +42,7 @@ class Blueprint(BaseSanic):
:param host: IP Address of FQDN for the sanic server to use.
:param version: Blueprint Version
:param strict_slashes: Enforce the API urls are requested with a
training */*
trailing */*
"""

__fake_slots__ = (
Expand Down Expand Up @@ -76,15 +78,9 @@ def __init__(
version_prefix: str = "/v",
):
super().__init__(name=name)

self._apps: Set[Sanic] = set()
self.reset()
self.ctx = SimpleNamespace()
self.exceptions: List[RouteHandler] = []
self.host = host
self.listeners: Dict[str, List[ListenerType]] = {}
self.middlewares: List[MiddlewareType] = []
self.routes: List[Route] = []
self.statics: List[RouteHandler] = []
self.strict_slashes = strict_slashes
self.url_prefix = (
url_prefix[:-1]
Expand All @@ -93,7 +89,6 @@ def __init__(
)
self.version = version
self.version_prefix = version_prefix
self.websocket_routes: List[Route] = []

def __repr__(self) -> str:
args = ", ".join(
Expand Down Expand Up @@ -144,6 +139,81 @@ def signal(self, event: str, *args, **kwargs):
kwargs["apply"] = False
return super().signal(event, *args, **kwargs)

def reset(self):
self._apps: Set[Sanic] = set()
self.exceptions: List[RouteHandler] = []
self.listeners: Dict[str, List[ListenerType]] = {}
self.middlewares: List[MiddlewareType] = []
self.routes: List[Route] = []
self.statics: List[RouteHandler] = []
self.websocket_routes: List[Route] = []

def copy(
self,
name: str,
url_prefix: Optional[Union[str, Default]] = _default,
version: Optional[Union[int, str, float, Default]] = _default,
version_prefix: Union[str, Default] = _default,
strict_slashes: Optional[Union[bool, Default]] = _default,
with_registration: bool = True,
with_ctx: bool = False,
):
"""
Copy a blueprint instance with some optional parameters to
override the values of attributes in the old instance.

:param name: unique name of the blueprint
:param url_prefix: URL to be prefixed before all route URLs
:param version: Blueprint Version
:param version_prefix: the prefix of the version number shown in the
URL.
:param strict_slashes: Enforce the API urls are requested with a
trailing */*
:param with_registration: whether register new blueprint instance with
sanic apps that were registered with the old instance or not.
:param with_ctx: whether ``ctx`` will be copied or not.
"""

attrs_backup = {
"_apps": self._apps,
"routes": self.routes,
"websocket_routes": self.websocket_routes,
"middlewares": self.middlewares,
"exceptions": self.exceptions,
"listeners": self.listeners,
"statics": self.statics,
}

self.reset()
new_bp = deepcopy(self)
ahopkins marked this conversation as resolved.
Show resolved Hide resolved
new_bp.name = name

if not isinstance(url_prefix, Default):
new_bp.url_prefix = url_prefix
if not isinstance(version, Default):
new_bp.version = version
if not isinstance(strict_slashes, Default):
new_bp.strict_slashes = strict_slashes
if not isinstance(version_prefix, Default):
new_bp.version_prefix = version_prefix

for key, value in attrs_backup.items():
setattr(self, key, value)
ahopkins marked this conversation as resolved.
Show resolved Hide resolved

if with_registration and self._apps:
if new_bp._future_statics:
raise SanicException(
"Static routes registered with the old blueprint instance,"
" cannot be registered again."
)
for app in self._apps:
app.blueprint(new_bp)

if not with_ctx:
new_bp.ctx = SimpleNamespace()
ahopkins marked this conversation as resolved.
Show resolved Hide resolved

return new_bp

@staticmethod
def group(
*blueprints: Union[Blueprint, BlueprintGroup],
Expand Down
14 changes: 14 additions & 0 deletions sanic/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,3 +155,17 @@ def import_string(module_name, package=None):
if ismodule(obj):
return obj
return obj()


class Default:
"""
It is used to replace `None` or `object()` as a sentinel
that represents a default value. Sometimes we want to set
a value to `None` so we cannot use `None` to represent the
default value, and `object()` is hard to be typed.
"""

pass


_default = Default()
70 changes: 70 additions & 0 deletions tests/test_blueprint_copy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
from copy import deepcopy

from sanic import Blueprint, Sanic, blueprints, response
from sanic.response import text


def test_bp_copy(app: Sanic):
bp1 = Blueprint("test_bp1", version=1)
bp1.ctx.test = 1
assert hasattr(bp1.ctx, "test")

@bp1.route("/page")
def handle_request(request):
return text("Hello world!")

bp2 = bp1.copy(name="test_bp2", version=2)
assert id(bp1) != id(bp2)
assert bp1._apps == bp2._apps == set()
assert not hasattr(bp2.ctx, "test")
assert len(bp2._future_exceptions) == len(bp1._future_exceptions)
assert len(bp2._future_listeners) == len(bp1._future_listeners)
assert len(bp2._future_middleware) == len(bp1._future_middleware)
assert len(bp2._future_routes) == len(bp1._future_routes)
assert len(bp2._future_signals) == len(bp1._future_signals)

app.blueprint(bp1)
app.blueprint(bp2)

bp3 = bp1.copy(name="test_bp3", version=3, with_registration=True)
assert id(bp1) != id(bp3)
assert bp1._apps == bp3._apps and bp3._apps
assert not hasattr(bp3.ctx, "test")

bp4 = bp1.copy(name="test_bp4", version=4, with_ctx=True)
assert id(bp1) != id(bp4)
assert bp4.ctx.test == 1

bp5 = bp1.copy(name="test_bp5", version=5, with_registration=False)
assert id(bp1) != id(bp5)
assert not bp5._apps
assert bp1._apps != set()

app.blueprint(bp5)

bp6 = bp1.copy(
name="test_bp6",
version=6,
with_registration=True,
version_prefix="/version",
)
assert bp6._apps
assert bp6.version_prefix == "/version"

_, response = app.test_client.get("/v1/page")
assert "Hello world!" in response.text

_, response = app.test_client.get("/v2/page")
assert "Hello world!" in response.text

_, response = app.test_client.get("/v3/page")
assert "Hello world!" in response.text

_, response = app.test_client.get("/v4/page")
assert "Hello world!" in response.text

_, response = app.test_client.get("/v5/page")
assert "Hello world!" in response.text

_, response = app.test_client.get("/version6/page")
assert "Hello world!" in response.text