From bde7a99cf300362a488537b996347e00eda4b8f9 Mon Sep 17 00:00:00 2001 From: Martin Zihlmann Date: Thu, 22 May 2025 22:14:17 +0100 Subject: [PATCH 1/7] allow passing in rule explicitly --- src/apispec_webframeworks/flask.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/apispec_webframeworks/flask.py b/src/apispec_webframeworks/flask.py index 2365087..e5b5fef 100644 --- a/src/apispec_webframeworks/flask.py +++ b/src/apispec_webframeworks/flask.py @@ -120,6 +120,15 @@ def _rule_for_view( rule = app.url_map._rules_by_endpoint[endpoint][0] return rule + @staticmethod + def _view_for_rule( + rule: Rule, + app: Optional[Flask] = None, + ) -> Union[Callable[..., Any], "RouteCallable"]: + if app is None: + app = current_app + return app.view_functions[rule.endpoint] + def path_helper( self, path: Optional[str] = None, @@ -127,14 +136,18 @@ def path_helper( parameters: Optional[List[dict]] = None, *, view: Optional[Union[Callable[..., Any], "RouteCallable"]] = None, + rule: Optional[Rule] = None, app: Optional[Flask] = None, **kwargs: Any, ) -> Optional[str]: """Path helper that allows passing a Flask view function.""" - assert view is not None + assert view is not None or rule is not None assert operations is not None - rule = self._rule_for_view(view, app=app) + if rule is None: + rule = self._rule_for_view(view, app=app) + if view is None: + view = self._view_for_rule(rule, app=app) view_doc = view.__doc__ or "" doc_operations = yaml_utils.load_operations_from_docstring(view_doc) operations.update(doc_operations) From 5030bc1ae63992737e9a683d264161cfd1ea4828 Mon Sep 17 00:00:00 2001 From: Martin Zihlmann Date: Thu, 22 May 2025 22:14:47 +0100 Subject: [PATCH 2/7] filter out methods --- src/apispec_webframeworks/flask.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/apispec_webframeworks/flask.py b/src/apispec_webframeworks/flask.py index e5b5fef..9e6ce7f 100644 --- a/src/apispec_webframeworks/flask.py +++ b/src/apispec_webframeworks/flask.py @@ -150,6 +150,7 @@ def path_helper( view = self._view_for_rule(rule, app=app) view_doc = view.__doc__ or "" doc_operations = yaml_utils.load_operations_from_docstring(view_doc) + doc_operations = {k: v for k, v in doc_operations.items() if k.upper() in rule.methods} operations.update(doc_operations) if hasattr(view, "view_class") and issubclass(view.view_class, MethodView): # noqa: E501 # method attribute is dynamically added, which is supported by mypy From 510bccfc9fcbebcc2aebb682682edbe6078e3dc6 Mon Sep 17 00:00:00 2001 From: Martin Zihlmann Date: Thu, 22 May 2025 22:15:06 +0100 Subject: [PATCH 3/7] flask by default does not enable POST --- tests/test_ext_flask.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_ext_flask.py b/tests/test_ext_flask.py index c3356d4..9c56f3b 100644 --- a/tests/test_ext_flask.py +++ b/tests/test_ext_flask.py @@ -124,7 +124,7 @@ def delete(self): assert "delete" not in paths["/hi"] def test_integration_with_docstring_introspection(self, app, spec): - @app.route("/hello") + @app.route("/hello", methods=["GET", "POST"]) def hello(): """A greeting endpoint. From 085b374c058e01d69c221ca2353e5f4e866f1e6e Mon Sep 17 00:00:00 2001 From: Martin Zihlmann Date: Thu, 22 May 2025 22:15:18 +0100 Subject: [PATCH 4/7] allow custom extensions --- src/apispec_webframeworks/flask.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apispec_webframeworks/flask.py b/src/apispec_webframeworks/flask.py index 9e6ce7f..a0d62b7 100644 --- a/src/apispec_webframeworks/flask.py +++ b/src/apispec_webframeworks/flask.py @@ -150,7 +150,7 @@ def path_helper( view = self._view_for_rule(rule, app=app) view_doc = view.__doc__ or "" doc_operations = yaml_utils.load_operations_from_docstring(view_doc) - doc_operations = {k: v for k, v in doc_operations.items() if k.upper() in rule.methods} + doc_operations = {k: v for k, v in doc_operations.items() if k.upper() in rule.methods or k.startswith("x-")} operations.update(doc_operations) if hasattr(view, "view_class") and issubclass(view.view_class, MethodView): # noqa: E501 # method attribute is dynamically added, which is supported by mypy From c53b006ff3cbb991ba1d704a61e3cc4c6aefd65c Mon Sep 17 00:00:00 2001 From: Martin Zihlmann Date: Thu, 22 May 2025 22:23:53 +0100 Subject: [PATCH 5/7] add instructive test for this case --- tests/test_ext_flask.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/test_ext_flask.py b/tests/test_ext_flask.py index 9c56f3b..2e38d4b 100644 --- a/tests/test_ext_flask.py +++ b/tests/test_ext_flask.py @@ -180,3 +180,32 @@ def get_pet(pet_id): spec.path(view=get_pet, app=app) assert "/pet/{pet_id}" in get_paths(spec) + + def test_multiple_paths(self, app, spec): + @app.put("/user") + @app.route("/user/") + def user(user = None): + """A greeting endpoint. + + --- + get: + description: get user details + responses: + 200: + description: a user to be returned + put: + description: create a user + responses: + 200: + description: some data + """ + pass + + for rule in app.url_map.iter_rules(): + spec.path(rule=rule) + + paths = get_paths(spec) + get_op = paths["/user/{user}"]["get"] + put_op = paths["/user"]["put"] + assert get_op["description"] == "get user details" + assert put_op["description"] == "create a user" From 35b7c442cdc5bd371b926b2b1a1d255281ecf269 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 22 May 2025 21:37:14 +0000 Subject: [PATCH 6/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/apispec_webframeworks/flask.py | 6 +++++- tests/test_ext_flask.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/apispec_webframeworks/flask.py b/src/apispec_webframeworks/flask.py index a0d62b7..0da6732 100644 --- a/src/apispec_webframeworks/flask.py +++ b/src/apispec_webframeworks/flask.py @@ -150,7 +150,11 @@ def path_helper( view = self._view_for_rule(rule, app=app) view_doc = view.__doc__ or "" doc_operations = yaml_utils.load_operations_from_docstring(view_doc) - doc_operations = {k: v for k, v in doc_operations.items() if k.upper() in rule.methods or k.startswith("x-")} + doc_operations = { + k: v + for k, v in doc_operations.items() + if k.upper() in rule.methods or k.startswith("x-") + } operations.update(doc_operations) if hasattr(view, "view_class") and issubclass(view.view_class, MethodView): # noqa: E501 # method attribute is dynamically added, which is supported by mypy diff --git a/tests/test_ext_flask.py b/tests/test_ext_flask.py index 2e38d4b..9ed260d 100644 --- a/tests/test_ext_flask.py +++ b/tests/test_ext_flask.py @@ -184,7 +184,7 @@ def get_pet(pet_id): def test_multiple_paths(self, app, spec): @app.put("/user") @app.route("/user/") - def user(user = None): + def user(user=None): """A greeting endpoint. --- From 0a67e08314145875655aedb536385b527a4bb159 Mon Sep 17 00:00:00 2001 From: Martin Zihlmann Date: Thu, 22 May 2025 22:42:23 +0100 Subject: [PATCH 7/7] mypy --- src/apispec_webframeworks/flask.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/apispec_webframeworks/flask.py b/src/apispec_webframeworks/flask.py index 0da6732..7a5767d 100644 --- a/src/apispec_webframeworks/flask.py +++ b/src/apispec_webframeworks/flask.py @@ -141,10 +141,10 @@ def path_helper( **kwargs: Any, ) -> Optional[str]: """Path helper that allows passing a Flask view function.""" - assert view is not None or rule is not None assert operations is not None if rule is None: + assert view is not None rule = self._rule_for_view(view, app=app) if view is None: view = self._view_for_rule(rule, app=app) @@ -153,7 +153,7 @@ def path_helper( doc_operations = { k: v for k, v in doc_operations.items() - if k.upper() in rule.methods or k.startswith("x-") + if rule.methods is None or k.upper() in rule.methods or k.startswith("x-") } operations.update(doc_operations) if hasattr(view, "view_class") and issubclass(view.view_class, MethodView): # noqa: E501