Allow blueprints and groups to be infinitely reusable (#2150)

* Allow blueprints and groups to be infinitely reusable
This commit is contained in:
Adam Hopkins 2021-06-21 18:41:04 +03:00 committed by GitHub
parent 108a4a99c7
commit 53da4dd091
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 92 additions and 58 deletions

View File

@ -420,7 +420,33 @@ class Sanic(BaseSanic):
"""
if isinstance(blueprint, (list, tuple, BlueprintGroup)):
for item in blueprint:
self.blueprint(item, **options)
params = {**options}
if isinstance(blueprint, BlueprintGroup):
if blueprint.url_prefix:
merge_from = [
options.get("url_prefix", ""),
blueprint.url_prefix,
]
if not isinstance(item, BlueprintGroup):
merge_from.append(item.url_prefix or "")
merged_prefix = "/".join(
u.strip("/") for u in merge_from
).rstrip("/")
params["url_prefix"] = f"/{merged_prefix}"
for _attr in ["version", "strict_slashes"]:
if getattr(item, _attr) is None:
params[_attr] = getattr(
blueprint, _attr
) or options.get(_attr)
if item.version_prefix == "/v":
if blueprint.version_prefix == "/v":
params["version_prefix"] = options.get(
"version_prefix"
)
else:
params["version_prefix"] = blueprint.version_prefix
self.blueprint(item, **params)
return
if blueprint.name in self.blueprints:
assert self.blueprints[blueprint.name] is blueprint, (

View File

@ -1,8 +1,8 @@
from __future__ import annotations
from collections.abc import MutableSequence
from typing import TYPE_CHECKING, List, Optional, Union
import sanic
if TYPE_CHECKING:
from sanic.blueprints import Blueprint
@ -97,7 +97,7 @@ class BlueprintGroup(MutableSequence):
return self._url_prefix
@property
def blueprints(self) -> List["sanic.Blueprint"]:
def blueprints(self) -> List[Blueprint]:
"""
Retrieve a list of all the available blueprints under this group.
@ -187,37 +187,16 @@ class BlueprintGroup(MutableSequence):
"""
return len(self._blueprints)
def _sanitize_blueprint(self, bp: "sanic.Blueprint") -> "sanic.Blueprint":
"""
Sanitize the Blueprint Entity to override the Version and strict slash
behaviors as required.
:param bp: Sanic Blueprint entity Object
:return: Modified Blueprint
"""
if self._url_prefix:
merged_prefix = "/".join(
u.strip("/") for u in [self._url_prefix, bp.url_prefix or ""]
).rstrip("/")
bp.url_prefix = f"/{merged_prefix}"
for _attr in ["version", "strict_slashes"]:
if getattr(bp, _attr) is None:
setattr(bp, _attr, getattr(self, _attr))
if bp.version_prefix == "/v":
bp.version_prefix = self._version_prefix
return bp
def append(self, value: "sanic.Blueprint") -> None:
def append(self, value: Blueprint) -> None:
"""
The Abstract class `MutableSequence` leverages this append method to
perform the `BlueprintGroup.append` operation.
:param value: New `Blueprint` object.
:return: None
"""
self._blueprints.append(self._sanitize_blueprint(bp=value))
self._blueprints.append(value)
def insert(self, index: int, item: "sanic.Blueprint") -> None:
def insert(self, index: int, item: Blueprint) -> None:
"""
The Abstract class `MutableSequence` leverages this insert method to
perform the `BlueprintGroup.append` operation.
@ -226,7 +205,7 @@ class BlueprintGroup(MutableSequence):
:param item: New `Blueprint` object.
:return: None
"""
self._blueprints.insert(index, self._sanitize_blueprint(item))
self._blueprints.insert(index, item)
def middleware(self, *args, **kwargs):
"""

View File

@ -168,8 +168,6 @@ class Blueprint(BaseSanic):
for i in nested:
if isinstance(i, (list, tuple)):
yield from chain(i)
elif isinstance(i, BlueprintGroup):
yield from i.blueprints
else:
yield i
@ -196,6 +194,7 @@ class Blueprint(BaseSanic):
self._apps.add(app)
url_prefix = options.get("url_prefix", self.url_prefix)
opt_version = options.get("version", None)
opt_strict_slashes = options.get("strict_slashes", None)
opt_version_prefix = options.get("version_prefix", self.version_prefix)
routes = []
@ -220,18 +219,13 @@ class Blueprint(BaseSanic):
version_prefix = prefix
break
version = self.version
for v in (future.version, opt_version, self.version):
if v is not None:
version = v
break
strict_slashes = (
self.strict_slashes
if future.strict_slashes is None
and self.strict_slashes is not None
else future.strict_slashes
version = self._extract_value(
future.version, opt_version, self.version
)
strict_slashes = self._extract_value(
future.strict_slashes, opt_strict_slashes, self.strict_slashes
)
name = app._generate_name(future.name)
apply_route = FutureRoute(
@ -315,3 +309,12 @@ class Blueprint(BaseSanic):
return_when=asyncio.FIRST_COMPLETED,
timeout=timeout,
)
@staticmethod
def _extract_value(*values):
value = values[-1]
for v in values:
if v is not None:
value = v
break
return value

View File

@ -200,7 +200,7 @@ def test_bp_group_as_nested_group():
blueprint_group_1 = Blueprint.group(
Blueprint.group(blueprint_1, blueprint_2)
)
assert len(blueprint_group_1) == 2
assert len(blueprint_group_1) == 1
def test_blueprint_group_insert():
@ -215,9 +215,29 @@ def test_blueprint_group_insert():
group.insert(0, blueprint_1)
group.insert(0, blueprint_2)
group.insert(0, blueprint_3)
assert group.blueprints[1].strict_slashes is False
assert group.blueprints[2].strict_slashes is True
assert group.blueprints[0].url_prefix == "/test"
@blueprint_1.route("/")
def blueprint_1_default_route(request):
return text("BP1_OK")
@blueprint_2.route("/")
def blueprint_2_default_route(request):
return text("BP2_OK")
@blueprint_3.route("/")
def blueprint_3_default_route(request):
return text("BP3_OK")
app = Sanic("PropTest")
app.blueprint(group)
app.router.finalize()
routes = [(route.path, route.strict) for route in app.router.routes]
assert len(routes) == 3
assert ("v1/test/bp1/", True) in routes
assert ("v1.3/test/bp2", False) in routes
assert ("v1.3/test", False) in routes
def test_bp_group_properties():
@ -231,19 +251,25 @@ def test_bp_group_properties():
url_prefix="/grouped",
strict_slashes=True,
)
primary = Blueprint.group(group, url_prefix="/primary")
assert group.version_prefix == "/api/v"
assert blueprint_1.version_prefix == "/api/v"
assert blueprint_2.version_prefix == "/api/v"
@blueprint_1.route("/")
def blueprint_1_default_route(request):
return text("BP1_OK")
assert group.version == 1
assert blueprint_1.version == 1
assert blueprint_2.version == 1
@blueprint_2.route("/")
def blueprint_2_default_route(request):
return text("BP2_OK")
assert group.strict_slashes
assert blueprint_1.strict_slashes
assert blueprint_2.strict_slashes
app = Sanic("PropTest")
app.blueprint(group)
app.blueprint(primary)
app.router.finalize()
assert group.url_prefix == "/grouped"
assert blueprint_1.url_prefix == "/grouped/bp1"
assert blueprint_2.url_prefix == "/grouped/bp2"
routes = [route.path for route in app.router.routes]
assert len(routes) == 4
assert "api/v1/grouped/bp1/" in routes
assert "api/v1/grouped/bp2/" in routes
assert "api/v1/primary/grouped/bp1" in routes
assert "api/v1/primary/grouped/bp2" in routes