From db11deb2bb915671f2e1457c68ebc637920fd453 Mon Sep 17 00:00:00 2001 From: Stefan Tjarks <66305+stj@users.noreply.github.com> Date: Sun, 24 Oct 2021 12:01:38 -0700 Subject: [PATCH] Support excluding handlers from document generation Sometimes not all route and/or method handlers need to be part of API documentation. This adds a simple way to mark handlers as excluded from the documenation generation. --- CHANGELOG | 6 ++++ aiohttp_swagger/__init__.py | 4 +-- aiohttp_swagger/helpers/builders.py | 6 ++++ aiohttp_swagger/helpers/decorators.py | 6 ++++ doc/source/customizing.rst | 24 ++++++++++++++ tests/test_openapi.py | 45 +++++++++++++++++++++++++++ tests/test_swagger.py | 43 +++++++++++++++++++++++++ 7 files changed, 132 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 8fcddfb..703819e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,10 @@ +Version dev +=========== + +- Added support to exclude handlers from generated swagger document. + Version 1.0.15 +============== - Fixes a bug in setup.py where the distribution exports the package tests along with the actual library. - Latest js-yaml fixes security issue related to https://www.npmjs.com/advisories/813 - Added logic that checks whether the route.method is * or contains a single method. When the latter is the case only the method that is specified will be added to the Swagger UI. diff --git a/aiohttp_swagger/__init__.py b/aiohttp_swagger/__init__.py index 72c786f..39acbc9 100644 --- a/aiohttp_swagger/__init__.py +++ b/aiohttp_swagger/__init__.py @@ -5,7 +5,7 @@ from aiohttp import web from .helpers import (generate_doc_from_each_end_point, - load_doc_from_yaml_file, swagger_path) + load_doc_from_yaml_file, swagger_ignore, swagger_path) try: import ujson as json @@ -108,4 +108,4 @@ def setup_swagger(app: web.Application, ) -__all__ = ("setup_swagger", "swagger_path") +__all__ = ("setup_swagger", "swagger_path", "swagger_ignore") diff --git a/aiohttp_swagger/helpers/builders.py b/aiohttp_swagger/helpers/builders.py index 6d32b2b..bfdd76c 100644 --- a/aiohttp_swagger/helpers/builders.py +++ b/aiohttp_swagger/helpers/builders.py @@ -44,6 +44,8 @@ def _build_doc_from_func_doc(route): if isclass(route.handler) and issubclass(route.handler, web.View): for method_name in _get_method_names_for_handler(route): method = getattr(route.handler, method_name) + if getattr(method, "swagger_ignore", False): + continue if method.__doc__ is not None and "---" in method.__doc__: end_point_doc = method.__doc__.splitlines() out.update(_extract_swagger_docs(end_point_doc, method=method_name)) @@ -132,6 +134,10 @@ def nesteddict2yaml(d, indent=10, result=""): end_point_doc = None + if getattr(route.handler, "swagger_ignore", False): + # Handlers can be ignored from documentation + continue + # If route has a external link to doc, we use it, not function doc if getattr(route.handler, "swagger_file", False): try: diff --git a/aiohttp_swagger/helpers/decorators.py b/aiohttp_swagger/helpers/decorators.py index 55032f9..b9a97a2 100644 --- a/aiohttp_swagger/helpers/decorators.py +++ b/aiohttp_swagger/helpers/decorators.py @@ -5,3 +5,9 @@ def __init__(self, swagger_file): def __call__(self, f): f.swagger_file = self.swagger_file return f + + +def swagger_ignore(handler): + """Mark handler as ignored from swagger docs""" + handler.swagger_ignore = True + return handler diff --git a/doc/source/customizing.rst b/doc/source/customizing.rst index eeb4a82..287db61 100644 --- a/doc/source/customizing.rst +++ b/doc/source/customizing.rst @@ -299,3 +299,27 @@ Swagger validation api_base_url='/sub_app_prefix', swagger_validator_url='//online.swagger.io/validator' ) + +Ignore path ++++++++++++ + +:samp:`aiohttp-swagger` ignores handlers that have an attribute `swagger_ignore` with value `True`. A helper method +`swagger_ignore`, that can be used as decorator, is available. + + +.. code-block:: python + + from aiohttp import web + from aiohttp_swagger import * + + @swagger_ignore + async def ping(request): + return web.Response(text="pong") + + app = web.Application() + + app.router.add_route('GET', "/ping", ping) + + setup_swagger(app) + + web.run_app(app, host="127.0.0.1") diff --git a/tests/test_openapi.py b/tests/test_openapi.py index b21bd0b..2abf34c 100644 --- a/tests/test_openapi.py +++ b/tests/test_openapi.py @@ -1,4 +1,6 @@ import json + +import aiohttp_swagger import pytest import yaml from os.path import join, dirname, abspath @@ -93,6 +95,30 @@ async def ping_partial(request): return web.Response(text="pong") +@swagger_ignore +async def ignore(request): + return web.Response(text="OK") +ignore.__doc__ = ping.__doc__ + + +class IgnoreView(ClassView): + + @swagger_ignore + async def get(self): + return await super().get() + get.__doc__ = ClassView.get.__doc__ + + @swagger_ignore + async def patch(self): + return await super().patch() + patch.__doc__ = ClassView.patch.__doc__ + + @swagger_ignore + async def post(self): + return await super().post() + post.__doc__ = ClassView.post.__doc__ + + async def test_swagger_ui(aiohttp_client, loop): TESTS_PATH = abspath(join(dirname(__file__))) @@ -241,6 +267,25 @@ async def test_undocumented_fn(aiohttp_client, loop): assert not result['paths'] +async def test_ignored_fn(aiohttp_client, loop): + app = web.Application(loop=loop) + app.router.add_route('GET', "/ignore", ignore) + assert ignore.swagger_ignore is True + app.router.add_route('*', "/ignore_view", IgnoreView) + assert IgnoreView.get.swagger_ignore is True + + setup_swagger(app, ui_version=3) + client = await aiohttp_client(app) + for path in ("/ignore", "/ignore_view"): + resp = await client.get(path) + assert resp.status == 200 + + swagger_resp1 = await client.get('/api/doc/swagger.json') + assert swagger_resp1.status == 200 + result = await swagger_resp1.json() + assert not result['paths'] + + async def test_wrong_method(aiohttp_client, loop): app = web.Application(loop=loop) app.router.add_route('POST', "/post_ping", ping) diff --git a/tests/test_swagger.py b/tests/test_swagger.py index 3001fbc..e6020b8 100644 --- a/tests/test_swagger.py +++ b/tests/test_swagger.py @@ -93,6 +93,30 @@ async def ping_partial(request): return web.Response(text="pong") +@swagger_ignore +async def ignore(request): + return web.Response(text="OK") +ignore.__doc__ = ping.__doc__ + + +class IgnoreView(ClassView): + + @swagger_ignore + async def get(self): + return await super().get() + get.__doc__ = ClassView.get.__doc__ + + @swagger_ignore + async def patch(self): + return await super().patch() + patch.__doc__ = ClassView.patch.__doc__ + + @swagger_ignore + async def post(self): + return await super().post() + post.__doc__ = ClassView.post.__doc__ + + async def test_ping(aiohttp_client, loop): app = web.Application(loop=loop) app.router.add_route('GET', "/ping", ping) @@ -265,6 +289,25 @@ async def test_undocumented_fn(aiohttp_client, loop): assert not result['paths'] +async def test_ignored_fn(aiohttp_client, loop): + app = web.Application(loop=loop) + app.router.add_route('GET', "/ignore", ignore) + assert ignore.swagger_ignore is True + app.router.add_route('*', "/ignore_view", IgnoreView) + assert IgnoreView.get.swagger_ignore is True + + setup_swagger(app, ui_version=3) + client = await aiohttp_client(app) + for path in ("/ignore", "/ignore_view"): + resp = await client.get(path) + assert resp.status == 200 + + swagger_resp1 = await client.get('/api/doc/swagger.json') + assert swagger_resp1.status == 200 + result = await swagger_resp1.json() + assert not result['paths'] + + async def test_wrong_method(aiohttp_client, loop): app = web.Application(loop=loop) app.router.add_route('POST', "/post_ping", ping)