Skip to content

Commit 1b6a213

Browse files
authored
Fixes issue where register_api isn't idempotent (#230)
* Fixes issue where register_api isn't idempotent In a situation where we may instantiate multiple apps that register the same blueprint, this change updates register_api to always update the APIBlueprint's instance variables to the same values no matter how many times it is invoked. * Implement for APIView
1 parent f5afbbc commit 1b6a213

File tree

4 files changed

+64
-1
lines changed

4 files changed

+64
-1
lines changed

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Before submitting pr, you need to complete the following steps:
1010
1. Install requirements
1111

1212
```bash
13-
pip install -U flask pydantic
13+
pip install -U flask pydantic pyyaml pytest ruff mypy
1414
```
1515

1616
2. Running the tests

flask_openapi3/openapi.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,8 +306,10 @@ def register_api(self, api: APIBlueprint, **options: Any) -> None:
306306
url_prefix = options.get("url_prefix")
307307
if url_prefix and api.url_prefix and url_prefix != api.url_prefix:
308308
api.paths = {url_prefix + k.removeprefix(api.url_prefix): v for k, v in api.paths.items()}
309+
api.url_prefix = url_prefix
309310
elif url_prefix and not api.url_prefix:
310311
api.paths = {url_prefix.rstrip("/") + "/" + k.lstrip("/"): v for k, v in api.paths.items()}
312+
api.url_prefix = url_prefix
311313
self.paths.update(**api.paths)
312314

313315
# Update component schemas with the APIBlueprint's component schemas
@@ -345,8 +347,10 @@ def register_api_view(
345347
# Update paths with the APIView's paths
346348
if url_prefix and api_view.url_prefix and url_prefix != api_view.url_prefix:
347349
api_view.paths = {url_prefix + k.removeprefix(api_view.url_prefix): v for k, v in api_view.paths.items()}
350+
api_view.url_prefix = url_prefix
348351
elif url_prefix and not api_view.url_prefix:
349352
api_view.paths = {url_prefix.rstrip("/") + "/" + k.lstrip("/"): v for k, v in api_view.paths.items()}
353+
api_view.url_prefix = url_prefix
350354
self.paths.update(**api_view.paths)
351355

352356
# Update component schemas with the APIView's component schemas

tests/test_api_blueprint.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,38 @@ def test_patch(client):
135135
def test_delete(client):
136136
resp = client.delete("/api/book/1")
137137
assert resp.status_code == 200
138+
139+
140+
# Create a second blueprint here to test when `url_prefix` is None
141+
author_api = APIBlueprint(
142+
'/author',
143+
__name__,
144+
abp_tags=[tag],
145+
abp_security=security,
146+
abp_responses={"401": Unauthorized},
147+
)
148+
149+
150+
class AuthorBody(BaseModel):
151+
age: Optional[int] = Field(..., ge=1, le=100, description='Age')
152+
153+
154+
@author_api.post('/<int:aid>')
155+
def get_author(body: AuthorBody):
156+
pass
157+
158+
159+
def create_app():
160+
app = OpenAPI(__name__, info=info, security_schemes=security_schemes)
161+
app.register_api(api, url_prefix='/1.0')
162+
app.register_api(author_api, url_prefix='/1.0/author')
163+
164+
165+
# Invoke twice to ensure that call is idempotent
166+
create_app()
167+
create_app()
168+
169+
170+
def test_blueprint_path_and_prefix():
171+
assert list(api.paths.keys()) == ['/1.0/book/{bid}', '/1.0/v2/book/{bid}']
172+
assert list(author_api.paths.keys()) == ['/1.0/author/{aid}']

tests/test_api_view.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
api_view = APIView(url_prefix="/api/v1/<name>", view_tags=[Tag(name="book")], view_security=security)
2626
api_view2 = APIView(doc_ui=False)
27+
api_view_no_url = APIView(view_tags=[Tag(name="book")], view_security=security)
2728

2829

2930
class BookPath(BaseModel):
@@ -86,6 +87,13 @@ def get(self, path: BookPath):
8687
return path.model_dump()
8788

8889

90+
@api_view_no_url.route("/book3")
91+
class BookAPIViewNoUrl:
92+
@api_view_no_url.doc(summary="get book3")
93+
def get(self, path: BookPath):
94+
return path.model_dump()
95+
96+
8997
app.register_api_view(api_view)
9098
app.register_api_view(api_view2)
9199

@@ -132,3 +140,19 @@ def test_get(client):
132140
def test_delete(client):
133141
resp = client.delete("/api/v1/name1/book/1")
134142
assert resp.status_code == 200
143+
144+
145+
def create_app():
146+
app = OpenAPI(__name__, info=info, security_schemes=security_schemes)
147+
app.register_api_view(api_view, url_prefix='/api/1.0')
148+
app.register_api_view(api_view_no_url, url_prefix='/api/1.0')
149+
150+
151+
# Invoke twice to ensure that call is idempotent
152+
create_app()
153+
create_app()
154+
155+
156+
def test_register_api_view_idempotency():
157+
assert list(api_view.paths.keys()) == ['/api/1.0/api/v1/{name}/book', '/api/1.0/api/v1/{name}/book/{id}']
158+
assert list(api_view_no_url.paths.keys()) == ['/api/1.0/book3']

0 commit comments

Comments
 (0)