Skip to content

Commit 7b1582e

Browse files
author
Carlton Gibson
authored
Allow schema = None. Deprecate exclude_from_schema (#5422)
* Add tests for schema exclusions * Move exclusion check to should_include_endpoint * Update docs * Switch to using `schema = None` * Test PendingDeprecationWarnings * Add note to release notes. * s/deprecated/pending deprecation/ * Add PR link to release notes * Correct typo in test class name * Test 'exclude_from_schema' deprecation warning message (#1) * Correct deprecation warning message
1 parent efff9ff commit 7b1582e

File tree

9 files changed

+149
-16
lines changed

9 files changed

+149
-16
lines changed

docs/api-guide/schemas.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,12 @@ To customise the `Link` generation you may:
225225
This allows manually specifying the schema for some views whilst maintaining
226226
automatic generation elsewhere.
227227

228+
You may disable schema generation for a view by setting `schema` to `None`:
229+
230+
class CustomView(APIView):
231+
...
232+
schema = None # Will not appear in schema
233+
228234
---
229235

230236
**Note**: For full details on `SchemaGenerator` plus the `AutoSchema` and

docs/api-guide/views.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ REST framework also allows you to work with regular function based views. It pr
130130

131131
## @api_view()
132132

133-
**Signature:** `@api_view(http_method_names=['GET'], exclude_from_schema=False)`
133+
**Signature:** `@api_view(http_method_names=['GET'])`
134134

135135
The core of this functionality is the `api_view` decorator, which takes a list of HTTP methods that your view should respond to. For example, this is how you would write a very simple view that just manually returns some data:
136136

@@ -150,12 +150,6 @@ By default only `GET` methods will be accepted. Other methods will respond with
150150
return Response({"message": "Got some data!", "data": request.data})
151151
return Response({"message": "Hello, world!"})
152152

153-
You can also mark an API view as being omitted from any [auto-generated schema][schemas],
154-
using the `exclude_from_schema` argument.:
155-
156-
@api_view(['GET'], exclude_from_schema=True)
157-
def api_docs(request):
158-
...
159153

160154
## API policy decorators
161155

@@ -204,7 +198,14 @@ decorator. For example:
204198
return Response({"message": "Hello for today! See you tomorrow!"})
205199

206200
This decorator takes a single `AutoSchema` instance, an `AutoSchema` subclass
207-
instance or `ManualSchema` instance as described in the [Schemas documentation][schemas],
201+
instance or `ManualSchema` instance as described in the [Schemas documentation][schemas].
202+
You may pass `None` in order to exclude the view from schema generation.
203+
204+
@api_view(['GET'])
205+
@schema(None)
206+
def view(request):
207+
return Response({"message": "Will not appear in schema!"})
208+
208209

209210
[cite]: http://reinout.vanrees.org/weblog/2011/08/24/class-based-views-usage.html
210211
[cite2]: http://www.boredomandlaziness.org/2012/05/djangos-cbvs-are-not-mistake-but.html

docs/topics/release-notes.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ You can determine your currently installed version using `pip freeze`:
4343
### 3.6.5
4444

4545
* Fix `DjangoModelPermissions` to ensure user authentication before calling the view's `get_queryset()` method. As a side effect, this changes the order of the HTTP method permissions and authentication checks, and 405 responses will only be returned when authenticated. If you want to replicate the old behavior, see the PR for details. [#5376][gh5376]
46+
* Deprecated `exclude_from_schema` on `APIView` and `api_view` decorator. Set `schema = None` or `@schema(None)` as appropriate. [#5422][gh5422]
47+
4648

4749
### 3.6.4
4850

@@ -1423,3 +1425,4 @@ For older release notes, [please see the version 2.x documentation][old-release-
14231425

14241426
<!-- 3.6.5 -->
14251427
[gh5376]: https://github.com/encode/django-rest-framework/issues/5376
1428+
[gh5422]: https://github.com/encode/django-rest-framework/issues/5422

rest_framework/decorators.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from __future__ import unicode_literals
1010

1111
import types
12+
import warnings
1213

1314
from django.utils import six
1415

@@ -75,7 +76,14 @@ def handler(self, *args, **kwargs):
7576
WrappedAPIView.schema = getattr(func, 'schema',
7677
APIView.schema)
7778

78-
WrappedAPIView.exclude_from_schema = exclude_from_schema
79+
if exclude_from_schema:
80+
warnings.warn(
81+
"The `exclude_from_schema` argument to `api_view` is pending deprecation. "
82+
"Use the `schema` decorator instead, passing `None`.",
83+
PendingDeprecationWarning
84+
)
85+
WrappedAPIView.exclude_from_schema = exclude_from_schema
86+
7987
return WrappedAPIView.as_view()
8088
return decorator
8189

rest_framework/routers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ class APIRootView(views.APIView):
291291
The default basic root view for DefaultRouter
292292
"""
293293
_ignore_model_permissions = True
294-
exclude_from_schema = True
294+
schema = None # exclude from schema
295295
api_root_dict = None
296296

297297
def get(self, request, *args, **kwargs):

rest_framework/schemas/generators.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
44
See schemas.__init__.py for package overview.
55
"""
6+
import warnings
67
from collections import OrderedDict
78
from importlib import import_module
89

@@ -148,6 +149,17 @@ def should_include_endpoint(self, path, callback):
148149
if not is_api_view(callback):
149150
return False # Ignore anything except REST framework views.
150151

152+
if hasattr(callback.cls, 'exclude_from_schema'):
153+
fmt = ("The `{}.exclude_from_schema` attribute is pending deprecation. "
154+
"Set `schema = None` instead.")
155+
msg = fmt.format(callback.cls.__name__)
156+
warnings.warn(msg, PendingDeprecationWarning)
157+
if getattr(callback.cls, 'exclude_from_schema', False):
158+
return False
159+
160+
if callback.cls.schema is None:
161+
return False
162+
151163
if path.endswith('.{format}') or path.endswith('.{format}/'):
152164
return False # Ignore .json style URLs.
153165

@@ -239,8 +251,6 @@ def get_links(self, request=None):
239251
view_endpoints = []
240252
for path, method, callback in self.endpoints:
241253
view = self.create_view(callback, method, request)
242-
if getattr(view, 'exclude_from_schema', False):
243-
continue
244254
path = self.coerce_path(path, method, view)
245255
paths.append(path)
246256
view_endpoints.append((path, method, view))

rest_framework/schemas/views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
class SchemaView(APIView):
1313
_ignore_model_permissions = True
14-
exclude_from_schema = True
14+
schema = None # exclude from schema
1515
renderer_classes = None
1616
schema_generator = None
1717
public = False

rest_framework/views.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,6 @@ class APIView(View):
112112
# Allow dependency injection of other settings to make testing easier.
113113
settings = api_settings
114114

115-
# Mark the view as being included or excluded from schema generation.
116-
exclude_from_schema = False
117115
schema = AutoSchema()
118116

119117
@classmethod

tests/test_schemas.py

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@
88

99
from rest_framework import filters, pagination, permissions, serializers
1010
from rest_framework.compat import coreapi, coreschema
11-
from rest_framework.decorators import detail_route, list_route
11+
from rest_framework.decorators import (
12+
api_view, detail_route, list_route, schema
13+
)
1214
from rest_framework.request import Request
1315
from rest_framework.routers import DefaultRouter
1416
from rest_framework.schemas import (
1517
AutoSchema, ManualSchema, SchemaGenerator, get_schema_view
1618
)
19+
from rest_framework.schemas.generators import EndpointEnumerator
1720
from rest_framework.test import APIClient, APIRequestFactory
1821
from rest_framework.utils import formatting
1922
from rest_framework.views import APIView
@@ -613,3 +616,107 @@ def post(self, request, *args, **kwargs):
613616
descr = schema.get_description('example', 'get')
614617
# the first and last character are '\n' correctly removed by get_description
615618
assert descr == formatting.dedent(ExampleDocstringAPIView.__doc__[1:][:-1])
619+
620+
621+
# Views for SchemaGenerationExclusionTests
622+
class ExcludedAPIView(APIView):
623+
schema = None
624+
625+
def get(self, request, *args, **kwargs):
626+
pass
627+
628+
629+
@api_view(['GET'])
630+
@schema(None)
631+
def excluded_fbv(request):
632+
pass
633+
634+
635+
@api_view(['GET'])
636+
def included_fbv(request):
637+
pass
638+
639+
640+
@unittest.skipUnless(coreapi, 'coreapi is not installed')
641+
class SchemaGenerationExclusionTests(TestCase):
642+
def setUp(self):
643+
self.patterns = [
644+
url('^excluded-cbv/$', ExcludedAPIView.as_view()),
645+
url('^excluded-fbv/$', excluded_fbv),
646+
url('^included-fbv/$', included_fbv),
647+
]
648+
649+
def test_schema_generator_excludes_correctly(self):
650+
"""Schema should not include excluded views"""
651+
generator = SchemaGenerator(title='Exclusions', patterns=self.patterns)
652+
schema = generator.get_schema()
653+
expected = coreapi.Document(
654+
url='',
655+
title='Exclusions',
656+
content={
657+
'included-fbv': {
658+
'list': coreapi.Link(url='/included-fbv/', action='get')
659+
}
660+
}
661+
)
662+
663+
assert len(schema.data) == 1
664+
assert 'included-fbv' in schema.data
665+
assert schema == expected
666+
667+
def test_endpoint_enumerator_excludes_correctly(self):
668+
"""It is responsibility of EndpointEnumerator to exclude views"""
669+
inspector = EndpointEnumerator(self.patterns)
670+
endpoints = inspector.get_api_endpoints()
671+
672+
assert len(endpoints) == 1
673+
path, method, callback = endpoints[0]
674+
assert path == '/included-fbv/'
675+
676+
def test_should_include_endpoint_excludes_correctly(self):
677+
"""This is the specific method that should handle the exclusion"""
678+
inspector = EndpointEnumerator(self.patterns)
679+
680+
# Not pretty. Mimics internals of EndpointEnumerator to put should_include_endpoint under test
681+
pairs = [(inspector.get_path_from_regex(pattern.regex.pattern), pattern.callback)
682+
for pattern in self.patterns]
683+
684+
should_include = [
685+
inspector.should_include_endpoint(*pair) for pair in pairs
686+
]
687+
688+
expected = [False, False, True]
689+
690+
assert should_include == expected
691+
692+
def test_deprecations(self):
693+
with pytest.warns(PendingDeprecationWarning) as record:
694+
@api_view(["GET"], exclude_from_schema=True)
695+
def view(request):
696+
pass
697+
698+
assert len(record) == 1
699+
assert str(record[0].message) == (
700+
"The `exclude_from_schema` argument to `api_view` is pending "
701+
"deprecation. Use the `schema` decorator instead, passing `None`."
702+
)
703+
704+
class OldFashionedExcludedView(APIView):
705+
exclude_from_schema = True
706+
707+
def get(self, request, *args, **kwargs):
708+
pass
709+
710+
patterns = [
711+
url('^excluded-old-fashioned/$', OldFashionedExcludedView.as_view()),
712+
]
713+
714+
inspector = EndpointEnumerator(patterns)
715+
with pytest.warns(PendingDeprecationWarning) as record:
716+
inspector.get_api_endpoints()
717+
718+
assert len(record) == 1
719+
assert str(record[0].message) == (
720+
"The `OldFashionedExcludedView.exclude_from_schema` attribute is "
721+
"pending deprecation. Set `schema = None` instead."
722+
)

0 commit comments

Comments
 (0)