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)): if isinstance(blueprint, (list, tuple, BlueprintGroup)):
for item in blueprint: 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 return
if blueprint.name in self.blueprints: if blueprint.name in self.blueprints:
assert self.blueprints[blueprint.name] is blueprint, ( assert self.blueprints[blueprint.name] is blueprint, (

View File

@ -1,8 +1,8 @@
from __future__ import annotations
from collections.abc import MutableSequence from collections.abc import MutableSequence
from typing import TYPE_CHECKING, List, Optional, Union from typing import TYPE_CHECKING, List, Optional, Union
import sanic
if TYPE_CHECKING: if TYPE_CHECKING:
from sanic.blueprints import Blueprint from sanic.blueprints import Blueprint
@ -97,7 +97,7 @@ class BlueprintGroup(MutableSequence):
return self._url_prefix return self._url_prefix
@property @property
def blueprints(self) -> List["sanic.Blueprint"]: def blueprints(self) -> List[Blueprint]:
""" """
Retrieve a list of all the available blueprints under this group. Retrieve a list of all the available blueprints under this group.
@ -187,37 +187,16 @@ class BlueprintGroup(MutableSequence):
""" """
return len(self._blueprints) return len(self._blueprints)
def _sanitize_blueprint(self, bp: "sanic.Blueprint") -> "sanic.Blueprint": def append(self, value: Blueprint) -> None:
"""
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:
""" """
The Abstract class `MutableSequence` leverages this append method to The Abstract class `MutableSequence` leverages this append method to
perform the `BlueprintGroup.append` operation. perform the `BlueprintGroup.append` operation.
:param value: New `Blueprint` object. :param value: New `Blueprint` object.
:return: None :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 The Abstract class `MutableSequence` leverages this insert method to
perform the `BlueprintGroup.append` operation. perform the `BlueprintGroup.append` operation.
@ -226,7 +205,7 @@ class BlueprintGroup(MutableSequence):
:param item: New `Blueprint` object. :param item: New `Blueprint` object.
:return: None :return: None
""" """
self._blueprints.insert(index, self._sanitize_blueprint(item)) self._blueprints.insert(index, item)
def middleware(self, *args, **kwargs): def middleware(self, *args, **kwargs):
""" """

View File

@ -168,8 +168,6 @@ class Blueprint(BaseSanic):
for i in nested: for i in nested:
if isinstance(i, (list, tuple)): if isinstance(i, (list, tuple)):
yield from chain(i) yield from chain(i)
elif isinstance(i, BlueprintGroup):
yield from i.blueprints
else: else:
yield i yield i
@ -196,6 +194,7 @@ class Blueprint(BaseSanic):
self._apps.add(app) self._apps.add(app)
url_prefix = options.get("url_prefix", self.url_prefix) url_prefix = options.get("url_prefix", self.url_prefix)
opt_version = options.get("version", None) opt_version = options.get("version", None)
opt_strict_slashes = options.get("strict_slashes", None)
opt_version_prefix = options.get("version_prefix", self.version_prefix) opt_version_prefix = options.get("version_prefix", self.version_prefix)
routes = [] routes = []
@ -220,18 +219,13 @@ class Blueprint(BaseSanic):
version_prefix = prefix version_prefix = prefix
break break
version = self.version version = self._extract_value(
for v in (future.version, opt_version, self.version): 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
) )
strict_slashes = self._extract_value(
future.strict_slashes, opt_strict_slashes, self.strict_slashes
)
name = app._generate_name(future.name) name = app._generate_name(future.name)
apply_route = FutureRoute( apply_route = FutureRoute(
@ -315,3 +309,12 @@ class Blueprint(BaseSanic):
return_when=asyncio.FIRST_COMPLETED, return_when=asyncio.FIRST_COMPLETED,
timeout=timeout, 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_1 = Blueprint.group(
Blueprint.group(blueprint_1, blueprint_2) Blueprint.group(blueprint_1, blueprint_2)
) )
assert len(blueprint_group_1) == 2 assert len(blueprint_group_1) == 1
def test_blueprint_group_insert(): def test_blueprint_group_insert():
@ -215,9 +215,29 @@ def test_blueprint_group_insert():
group.insert(0, blueprint_1) group.insert(0, blueprint_1)
group.insert(0, blueprint_2) group.insert(0, blueprint_2)
group.insert(0, blueprint_3) group.insert(0, blueprint_3)
assert group.blueprints[1].strict_slashes is False
assert group.blueprints[2].strict_slashes is True @blueprint_1.route("/")
assert group.blueprints[0].url_prefix == "/test" 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(): def test_bp_group_properties():
@ -231,19 +251,25 @@ def test_bp_group_properties():
url_prefix="/grouped", url_prefix="/grouped",
strict_slashes=True, strict_slashes=True,
) )
primary = Blueprint.group(group, url_prefix="/primary")
assert group.version_prefix == "/api/v" @blueprint_1.route("/")
assert blueprint_1.version_prefix == "/api/v" def blueprint_1_default_route(request):
assert blueprint_2.version_prefix == "/api/v" return text("BP1_OK")
assert group.version == 1 @blueprint_2.route("/")
assert blueprint_1.version == 1 def blueprint_2_default_route(request):
assert blueprint_2.version == 1 return text("BP2_OK")
assert group.strict_slashes app = Sanic("PropTest")
assert blueprint_1.strict_slashes app.blueprint(group)
assert blueprint_2.strict_slashes app.blueprint(primary)
app.router.finalize()
assert group.url_prefix == "/grouped" routes = [route.path for route in app.router.routes]
assert blueprint_1.url_prefix == "/grouped/bp1"
assert blueprint_2.url_prefix == "/grouped/bp2" 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