Skip to content

Commit d0690f7

Browse files
authored
Allow url_prefix to be set during API/APIView registration (#215)
* Allow url_prefix to be set during API/APIView registration * Add tests
1 parent 5c537ec commit d0690f7

File tree

4 files changed

+103
-6
lines changed

4 files changed

+103
-6
lines changed

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
strategy:
2424
max-parallel: 4
2525
matrix:
26-
python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12", "3.13" ]
26+
python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ]
2727
flask-version: [ "Flask>=2.0,<3.0", "Flask>=3.0" ]
2828
env:
2929
PYTHONPATH: .

flask_openapi3/openapi.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -280,13 +280,19 @@ def generate_spec_json(self):
280280
}
281281
}
282282

283-
def register_api(self, api: APIBlueprint) -> None:
283+
def register_api(self, api: APIBlueprint, **options: Any) -> None:
284284
"""
285285
Register an APIBlueprint.
286286
287287
Args:
288288
api: The APIBlueprint instance to register.
289289
290+
options: Additional keyword arguments are passed to :class:`~flask.blueprints.BlueprintSetupState`.
291+
They can be accessed in :meth:`~flask.Blueprint.record` callbacks.
292+
url_prefix, Blueprint routes will be prefixed with this.
293+
subdomain, Blueprint routes will match on this subdomain.
294+
url_defaults, Blueprint routes will use these default values for view arguments.
295+
290296
"""
291297
for tag in api.tags:
292298
if tag.name not in self.tag_names:
@@ -297,20 +303,31 @@ def register_api(self, api: APIBlueprint) -> None:
297303
self.tag_names.append(tag.name)
298304

299305
# Update paths with the APIBlueprint's paths
306+
url_prefix = options.get("url_prefix")
307+
if url_prefix and api.url_prefix and url_prefix != api.url_prefix:
308+
api.paths = {url_prefix + k.removeprefix(api.url_prefix): v for k, v in api.paths.items()}
309+
elif url_prefix and not api.url_prefix:
310+
api.paths = {url_prefix.rstrip("/") + "/" + k.lstrip("/"): v for k, v in api.paths.items()}
300311
self.paths.update(**api.paths)
301312

302313
# Update component schemas with the APIBlueprint's component schemas
303314
self.components_schemas.update(**api.components_schemas)
304315

305316
# Register the APIBlueprint with the current instance
306-
self.register_blueprint(api)
317+
self.register_blueprint(api, **options)
307318

308-
def register_api_view(self, api_view: APIView, view_kwargs: Optional[Dict[Any, Any]] = None) -> None:
319+
def register_api_view(
320+
self,
321+
api_view: APIView,
322+
url_prefix: Optional[str] = None,
323+
view_kwargs: Optional[Dict[Any, Any]] = None
324+
) -> None:
309325
"""
310326
Register APIView
311327
312328
Args:
313329
api_view: The APIView instance to register.
330+
url_prefix: A path to prepend to all the APIView's urls
314331
view_kwargs: Additional keyword arguments to pass to the API views.
315332
"""
316333
if view_kwargs is None:
@@ -326,13 +343,17 @@ def register_api_view(self, api_view: APIView, view_kwargs: Optional[Dict[Any, A
326343
self.tag_names.append(tag.name)
327344

328345
# Update paths with the APIView's paths
346+
if url_prefix and api_view.url_prefix and url_prefix != api_view.url_prefix:
347+
api_view.paths = {url_prefix + k.removeprefix(api_view.url_prefix): v for k, v in api_view.paths.items()}
348+
elif url_prefix and not api_view.url_prefix:
349+
api_view.paths = {url_prefix.rstrip("/") + "/" + k.lstrip("/"): v for k, v in api_view.paths.items()}
329350
self.paths.update(**api_view.paths)
330351

331352
# Update component schemas with the APIView's component schemas
332353
self.components_schemas.update(**api_view.components_schemas)
333354

334355
# Register the APIView with the current instance
335-
api_view.register(self, view_kwargs=view_kwargs)
356+
api_view.register(self, url_prefix=url_prefix, view_kwargs=view_kwargs)
336357

337358
def _add_url_rule(
338359
self,

flask_openapi3/view.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,12 +190,18 @@ def decorator(func):
190190

191191
return decorator
192192

193-
def register(self, app: "OpenAPI", view_kwargs: Optional[Dict[Any, Any]] = None) -> None:
193+
def register(
194+
self,
195+
app: "OpenAPI",
196+
url_prefix: Optional[str] = None,
197+
view_kwargs: Optional[Dict[Any, Any]] = None
198+
) -> None:
194199
"""
195200
Register the API views with the given OpenAPI app.
196201
197202
Args:
198203
app: An instance of the OpenAPI app.
204+
url_prefix: A path to prepend to all the APIView's urls
199205
view_kwargs: Additional keyword arguments to pass to the API views.
200206
"""
201207
for rule, (cls, methods) in self.views.items():
@@ -214,6 +220,12 @@ def register(self, app: "OpenAPI", view_kwargs: Optional[Dict[Any, Any]] = None)
214220
view_class=cls,
215221
view_kwargs=view_kwargs
216222
)
223+
224+
if url_prefix and self.url_prefix and url_prefix != self.url_prefix:
225+
rule = url_prefix + rule.removeprefix(self.url_prefix)
226+
elif url_prefix and not self.url_prefix:
227+
rule = url_prefix.rstrip("/") + "/" + rule.lstrip("/")
228+
217229
options = {
218230
"endpoint": cls.__name__ + "." + method.lower(),
219231
"methods": [method.upper()]

tests/test_url_prefix.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# -*- coding: utf-8 -*-
2+
# @Author : llc
3+
# @Time : 2025/1/15 9:58
4+
import pytest
5+
6+
from flask_openapi3 import APIBlueprint, OpenAPI, APIView
7+
8+
app = OpenAPI(__name__, )
9+
app.config["TESTING"] = True
10+
11+
12+
@pytest.fixture
13+
def client():
14+
client = app.test_client()
15+
16+
return client
17+
18+
19+
api1 = APIBlueprint("/book1", __name__, url_prefix="/api")
20+
api2 = APIBlueprint("/book2", __name__)
21+
22+
23+
@api1.get("/book")
24+
def create_book1():
25+
return "ok"
26+
27+
28+
@api2.get("/book")
29+
def create_book2():
30+
return "ok"
31+
32+
33+
app.register_api(api1, url_prefix="/api1")
34+
app.register_api(api2, url_prefix="/api2")
35+
36+
api_view1 = APIView(url_prefix="/api")
37+
api_view2 = APIView()
38+
39+
40+
@api_view1.route("/book")
41+
class BookAPIView:
42+
@api_view1.doc(summary="get book")
43+
def get(self):
44+
return "ok"
45+
46+
47+
@api_view2.route("/book")
48+
class BookAPIView2:
49+
@api_view2.doc(summary="get book")
50+
def get(self, ):
51+
return "ok"
52+
53+
54+
app.register_api_view(api_view1, url_prefix="/api3")
55+
app.register_api_view(api_view2, url_prefix="/api4")
56+
57+
58+
def test_openapi(client):
59+
resp = client.get("/openapi/openapi.json")
60+
_json = resp.json
61+
assert "/api1/book" in _json["paths"].keys()
62+
assert "/api2/book" in _json["paths"].keys()
63+
assert "/api3/book" in _json["paths"].keys()
64+
assert "/api4/book" in _json["paths"].keys()

0 commit comments

Comments
 (0)