Repository: sanic-org/sanic
Pull request #2727 (2023-03-28 17:23:24)

Add name prefixing to BP groups
Because of the removal of duplicate route naming, the following is no longer possible. This was a hidden use case that we did not have unit tests to cover, and so the change was not caught.

```python
bp1 = Blueprint("bp1", url_prefix="/bp1")
bp2 = Blueprint("bp2", url_prefix="/bp2")

bp1.add_route(..., "/")
bp2.add_route(..., "/")

group_a = Blueprint.group(bp1, bp2, url_prefix="/group-a")
group_b = Blueprint.group(bp1, bp2, url_prefix="/group-b")

app.blueprint(group_a)
app.blueprint(group_b)
```

The solution is to add `name_prefix` to go along with `version_prefix` and `url_prefix` to the BlueprintGroup. This will allow proper constructing and nesting of blueprints and groups.

---

TODO:
- [x] Tests

codecov[bot]: ## [Codecov](https://codecov.io/gh/sanic-org/sanic/pull/2727?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org) Report
Patch coverage: **`66.666`**% and project coverage change: **`-0.040`** :warning:
> Comparison is base [(`6eaab2a`)](https://codecov.io/gh/sanic-org/sanic/commit/6eaab2a7e5be418385856371fdaebe4701f8c4fc?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org) 88.938% compared to head [(`a5cb3fb`)](https://codecov.io/gh/sanic-org/sanic/pull/2727?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org) 88.898%.

<details><summary>Additional details and impacted files</summary>


```diff
@@              Coverage Diff              @@
##              main     #2727       +/-   ##
=============================================
- Coverage   88.938%   88.898%   -0.040%     
=============================================
  Files           92        92               
  Lines         6952      6963       +11     
  Branches      1185      1187        +2     
=============================================
+ Hits          6183      6190        +7     
- Misses         527       529        +2     
- Partials       242       244        +2     
```


| [Impacted Files](https://codecov.io/gh/sanic-org/sanic/pull/2727?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org) | Coverage Δ | |
|---|---|---|
| [sanic/app.py](https://codecov.io/gh/sanic-org/sanic/pull/2727?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org#diff-c2FuaWMvYXBwLnB5) | `89.813% <33.333%> (-0.290%)` | :arrow_down: |
| [sanic/blueprints.py](https://codecov.io/gh/sanic-org/sanic/pull/2727?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org#diff-c2FuaWMvYmx1ZXByaW50cy5weQ==) | `90.610% <60.000%> (-0.778%)` | :arrow_down: |
| [sanic/blueprint\_group.py](https://codecov.io/gh/sanic-org/sanic/pull/2727?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org#diff-c2FuaWMvYmx1ZXByaW50X2dyb3VwLnB5) | `100.000% <100.000%> (ø)` | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org)

</details>

[:umbrella: View full report in Codecov by Sentry](https://codecov.io/gh/sanic-org/sanic/pull/2727?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org).


Patch:
diff --git a/sanic/app.py b/sanic/app.py
index 8eceec9a..05ed7d54 100644
--- a/sanic/app.py
+++ b/sanic/app.py
@@ -550,6 +550,9 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta):
                             )
                         else:
                             params["version_prefix"] = blueprint.version_prefix
+                    name_prefix = getattr(blueprint, "name_prefix", None)
+                    if name_prefix and "name_prefix" not in params:
+                        params["name_prefix"] = name_prefix
                 self.blueprint(item, **params)
             return
         if blueprint.name in self.blueprints:
diff --git a/sanic/blueprint_group.py b/sanic/blueprint_group.py
index a9b51410..8b706c59 100644
--- a/sanic/blueprint_group.py
+++ b/sanic/blueprint_group.py
@@ -65,6 +65,7 @@ class BlueprintGroup(MutableSequence):
         "_version",
         "_strict_slashes",
         "_version_prefix",
+        "_name_prefix",
     )
 
     def __init__(
@@ -73,6 +74,7 @@ class BlueprintGroup(MutableSequence):
         version: Optional[Union[int, str, float]] = None,
         strict_slashes: Optional[bool] = None,
         version_prefix: str = "/v",
+        name_prefix: Optional[str] = "",
     ):
         """
         Create a new Blueprint Group
@@ -87,6 +89,7 @@ class BlueprintGroup(MutableSequence):
         self._version = version
         self._version_prefix = version_prefix
         self._strict_slashes = strict_slashes
+        self._name_prefix = name_prefix
 
     @property
     def url_prefix(self) -> Optional[Union[int, str, float]]:
@@ -134,6 +137,15 @@ class BlueprintGroup(MutableSequence):
         """
         return self._version_prefix
 
+    @property
+    def name_prefix(self) -> Optional[str]:
+        """
+        Name prefix for the blueprint group
+
+        :return: str
+        """
+        return self._name_prefix
+
     def __iter__(self):
         """
         Tun the class Blueprint Group into an Iterable item
diff --git a/sanic/blueprints.py b/sanic/blueprints.py
index 508e25ac..ef9c6756 100644
--- a/sanic/blueprints.py
+++ b/sanic/blueprints.py
@@ -250,6 +250,7 @@ class Blueprint(BaseSanic):
         version: Optional[Union[int, str, float]] = None,
         strict_slashes: Optional[bool] = None,
         version_prefix: str = "/v",
+        name_prefix: Optional[str] = "",
     ) -> BlueprintGroup:
         """
         Create a list of blueprints, optionally grouping them under a
@@ -275,6 +276,7 @@ class Blueprint(BaseSanic):
             version=version,
             strict_slashes=strict_slashes,
             version_prefix=version_prefix,
+            name_prefix=name_prefix,
         )
         for bp in chain(blueprints):
             bps.append(bp)
@@ -295,6 +297,7 @@ class Blueprint(BaseSanic):
         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_name_prefix = options.get("name_prefix", None)
         error_format = options.get(
             "error_format", app.config.FALLBACK_ERROR_FORMAT
         )
@@ -326,7 +329,10 @@ class Blueprint(BaseSanic):
                 future.strict_slashes, opt_strict_slashes, self.strict_slashes
             )
 
-            name = app._generate_name(future.name)
+            name = future.name
+            if opt_name_prefix:
+                name = f"{opt_name_prefix}_{future.name}"
+            name = app._generate_name(name)
             host = future.host or self.host
             if isinstance(host, list):
                 host = tuple(host)


Test patch:
diff --git a/tests/test_blueprint_group.py b/tests/test_blueprint_group.py
index 5e793cc1..330d5d9a 100644
--- a/tests/test_blueprint_group.py
+++ b/tests/test_blueprint_group.py
@@ -1,3 +1,5 @@
+import pytest
+
 from pytest import raises
 
 from sanic.app import Sanic
@@ -340,3 +342,40 @@ def test_nested_bp_group_properties():
 
     routes = [route.path for route in app.router.routes]
     assert routes == ["three/one/four"]
+
+
+@pytest.mark.asyncio
+async def test_multiple_nested_bp_group():
+    bp1 = Blueprint("bp1", url_prefix="/bp1")
+    bp2 = Blueprint("bp2", url_prefix="/bp2")
+
+    bp1.add_route(lambda _: ..., "/", name="route1")
+    bp2.add_route(lambda _: ..., "/", name="route2")
+
+    group_a = Blueprint.group(
+        bp1, bp2, url_prefix="/group-a", name_prefix="group-a"
+    )
+    group_b = Blueprint.group(
+        bp1, bp2, url_prefix="/group-b", name_prefix="group-b"
+    )
+
+    app = Sanic("PropTest")
+    app.blueprint(group_a)
+    app.blueprint(group_b)
+
+    await app._startup()
+
+    routes = [route.path for route in app.router.routes]
+    assert routes == [
+        "group-a/bp1",
+        "group-a/bp2",
+        "group-b/bp1",
+        "group-b/bp2",
+    ]
+    names = [route.name for route in app.router.routes]
+    assert names == [
+        "PropTest.group-a_bp1.route1",
+        "PropTest.group-a_bp2.route2",
+        "PropTest.group-b_bp1.route1",
+        "PropTest.group-b_bp2.route2",
+    ]