diff --git a/CHANGELOG.rst b/CHANGELOG.rst index fc74f94..7800faa 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,6 +14,15 @@ Change Log Unreleased ---------- +1.2.0 --- 2020-03-20 +-------------------- + +* Added three new decorators for excluding endpoints from API documentation generation: + + * ``@exclude_schema`` + * ``@exclude_schema_for(method_name)`` + * ``@exclude_all_schemas`` + 1.1.0 --- 2020-03-20 -------------------- diff --git a/docs/excluding.rst b/docs/excluding.rst new file mode 100644 index 0000000..a6d53de --- /dev/null +++ b/docs/excluding.rst @@ -0,0 +1,88 @@ +.. _excluding: + +Excluding API endpoints from documentation +========================================== + +Once installed, ``edx-api-doc-tools`` will automatically generate browsable +documentation for all API endpoints within the ``/api/`` path. +This may not be what you want. + +Analogous to the :func:`schema` and :func:`schema_for` decorators, +there exist the ``exclude_schema`` and :func:`exclude_schema_for` decorators, +both of which prevent the target endpoint from appearing in your API documentation. +The former is useful when your endpoint handler is defined directly in your source file, +whereas the latter is useful when the handler is implemented by a base class. + +Furthermore, :func:`exclude_schema_for` can be used on a View or Viewset to +exclude multiple endpoints at once. +If you wish to exclude *all* endpoints for View or Viewset, decorate it with +``exclude_schema_for_all``. + +For example:: + + ... + from edx_api_doc_tools import exclude_schema, exclude_schema_for, exclude_schema_for_all + ... + class MyViewsetWithSomeDocs(ViewSet): + def retrieve(...): + """ + This will appear in the docs. + """ + + @exclude_schema + def update(...): + """ + This will NOT appear in the docs. + """ + + + @exclude_schema_for_all + class MyViewsetWithNoDocs(ViewSet): + def retrieve(...): + """ + This will NOT appear in the docs. + """ + def update(...): + """ + This will NOT appear in the docs. + """ + + + # Note that ``ModelAPIView`` comes with pre-implemented handlers for + # GET, POST, PUT, PATCH, and DESTROY. + + + class MyModelViewWithAllDocs(ModelAPIView): + """ + Will have docs for GET, POST, PUT, PATCH, and DESTROY. + """ + + @exclude_schema_for('destroy') + class MyModelViewWithMostDocs(ModelAPIView): + """ + Will have docs for GET, POST, PUT, and PATCH. + """ + + @exclude_schema_for('put', 'patch', 'destroy') + class MyModelViewWithSomeDocs(ModelAPIView) + """ + Will have docs for GET and POST. + """ + + @exclude_schema_for_all + class MyViewModelViewNoDocs(ModelAPIView) + """ + ModelAPIView has handlers for GET, POST, PUT, PATCH, and DESTROY, + but we will not see any docs for this view. + """ + + @exclude_schema_for_all + class MyViewWithMostDocs(APIView) + def get(self, request): + """ + This won't appear in the docs. + """ + def post(self, request): + """ + Nor will this. + """ diff --git a/docs/index.rst b/docs/index.rst index 5678c13..bbd54fb 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -11,6 +11,7 @@ Contents: adding writing + excluding api development changelog diff --git a/edx_api_doc_tools/__init__.py b/edx_api_doc_tools/__init__.py index d854156..7b2733b 100644 --- a/edx_api_doc_tools/__init__.py +++ b/edx_api_doc_tools/__init__.py @@ -37,9 +37,16 @@ query_parameter, string_parameter, ) -from .view_utils import is_schema_request, schema, schema_for +from .view_utils import ( + exclude_schema, + exclude_schema_for, + exclude_schema_for_all, + is_schema_request, + schema, + schema_for, +) -__version__ = '1.1.0' +__version__ = '1.2.0' default_app_config = 'edx_api_doc_tools.apps.EdxApiDocToolsConfig' diff --git a/edx_api_doc_tools/view_utils.py b/edx_api_doc_tools/view_utils.py index 90a967d..64ca897 100644 --- a/edx_api_doc_tools/view_utils.py +++ b/edx_api_doc_tools/view_utils.py @@ -7,6 +7,7 @@ from django.utils.decorators import method_decorator from drf_yasg.utils import swagger_auto_schema +from rest_framework.viewsets import ViewSet from .internal_utils import dedent, split_docstring @@ -47,6 +48,61 @@ def schema_for_inner(view_class): return schema_for_inner +def exclude_schema_for(*method_names): + """ + Decorate a class to exlcude one or more of of its methods from the API docs. + + Arguments: + method_names (list[str]): Names of view methods whose operations will be + excluded from the generated API documentation. + + Example:: + + @schema_for('get', ...) + @schema_for('delete', ...) + @exclude_schema_for('put', 'patch') + class MyView(RetrieveUpdateDestroyAPIView): + pass + """ + def exclude_schema_for_inner(view_class): + """ + Decorate a view class to exclude specified methods. + """ + for method_name in method_names: + method_decorator( + name=method_name, decorator=exclude_schema + )(view_class) + return view_class + return exclude_schema_for_inner + + +def exclude_schema_for_all(view_class): + """ + Decorate a class to exlcude all of its methods from the API docs. + + Arguments: + view_class (type): A type, typically a subclass of View or ViewSet. + + Example:: + + @exclude_schema_for_all + class MyView(RetrieveUpdateDestroyAPIView): + pass + """ + all_viewset_api_methods = { + 'list', 'retrieve', 'create', 'update', 'partial_update', 'destroy' + } + all_view_api_methods = { + 'get', 'post', 'put', 'patch', 'delete' + } + is_viewset = issubclass(view_class, ViewSet) + all_api_methods = all_viewset_api_methods if is_viewset else all_view_api_methods + methods_to_exclude = { + method for method in all_api_methods if hasattr(view_class, method) + } + return exclude_schema_for(*methods_to_exclude)(view_class) + + def schema( parameters=None, responses=None, @@ -91,6 +147,25 @@ def schema_inner(view_func): return schema_inner +def exclude_schema(view_func): + """ + Decorate an API-endpoint-handling function to exclude it from the API docs. + + Example:: + + class MyView(APIView): + + @schema(...) + def get(...): + pass + + @exclude_schema + def post(...): + pass + """ + return swagger_auto_schema(auto_schema=None)(view_func) + + def is_schema_request(request): """ Return whether this request is serving an OpenAPI schema. diff --git a/example/urls.py b/example/urls.py index cf230cf..db61249 100644 --- a/example/urls.py +++ b/example/urls.py @@ -8,17 +8,23 @@ from edx_api_doc_tools import make_api_info, make_docs_urls -from .views import HedgehogInfoView, HedgehogViewSet +from .views import HedgehogInfoView, HedgehogUndocumentedView, HedgehogUndocumentedViewset, HedgehogViewSet urlpatterns = [] ROUTER = SimpleRouter() -ROUTER.register(r'api/hedgehog/v0/hogs', HedgehogViewSet, basename='hedgehog') +ROUTER.register( + r'api/hedgehog/v0/hogs', HedgehogViewSet, basename='hedgehog' +) +ROUTER.register( + r'api/hedgehog/v0/undoc-viewset', HedgehogUndocumentedViewset, basename='undoc-viewset' +) urlpatterns += ROUTER.urls urlpatterns += [ url(r'/api/hedgehog/v0/info', HedgehogInfoView.as_view()), + url(r'/api/hedgehog/v0/undoc-view', HedgehogUndocumentedView.as_view()), ] urlpatterns += make_docs_urls( diff --git a/example/views.py b/example/views.py index c08db50..ed9e6bc 100644 --- a/example/views.py +++ b/example/views.py @@ -9,9 +9,17 @@ from rest_framework.exceptions import APIException, NotFound from rest_framework.generics import GenericAPIView -from rest_framework.viewsets import ModelViewSet - -from edx_api_doc_tools import path_parameter, query_parameter, schema, schema_for +from rest_framework.viewsets import ModelViewSet, ViewSet + +from edx_api_doc_tools import ( + exclude_schema, + exclude_schema_for, + exclude_schema_for_all, + path_parameter, + query_parameter, + schema, + schema_for, +) from .data import get_hedgehogs from .serializers import HedgehogSerializer @@ -66,22 +74,8 @@ parameters=[HEDGEHOG_KEY_PARAMETER], responses=HEDGEHOG_ERROR_RESPONSES, ) -@schema_for( - 'update', - """ - Create a or modify a hedgehog. - """, - parameters=[HEDGEHOG_KEY_PARAMETER], - responses=HEDGEHOG_ERROR_RESPONSES, -) -@schema_for( - 'partial_update', - """ - Modify an existing hedgehog. - """, - parameters=[HEDGEHOG_KEY_PARAMETER], - responses=HEDGEHOG_ERROR_RESPONSES, -) +# The next line will exclude the PUT and PATCH endpoints from the API docs. +@exclude_schema_for('update', 'partial_update') @schema_for( 'destroy', """ @@ -142,6 +136,20 @@ def perform_destroy(self, instance): raise EndpointNotImplemented() +@exclude_schema_for_all +class HedgehogUndocumentedViewset(ViewSet): + """ + A view that allows us to retrieve something. + + For whatever reason, we don't want it showing up on the API docs page. + """ + def retrieve(self, request): + """ + Retrieve something or other. + """ + raise EndpointNotImplemented() + + class HedgehogInfoView(GenericAPIView): """Information about the API.""" @@ -154,6 +162,29 @@ def get(self, request): """ raise EndpointNotImplemented() + @exclude_schema + def patch(self, request): + """ + Update information about the Hedgehog API. + + Internal-only; this endpoint is not exposed in the docs. + """ + raise EndpointNotImplemented() + + +@exclude_schema_for_all +class HedgehogUndocumentedView(GenericAPIView): + """ + A view that allows us to GET something. + + For whatever reason, we don't want it showing up on the API docs page. + """ + def get(self, request): + """ + Get something or other. + """ + raise EndpointNotImplemented() + class EndpointNotImplemented(APIException): """ diff --git a/tests/expected_schema.json b/tests/expected_schema.json index de58aa9..128ca87 100644 --- a/tests/expected_schema.json +++ b/tests/expected_schema.json @@ -227,91 +227,7 @@ "required": true, "type": "string" } - ], - "patch": { - "description": "Modify an existing hedgehog.", - "operationId": "hedgehog_v0_hogs_partial_update", - "parameters": [ - { - "in": "body", - "name": "data", - "required": true, - "schema": { - "$ref": "#/definitions/Hedgehog" - } - }, - { - "description": "Key identifying the hog. Lowercase letters only.", - "in": "path", - "name": "hedgehog_key", - "required": true, - "type": "string" - } - ], - "responses": { - "200": { - "description": "", - "schema": { - "$ref": "#/definitions/Hedgehog" - } - }, - "401": { - "description": "Not authenticated" - }, - "403": { - "description": "Operation not permitted." - }, - "404": { - "description": "Hedgehog with given key not found." - } - }, - "summary": "Modify an existing hedgehog.", - "tags": [ - "hedgehog" - ] - }, - "put": { - "description": "Create a or modify a hedgehog.", - "operationId": "hedgehog_v0_hogs_update", - "parameters": [ - { - "in": "body", - "name": "data", - "required": true, - "schema": { - "$ref": "#/definitions/Hedgehog" - } - }, - { - "description": "Key identifying the hog. Lowercase letters only.", - "in": "path", - "name": "hedgehog_key", - "required": true, - "type": "string" - } - ], - "responses": { - "200": { - "description": "", - "schema": { - "$ref": "#/definitions/Hedgehog" - } - }, - "401": { - "description": "Not authenticated" - }, - "403": { - "description": "Operation not permitted." - }, - "404": { - "description": "Hedgehog with given key not found." - } - }, - "summary": "Create a or modify a hedgehog.", - "tags": [ - "hedgehog" - ] - } + ] }, "/hedgehog/v0/info": { "get": { diff --git a/tests/test_doc_tools.py b/tests/test_doc_tools.py index d6dad27..2e47f27 100644 --- a/tests/test_doc_tools.py +++ b/tests/test_doc_tools.py @@ -23,6 +23,8 @@ class DocViewTests(SimpleTestCase): """ Test that the API docs generated from the example Hedgehog API look right. """ + maxDiff = None # Always show full diff output. + base_path = os.path.dirname(__file__) path_of_expected_schema = os.path.join(base_path, 'expected_schema.json') path_of_actual_schema = os.path.join(base_path, 'actual_schema.json')