Repository: marshmallow-code/flask-smorest

Issue:
Documenting path parameters in blueprint url_prefix
How do I document path parameters that appear in `url_prefix` in a Blueprint? Example:

```
blueprint = Blueprint(
    'Media',
    __name__,
    url_prefix='/brands/<int:brand_id>/media',
)

@blueprint.route(...)
...
```

I've read through https://github.com/marshmallow-code/flask-smorest/issues/23 but it only seems to apply to `@blueprint.route()`, not `Blueprint()`.


Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/marshmallow-code/flask-smorest/pull/313?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=marshmallow-code) Report
> Merging [#313](https://codecov.io/gh/marshmallow-code/flask-smorest/pull/313?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=marshmallow-code) (2a1263f) into [master](https://codecov.io/gh/marshmallow-code/flask-smorest/commit/65bb764a0dc6bedff47170a8b62e54efacb3f303?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=marshmallow-code) (65bb764) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/marshmallow-code/flask-smorest/pull/313/graphs/tree.svg?width=650&height=150&src=pr&token=F676tOSaLF&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=marshmallow-code)](https://codecov.io/gh/marshmallow-code/flask-smorest/pull/313?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=marshmallow-code)

```diff
@@           Coverage Diff           @@
##           master     #313   +/-   ##
=======================================
  Coverage   99.87%   99.87%           
=======================================
  Files          14       14           
  Lines         791      793    +2     
  Branches      169      169           
=======================================
+ Hits          790      792    +2     
  Partials        1        1           
```


| [Impacted Files](https://codecov.io/gh/marshmallow-code/flask-smorest/pull/313?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=marshmallow-code) | Coverage Δ | |
|---|---|---|
| [flask\_smorest/\_\_init\_\_.py](https://codecov.io/gh/marshmallow-code/flask-smorest/pull/313/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=marshmallow-code#diff-Zmxhc2tfc21vcmVzdC9fX2luaXRfXy5weQ==) | `100.00% <100.00%> (ø)` | |
| [flask\_smorest/blueprint.py](https://codecov.io/gh/marshmallow-code/flask-smorest/pull/313/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=marshmallow-code#diff-Zmxhc2tfc21vcmVzdC9ibHVlcHJpbnQucHk=) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/marshmallow-code/flask-smorest/pull/313?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=marshmallow-code).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=marshmallow-code)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/marshmallow-code/flask-smorest/pull/313?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=marshmallow-code). Last update [65bb764...2a1263f](https://codecov.io/gh/marshmallow-code/flask-smorest/pull/313?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=marshmallow-code). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=marshmallow-code).


Patch:
diff --git a/docs/openapi.rst b/docs/openapi.rst
index 794b767..89d5ea1 100644
--- a/docs/openapi.rst
+++ b/docs/openapi.rst
@@ -117,6 +117,10 @@ argument to pass documentation for parameters that are shared by all operations
 of a path. It can be used to pass extra documentation, such as examples, for
 path parameters.
 
+Likewise, path parameters in ``url_prefix`` are also automatically documented,
+and their automatic documentation can be completed by passing a ``parameters``
+argument to the :meth:`Api.register_blueprint` method.
+
 Pass Extra Documentation Information
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
diff --git a/flask_smorest/__init__.py b/flask_smorest/__init__.py
index f611d26..4b47693 100644
--- a/flask_smorest/__init__.py
+++ b/flask_smorest/__init__.py
@@ -71,22 +71,31 @@ class Api(APISpecMixin, ErrorHandlerMixin):
         # Register error handlers
         self._register_error_handlers()
 
-    def register_blueprint(self, blp, **options):
+    def register_blueprint(self, blp, *, parameters=None, **options):
         """Register a blueprint in the application
 
         Also registers documentation for the blueprint/resource
 
         :param Blueprint blp: Blueprint to register
-        :param options: Keyword arguments overriding Blueprint defaults
+        :param list parameters: List of parameter descriptions for the path parameters
+            in the ``url_prefix`` of the Blueprint. Only used to document the resource.
+        :param options: Keyword arguments overriding
+            :class:`Blueprint <flask.Blueprint>` defaults
 
         Must be called after app is initialized.
         """
-        blp_name = blp.name if "name" not in options else options["name"]
+        blp_name = options.get("name", blp.name)
 
         self._app.register_blueprint(blp, **options)
 
         # Register views in API documentation for this resource
-        blp.register_views_in_doc(self, self._app, self.spec, name=blp_name)
+        blp.register_views_in_doc(
+            self,
+            self._app,
+            self.spec,
+            name=blp_name,
+            parameters=parameters,
+        )
 
         # Add tag relative to this resource to the global tag list
         self.spec.tag({"name": blp_name, "description": blp.description})
diff --git a/flask_smorest/blueprint.py b/flask_smorest/blueprint.py
index c1f3f53..919f183 100644
--- a/flask_smorest/blueprint.py
+++ b/flask_smorest/blueprint.py
@@ -118,8 +118,8 @@ class Blueprint(
         :param str endpoint: Endpoint for the registered URL rule (defaults
             to function name).
         :param callable|MethodView view_func: View function or MethodView class
-        :param list parameters: List of parameters relevant to all operations
-            in this path, only used to document the resource.
+        :param list parameters: List of parameter descriptions relevant to all
+            operations in this path. Only used to document the resource.
         :param list tags: List of tags for the resource.
             If None, ``Blueprint`` name is used.
         :param options: Options to be forwarded to the underlying
@@ -132,7 +132,7 @@ class Blueprint(
             endpoint = view_func.__name__
 
         # Ensure endpoint name is unique
-        # - to avoid a name clash when registering a MehtodView
+        # - to avoid a name clash when registering a MethodView
         # - to use it as a key internally in endpoint -> doc mapping
         if endpoint in self._endpoints:
             endpoint = f"{endpoint}_{len(self._endpoints)}"
@@ -198,7 +198,7 @@ class Blueprint(
         # Store parameters doc info from route decorator
         endpoint_doc_info["parameters"] = parameters
 
-    def register_views_in_doc(self, api, app, spec, *, name):
+    def register_views_in_doc(self, api, app, spec, *, name, parameters):
         """Register views information in documentation
 
         If a schema in a parameter or a response appears in the spec
@@ -207,13 +207,16 @@ class Blueprint(
 
         "schema":{"$ref": "#/components/schemas/MySchema"}
         """
+        url_prefix_parameters = parameters or []
+
         # This method uses the documentation information associated with each
         # endpoint in self._docs to provide documentation for corresponding
         # route to the spec object.
         # Deepcopy to avoid mutating the source. Allows registering blueprint
         # multiple times (e.g. when creating multiple apps during tests).
         for endpoint, endpoint_doc_info in deepcopy(self._docs).items():
-            parameters = endpoint_doc_info.pop("parameters")
+            endpoint_route_parameters = endpoint_doc_info.pop("parameters") or []
+            endpoint_parameters = url_prefix_parameters + endpoint_route_parameters
             doc = {}
             # Use doc info stored by decorators to generate doc
             for method_l, operation_doc_info in endpoint_doc_info.items():
@@ -244,7 +247,7 @@ class Blueprint(
             # Thanks to self.route, there can only be one rule per endpoint
             full_endpoint = ".".join((name, endpoint))
             rule = next(app.url_map.iter_rules(full_endpoint))
-            spec.path(rule=rule, operations=doc, parameters=parameters)
+            spec.path(rule=rule, operations=doc, parameters=endpoint_parameters)
 
     @staticmethod
     def doc(**kwargs):


Test patch:
diff --git a/tests/test_blueprint.py b/tests/test_blueprint.py
index b6335e5..feb65fe 100644
--- a/tests/test_blueprint.py
+++ b/tests/test_blueprint.py
@@ -482,6 +482,66 @@ class TestBlueprint:
         assert "parameters" not in paths["/test/"]
         assert paths["/test/{user_id}"]["parameters"][0]["name"] == "user_id"
 
+    @pytest.mark.parametrize("as_method_view", (True, False))
+    def test_blueprint_url_prefix_path_parameter(self, app, as_method_view):
+        """Test registering a blueprint with path parameter in url_prefix
+
+        Checks path parameters in url_prefix are correctly documented, even
+        if registering the same Blueprint multiple times with a different url_prefix.
+        """
+        api = Api(app)
+        blp = Blueprint("test", __name__, url_prefix="/<int:user_id>")
+
+        if as_method_view:
+
+            @blp.route("/")
+            class Resource(MethodView):
+                def get(self, user_id):
+                    pass
+
+        else:
+
+            @blp.route("/")
+            def func(user_id):
+                pass
+
+        api.register_blueprint(blp)
+        api.register_blueprint(blp, url_prefix="/<int:team_id>", name="team")
+
+        paths = api.spec.to_dict()["paths"]
+
+        assert paths["/{user_id}/"]["parameters"][0]["name"] == "user_id"
+        assert paths["/{team_id}/"]["parameters"][0]["name"] == "team_id"
+
+    @pytest.mark.parametrize("openapi_version", ("2.0", "3.0.2"))
+    def test_blueprint_url_prefix_register_blueprint_parameters(
+        self, app, openapi_version
+    ):
+        """Check url_prefix path parameters docs are merged with auto docs"""
+        app.config["OPENAPI_VERSION"] = openapi_version
+        api = Api(app)
+        blp = Blueprint("test", __name__, url_prefix="/<int:item_id>/")
+
+        parameters = [
+            "TestParameter",
+            {"name": "item_id", "in": "path", "description": "Item ID"},
+        ]
+
+        @blp.route("/")
+        def get(item_id):
+            pass
+
+        api.register_blueprint(blp, parameters=parameters)
+        spec = api.spec.to_dict()
+        params = spec["paths"]["/{item_id}/"]["parameters"]
+        assert len(params) == 2
+        assert params[0] == build_ref(api.spec, "parameter", "TestParameter")
+        assert params[1]["description"] == "Item ID"
+        if openapi_version == "2.0":
+            assert params[1]["type"] == "integer"
+        else:
+            assert params[1]["schema"]["type"] == "integer"
+
     @pytest.mark.parametrize("openapi_version", ("2.0", "3.0.2"))
     def test_blueprint_route_multiple_methods(self, app, schemas, openapi_version):
         """Test calling route with multiple methods