Allow blueprints and groups to be infinitely reusable (#2150)
* Allow blueprints and groups to be infinitely reusable
This commit is contained in:
		
							
								
								
									
										28
									
								
								sanic/app.py
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								sanic/app.py
									
									
									
									
									
								
							@@ -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, (
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user