From 2c9c0f1b7f24e2fbedacbb85e6b56eb244a2353c Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 18 Oct 2018 10:38:46 +0100 Subject: [PATCH] Version 3.9 (#6247) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Release notes to 5174a26ec9d50c7b1e934ee2613de4ae2e2c * Update version for v3.9.0 * Removed exclude_from_schema per deprecation policy. * Updated list_route() and detail_route() deprecations. * Weakened to PendingDeprecationWarning for `base_name` cc @rpkilby. * Add (beginning of) 3.9 release announcement. @tomchristie: Input on OpenAPI and What’s Next very welcome! :) * Add announcement section for Extra Actions in Browsable API * Update release notes and add deprecation note for Django Guardian backend. * Add release note for #6073 * Add release notes to dd19a44583e30bdb325da23583f2a934399155e1 * Adding release notes * Update 3.9 announcement * Add Oct 18 release date --- docs/community/3.9-announcement.md | 212 +++++++++++++++++++++++++++ docs/community/release-notes.md | 78 +++++++++- mkdocs.yml | 1 + rest_framework/__init__.py | 2 +- rest_framework/decorators.py | 19 +-- rest_framework/routers.py | 14 +- rest_framework/schemas/generators.py | 9 -- tests/test_decorators.py | 10 +- tests/test_routers.py | 8 +- tests/test_schemas.py | 32 ---- 10 files changed, 313 insertions(+), 72 deletions(-) create mode 100644 docs/community/3.9-announcement.md diff --git a/docs/community/3.9-announcement.md b/docs/community/3.9-announcement.md new file mode 100644 index 0000000000..3cd9b1a54e --- /dev/null +++ b/docs/community/3.9-announcement.md @@ -0,0 +1,212 @@ + + +# Django REST framework 3.9 + +The 3.9 release gives access to _extra actions_ in the Browsable API, introduces composable permissions and built-in [OpenAPI][openapi] schema support. (Formerly known as Swagger) + +--- + +## Funding + +If you use REST framework commercially and would like to see this work continue, we strongly encourage you to invest in its continued development by +**[signing up for a paid plan][funding]**. + + + +
+ +*Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Rover](http://jobs.rover.com/), [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf), [Auklet](https://auklet.io/), [Rollbar](https://rollbar.com), [Cadre](https://cadre.com), [Load Impact](https://loadimpact.com/?utm_campaign=Sponsorship%20links&utm_source=drf&utm_medium=drf), and [Kloudless](https://hubs.ly/H0f30Lf0).* + +--- + +## Built-in OpenAPI schema support + +REST framework now has a first-pass at directly including OpenAPI schema support. (Formerly known as Swagger) + +Specifically: + +* There are now `OpenAPIRenderer`, and `JSONOpenAPIRenderer` classes that deal with encoding `coreapi.Document` instances into OpenAPI YAML or OpenAPI JSON. +* The `get_schema_view(...)` method now defaults to OpenAPI YAML, with CoreJSON as a secondary +option if it is selected via HTTP content negotiation. +* There is a new management command `generateschema`, which you can use to dump +the schema into your repository. + +Here's an example of adding an OpenAPI schema to the URL conf: + +```python +from rest_framework.schemas import get_schema_view +from rest_framework.renderers import JSONOpenAPIRenderer + +schema_view = get_schema_view( + title='Server Monitoring API', + url='https://www.example.org/api/', + renderer_classes=[JSONOpenAPIRenderer] +) + +urlpatterns = [ + url('^schema.json$', schema_view), + ... +] +``` + +And here's how you can use the `generateschema` management command: + +```shell +$ python manage.py generateschema --format openapi > schema.yml +``` + +There's lots of different tooling that you can use for working with OpenAPI +schemas. One option that we're working on is the [API Star](https://docs.apistar.com/) +command line tool. + +You can use `apistar` to validate your API schema: + +```shell +$ apistar validate --path schema.json --format openapi +✓ Valid OpenAPI schema. +``` + +Or to build API documentation: + +```shell +$ apistar docs --path schema.json --format openapi +✓ Documentation built at "build/index.html". +``` + +API Star also includes a [dynamic client library](https://docs.apistar.com/client-library/) +that uses an API schema to automatically provide a client library interface for making requests. + +## Composable permission classes + +You can now compose permission classes using the and/or operators, `&` and `|`. + +For example... + +```python +permission_classes = [IsAuthenticated & (ReadOnly | IsAdmin)] +``` + +If you're using custom permission classes then make sure that you are subclassing +from `BasePermission` in order to enable this support. + +## ViewSet _Extra Actions_ available in the Browsable API + +Following the introduction of the `action` decorator in v3.8, _extra actions_ defined on a ViewSet are now available +from the Browsable API. + +![Extra Actions displayed in the Browsable API](https://user-images.githubusercontent.com/2370209/32976956-1ca9ab7e-cbf1-11e7-981a-a20cb1e83d63.png) + +When defined, a dropdown of "Extra Actions", appropriately filtered to detail/non-detail actions, is displayed. + +--- + +## Supported Versions + +REST framework 3.9 supports Django versions 1.11, 2.0, and 2.1. + +--- + +## Deprecations + +### `DjangoObjectPermissionsFilter` moved to third-party package. + +The `DjangoObjectPermissionsFilter` class is pending deprecation, will be deprecated in 3.10 and removed entirely in 3.11. + +It has been moved to the third-party [`djangorestframework-guardian`](https://github.com/rpkilby/django-rest-framework-guardian) +package. Please use this instead. + +### Router argument/method renamed to use `basename` for consistency. + +* The `Router.register` `base_name` argument has been renamed in favor of `basename`. +* The `Router.get_default_base_name` method has been renamed in favor of `Router.get_default_basename`. [#5990][gh5990] + +See [#5990][gh5990]. + +[gh5990]: https://github.com/encode/django-rest-framework/pull/5990 + +`base_name` and `get_default_base_name()` are pending deprecation. They will be deprecated in 3.10 and removed entirely in 3.11. + +### `action` decorator replaces `list_route` and `detail_route` + +Both `list_route` and `detail_route` are now deprecated in favour of the single `action` decorator. +They will be removed entirely in 3.10. + +The `action` decorator takes a boolean `detail` argument. + +* Replace `detail_route` uses with `@action(detail=True)`. +* Replace `list_route` uses with `@action(detail=False)`. + +### `exclude_from_schema` + +Both `APIView.exclude_from_schema` and the `exclude_from_schema` argument to the `@api_view` have now been removed. + +For `APIView` you should instead set a `schema = None` attribute on the view class. + +For function based views the `@schema` decorator can be used to exclude the view from the schema, by using `@schema(None)`. + +--- + +## Minor fixes and improvements + +There are a large number of minor fixes and improvements in this release. See the [release notes](release-notes.md) page for a complete listing. + + +## What's next + +We're planning to iteratively working towards OpenAPI becoming the standard schema +representation. This will mean that the `coreapi` dependency will gradually become +removed, and we'll instead generate the schema directly, rather than building +a CoreAPI `Document` object. + +OpenAPI has clearly become the standard for specifying Web APIs, so there's not +much value any more in our schema-agnostic document model. Making this change +will mean that we'll more easily be able to take advantage of the full set of +OpenAPI functionality. + +This will also make a wider range of tooling available. + +We'll focus on continuing to develop the [API Star](https://docs.apistar.com/) +library and client tool into a recommended option for generating API docs, +validating API schemas, and providing a dynamic client library. + +There's also a huge amount of ongoing work on maturing the ASGI landscape, +with the possibility that some of this work will eventually [feed back into +Django](https://www.aeracode.org/2018/06/04/django-async-roadmap/). + +There will be further work on the [Uvicorn](https://www.uvicorn.org/) +webserver, as well as lots of functionality planned for the [Starlette](https://www.starlette.io/) +web framework, which is building a foundational set of tooling for working with +ASGI. + + +[funding]: funding.md +[gh5886]: https://github.com/encode/django-rest-framework/issues/5886 +[gh5705]: https://github.com/encode/django-rest-framework/issues/5705 +[openapi]: https://www.openapis.org/ +[sponsors]: https://fund.django-rest-framework.org/topics/funding/#our-sponsors diff --git a/docs/community/release-notes.md b/docs/community/release-notes.md index 700d1d97f6..ac415c9b89 100644 --- a/docs/community/release-notes.md +++ b/docs/community/release-notes.md @@ -42,11 +42,52 @@ You can determine your currently installed version using `pip show`: ### 3.9.0 -**Date**: Unreleased +**Date**: [18st October 2018][3.9.0-milestone] +* Improvements to ViewSet extra actions [#5605][gh5605] +* Fix `action` support for ViewSet suffixes [#6081][gh6081] +* Allow `action` docs sections [#6060][gh6060] * Deprecate the `Router.register` `base_name` argument in favor of `basename`. [#5990][gh5990] * Deprecate the `Router.get_default_base_name` method in favor of `Router.get_default_basename`. [#5990][gh5990] +* Change `CharField` to disallow null bytes. [#6073][gh6073] + To revert to the old behavior, subclass `CharField` and remove `ProhibitNullCharactersValidator` from the validators. + ```python + class NullableCharField(serializers.CharField): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.validators = [v for v in self.validators if not isinstance(v, ProhibitNullCharactersValidator)] + ``` +* Add `OpenAPIRenderer` and `generate_schema` management command. [#6229][gh6229] +* Add OpenAPIRenderer by default, and add schema docs. [#6233][gh6233] +* Allow permissions to be composed [#5753][gh5753] +* Allow nullable BooleanField in Django 2.1 [#6183][gh6183] +* Add testing of Python 3.7 support [#6141][gh6141] +* Test using Django 2.1 final release. [#6109][gh6109] +* Added djangorestframework-datatables to third-party packages [#5931][gh5931] +* Change ISO 8601 date format to exclude year/month [#5936][gh5936] +* Update all pypi.python.org URLs to pypi.org [#5942][gh5942] +* Ensure that html forms (multipart form data) respect optional fields [#5927][gh5927] +* Allow hashing of ErrorDetail. [#5932][gh5932] +* Correct schema parsing for JSONField [#5878][gh5878] +* Render descriptions (from help_text) using safe [#5869][gh5869] +* Removed input value from deault_error_message [#5881][gh5881] +* Added min_value/max_value support in DurationField [#5643][gh5643] +* Fixed instance being overwritten in pk-only optimization try/except block [#5747][gh5747] +* Fixed AttributeError from items filter when value is None [#5981][gh5981] +* Fixed Javascript `e.indexOf` is not a function error [#5982][gh5982] +* Fix schemas for extra actions [#5992][gh5992] +* Improved get_error_detail to use error_dict/error_list [#5785][gh5785] +* Imprvied URLs in Admin renderer [#5988][gh5988] +* Add "Community" section to docs, minor cleanup [#5993][gh5993] +* Moved guardian imports out of compat [#6054][gh6054] * Deprecate the `DjangoObjectPermissionsFilter` class, moved to the `djangorestframework-guardian` package. [#6075][gh6075] +* Drop Django 1.10 support [#5657][gh5657] +* Only catch TypeError/ValueError for object lookups [#6028][gh6028] +* Handle models without .objects manager in ModelSerializer. [#6111][gh6111] +* Improve ModelSerializer.create() error message. [#6112][gh6112] +* Fix CSRF cookie check failure when using session auth with django 1.11.6+ [#6113][gh6113] +* Updated JWT docs. [#6138][gh6138] +* Fix autoescape not getting passed to urlize_quoted_links filter [#6191][gh6191] ## 3.8.x series @@ -1093,6 +1134,7 @@ For older release notes, [please see the version 2.x documentation][old-release- [3.8.0-milestone]: https://github.com/encode/django-rest-framework/milestone/61?closed=1 [3.8.1-milestone]: https://github.com/encode/django-rest-framework/milestone/67?closed=1 [3.8.2-milestone]: https://github.com/encode/django-rest-framework/milestone/68?closed=1 +[3.9.0-milestone]: https://github.com/encode/django-rest-framework/milestone/66?closed=1 [gh2013]: https://github.com/encode/django-rest-framework/issues/2013 @@ -1974,5 +2016,39 @@ For older release notes, [please see the version 2.x documentation][old-release- [gh5920]: https://github.com/encode/django-rest-framework/issues/5920 +[gh6109]: https://github.com/encode/django-rest-framework/issues/6109 +[gh6141]: https://github.com/encode/django-rest-framework/issues/6141 +[gh6113]: https://github.com/encode/django-rest-framework/issues/6113 +[gh6112]: https://github.com/encode/django-rest-framework/issues/6112 +[gh6111]: https://github.com/encode/django-rest-framework/issues/6111 +[gh6028]: https://github.com/encode/django-rest-framework/issues/6028 +[gh5657]: https://github.com/encode/django-rest-framework/issues/5657 +[gh6054]: https://github.com/encode/django-rest-framework/issues/6054 +[gh5993]: https://github.com/encode/django-rest-framework/issues/5993 [gh5990]: https://github.com/encode/django-rest-framework/issues/5990 +[gh5988]: https://github.com/encode/django-rest-framework/issues/5988 +[gh5785]: https://github.com/encode/django-rest-framework/issues/5785 +[gh5992]: https://github.com/encode/django-rest-framework/issues/5992 +[gh5605]: https://github.com/encode/django-rest-framework/issues/5605 +[gh5982]: https://github.com/encode/django-rest-framework/issues/5982 +[gh5981]: https://github.com/encode/django-rest-framework/issues/5981 +[gh5747]: https://github.com/encode/django-rest-framework/issues/5747 +[gh5643]: https://github.com/encode/django-rest-framework/issues/5643 +[gh5881]: https://github.com/encode/django-rest-framework/issues/5881 +[gh5869]: https://github.com/encode/django-rest-framework/issues/5869 +[gh5878]: https://github.com/encode/django-rest-framework/issues/5878 +[gh5932]: https://github.com/encode/django-rest-framework/issues/5932 +[gh5927]: https://github.com/encode/django-rest-framework/issues/5927 +[gh5942]: https://github.com/encode/django-rest-framework/issues/5942 +[gh5936]: https://github.com/encode/django-rest-framework/issues/5936 +[gh5931]: https://github.com/encode/django-rest-framework/issues/5931 +[gh6183]: https://github.com/encode/django-rest-framework/issues/6183 [gh6075]: https://github.com/encode/django-rest-framework/issues/6075 +[gh6138]: https://github.com/encode/django-rest-framework/issues/6138 +[gh6081]: https://github.com/encode/django-rest-framework/issues/6081 +[gh6073]: https://github.com/encode/django-rest-framework/issues/6073 +[gh6191]: https://github.com/encode/django-rest-framework/issues/6191 +[gh6060]: https://github.com/encode/django-rest-framework/issues/6060 +[gh6233]: https://github.com/encode/django-rest-framework/issues/6233 +[gh5753]: https://github.com/encode/django-rest-framework/issues/5753 +[gh6229]: https://github.com/encode/django-rest-framework/issues/6229 diff --git a/mkdocs.yml b/mkdocs.yml index fbdb8d779c..83b3c8141e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -65,6 +65,7 @@ pages: - 'Contributing to REST framework': 'community/contributing.md' - 'Project management': 'community/project-management.md' - 'Release Notes': 'community/release-notes.md' + - '3.9 Announcement': 'community/3.9-announcement.md' - '3.8 Announcement': 'community/3.8-announcement.md' - '3.7 Announcement': 'community/3.7-announcement.md' - '3.6 Announcement': 'community/3.6-announcement.md' diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index fa92ab8014..1a9a6527ad 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -8,7 +8,7 @@ """ __title__ = 'Django REST framework' -__version__ = '3.8.2' +__version__ = '3.9.0' __author__ = 'Tom Christie' __license__ = 'BSD 2-Clause' __copyright__ = 'Copyright 2011-2018 Tom Christie' diff --git a/rest_framework/decorators.py b/rest_framework/decorators.py index 2d3bbe46f9..f6d557d11f 100644 --- a/rest_framework/decorators.py +++ b/rest_framework/decorators.py @@ -17,7 +17,7 @@ from rest_framework.views import APIView -def api_view(http_method_names=None, exclude_from_schema=False): +def api_view(http_method_names=None): """ Decorator that converts a function-based view into an APIView subclass. Takes a list of allowed methods for the view as an argument. @@ -77,15 +77,8 @@ def handler(self, *args, **kwargs): WrappedAPIView.schema = getattr(func, 'schema', APIView.schema) - if exclude_from_schema: - warnings.warn( - "The `exclude_from_schema` argument to `api_view` is deprecated. " - "Use the `schema` decorator instead, passing `None`.", - DeprecationWarning - ) - WrappedAPIView.exclude_from_schema = exclude_from_schema - return WrappedAPIView.as_view() + return decorator @@ -230,9 +223,9 @@ def detail_route(methods=None, **kwargs): Used to mark a method on a ViewSet that should be routed for detail requests. """ warnings.warn( - "`detail_route` is pending deprecation and will be removed in 3.10 in favor of " + "`detail_route` is deprecated and will be removed in 3.10 in favor of " "`action`, which accepts a `detail` bool. Use `@action(detail=True)` instead.", - PendingDeprecationWarning, stacklevel=2 + DeprecationWarning, stacklevel=2 ) def decorator(func): @@ -248,9 +241,9 @@ def list_route(methods=None, **kwargs): Used to mark a method on a ViewSet that should be routed for list requests. """ warnings.warn( - "`list_route` is pending deprecation and will be removed in 3.10 in favor of " + "`list_route` is deprecated and will be removed in 3.10 in favor of " "`action`, which accepts a `detail` bool. Use `@action(detail=False)` instead.", - PendingDeprecationWarning, stacklevel=2 + DeprecationWarning, stacklevel=2 ) def decorator(func): diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 392d43f797..de04cb6740 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -40,10 +40,10 @@ class DynamicDetailRoute(object): def __new__(cls, url, name, initkwargs): warnings.warn( - "`DynamicDetailRoute` is pending deprecation and will be removed in 3.10 " + "`DynamicDetailRoute` is deprecated and will be removed in 3.10 " "in favor of `DynamicRoute`, which accepts a `detail` boolean. Use " "`DynamicRoute(url, name, True, initkwargs)` instead.", - PendingDeprecationWarning, stacklevel=2 + DeprecationWarning, stacklevel=2 ) return DynamicRoute(url, name, True, initkwargs) @@ -51,10 +51,10 @@ def __new__(cls, url, name, initkwargs): class DynamicListRoute(object): def __new__(cls, url, name, initkwargs): warnings.warn( - "`DynamicListRoute` is pending deprecation and will be removed in 3.10 in " + "`DynamicListRoute` is deprecated and will be removed in 3.10 in " "favor of `DynamicRoute`, which accepts a `detail` boolean. Use " "`DynamicRoute(url, name, False, initkwargs)` instead.", - PendingDeprecationWarning, stacklevel=2 + DeprecationWarning, stacklevel=2 ) return DynamicRoute(url, name, False, initkwargs) @@ -77,7 +77,7 @@ def flatten(list_of_lists): class RenameRouterMethods(RenameMethodsBase): renamed_methods = ( - ('get_default_base_name', 'get_default_basename', DeprecationWarning), + ('get_default_base_name', 'get_default_basename', PendingDeprecationWarning), ) @@ -87,8 +87,8 @@ def __init__(self): def register(self, prefix, viewset, basename=None, base_name=None): if base_name is not None: - msg = "The `base_name` argument has been deprecated in favor of `basename`." - warnings.warn(msg, DeprecationWarning, 2) + msg = "The `base_name` argument is pending deprecation in favor of `basename`." + warnings.warn(msg, PendingDeprecationWarning, 2) assert not (basename and base_name), ( "Do not provide both the `basename` and `base_name` arguments.") diff --git a/rest_framework/schemas/generators.py b/rest_framework/schemas/generators.py index 8794c9967c..c5bda1f1f5 100644 --- a/rest_framework/schemas/generators.py +++ b/rest_framework/schemas/generators.py @@ -4,7 +4,6 @@ See schemas.__init__.py for package overview. """ import re -import warnings from collections import Counter, OrderedDict from importlib import import_module @@ -207,14 +206,6 @@ def should_include_endpoint(self, path, callback): if not is_api_view(callback): return False # Ignore anything except REST framework views. - if hasattr(callback.cls, 'exclude_from_schema'): - fmt = ("The `{}.exclude_from_schema` attribute is deprecated. " - "Set `schema = None` instead.") - msg = fmt.format(callback.cls.__name__) - warnings.warn(msg, DeprecationWarning) - if getattr(callback.cls, 'exclude_from_schema', False): - return False - if callback.cls.schema is None: return False diff --git a/tests/test_decorators.py b/tests/test_decorators.py index 77c488c340..9c6a899bfa 100644 --- a/tests/test_decorators.py +++ b/tests/test_decorators.py @@ -290,34 +290,34 @@ def test_action(): raise NotImplementedError def test_detail_route_deprecation(self): - with pytest.warns(PendingDeprecationWarning) as record: + with pytest.warns(DeprecationWarning) as record: @detail_route() def view(request): raise NotImplementedError assert len(record) == 1 assert str(record[0].message) == ( - "`detail_route` is pending deprecation and will be removed in " + "`detail_route` is deprecated and will be removed in " "3.10 in favor of `action`, which accepts a `detail` bool. Use " "`@action(detail=True)` instead." ) def test_list_route_deprecation(self): - with pytest.warns(PendingDeprecationWarning) as record: + with pytest.warns(DeprecationWarning) as record: @list_route() def view(request): raise NotImplementedError assert len(record) == 1 assert str(record[0].message) == ( - "`list_route` is pending deprecation and will be removed in " + "`list_route` is deprecated and will be removed in " "3.10 in favor of `action`, which accepts a `detail` bool. Use " "`@action(detail=False)` instead." ) def test_route_url_name_from_path(self): # pre-3.8 behavior was to base the `url_name` off of the `url_path` - with pytest.warns(PendingDeprecationWarning): + with pytest.warns(DeprecationWarning): @list_route(url_path='foo_bar') def view(request): raise NotImplementedError diff --git a/tests/test_routers.py b/tests/test_routers.py index 1dd2d2b0a4..eae3f54585 100644 --- a/tests/test_routers.py +++ b/tests/test_routers.py @@ -495,18 +495,18 @@ def test_base_name_and_basename_assertion(self): warnings.simplefilter('always') router.register('mock', MockViewSet, 'mock', base_name='mock') - msg = "The `base_name` argument has been deprecated in favor of `basename`." + msg = "The `base_name` argument is pending deprecation in favor of `basename`." assert len(w) == 1 assert str(w[0].message) == msg def test_base_name_argument_deprecation(self): router = SimpleRouter() - with warnings.catch_warnings(record=True) as w: + with pytest.warns(PendingDeprecationWarning) as w: warnings.simplefilter('always') router.register('mock', MockViewSet, base_name='mock') - msg = "The `base_name` argument has been deprecated in favor of `basename`." + msg = "The `base_name` argument is pending deprecation in favor of `basename`." assert len(w) == 1 assert str(w[0].message) == msg assert router.registry == [ @@ -529,7 +529,7 @@ def test_get_default_base_name_deprecation(self): msg = "`CustomRouter.get_default_base_name` method should be renamed `get_default_basename`." # Class definition should raise a warning - with warnings.catch_warnings(record=True) as w: + with pytest.warns(PendingDeprecationWarning) as w: warnings.simplefilter('always') class CustomRouter(SimpleRouter): diff --git a/tests/test_schemas.py b/tests/test_schemas.py index ad2e34a4ba..8e097f9f4b 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -1032,38 +1032,6 @@ def test_should_include_endpoint_excludes_correctly(self): assert should_include == expected - def test_deprecations(self): - with pytest.warns(DeprecationWarning) as record: - @api_view(["GET"], exclude_from_schema=True) - def view(request): - pass - - assert len(record) == 1 - assert str(record[0].message) == ( - "The `exclude_from_schema` argument to `api_view` is deprecated. " - "Use the `schema` decorator instead, passing `None`." - ) - - class OldFashionedExcludedView(APIView): - exclude_from_schema = True - - def get(self, request, *args, **kwargs): - pass - - patterns = [ - url('^excluded-old-fashioned/$', OldFashionedExcludedView.as_view()), - ] - - inspector = EndpointEnumerator(patterns) - with pytest.warns(DeprecationWarning) as record: - inspector.get_api_endpoints() - - assert len(record) == 1 - assert str(record[0].message) == ( - "The `OldFashionedExcludedView.exclude_from_schema` attribute is " - "deprecated. Set `schema = None` instead." - ) - @api_view(["GET"]) def simple_fbv(request):