diff --git a/sanic/app.py b/sanic/app.py index 002c92dc..ec9027b5 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -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, ( diff --git a/sanic/blueprint_group.py b/sanic/blueprint_group.py index 7dd729a9..45f30894 100644 --- a/sanic/blueprint_group.py +++ b/sanic/blueprint_group.py @@ -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): """ diff --git a/sanic/blueprints.py b/sanic/blueprints.py index eba91c39..1fa24e88 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -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 diff --git a/tests/test_blueprint_group.py b/tests/test_blueprint_group.py index 893cc805..77ddf44c 100644 --- a/tests/test_blueprint_group.py +++ b/tests/test_blueprint_group.py @@ -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