From 92bb9ec3e99e9ff71de86b211e8a534f53c7f917 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Thu, 17 May 2018 02:07:44 -0400 Subject: [PATCH 1/4] Add failing test for extra action schemas --- tests/test_schemas.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/test_schemas.py b/tests/test_schemas.py index f929fece5f..c0d6deca1e 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -720,6 +720,31 @@ class CustomView(APIView): assert len(fields) == 2 assert "my_extra_field" in [f.name for f in fields] + @pytest.mark.skipif(not coreapi, reason='coreapi is not installed') + def test_viewset_action_with_schema(self): + class CustomViewSet(GenericViewSet): + @action(detail=True, schema=AutoSchema(manual_fields=[ + coreapi.Field( + "my_extra_field", + required=True, + location="path", + schema=coreschema.String() + ), + ])) + def extra_action(self, pk, **kwargs): + pass + + router = SimpleRouter() + router.register(r'detail', CustomViewSet, base_name='detail') + + generator = SchemaGenerator() + view = generator.create_view(router.urls[0].callback, 'GET') + link = view.schema.get_link('/a/url/{id}/', 'GET', '') + fields = link.fields + + assert len(fields) == 2 + assert "my_extra_field" in [f.name for f in fields] + @pytest.mark.skipif(not coreapi, reason='coreapi is not installed') def test_view_with_manual_schema(self): From 3642fe0b30b2f7ddc824addf97cdad2d70c47671 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Thu, 17 May 2018 02:10:05 -0400 Subject: [PATCH 2/4] Add ViewInspector setter to store instances --- rest_framework/schemas/generators.py | 4 +--- rest_framework/schemas/inspectors.py | 20 +++++++++++++++++++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/rest_framework/schemas/generators.py b/rest_framework/schemas/generators.py index 629f92b0da..3d46251b99 100644 --- a/rest_framework/schemas/generators.py +++ b/rest_framework/schemas/generators.py @@ -365,9 +365,7 @@ def create_view(self, callback, method, request=None): """ Given a callback, return an actual view instance. """ - view = callback.cls() - for attr, val in getattr(callback, 'initkwargs', {}).items(): - setattr(view, attr, val) + view = callback.cls(**getattr(callback, 'initkwargs', {})) view.args = () view.kwargs = {} view.format_kwarg = None diff --git a/rest_framework/schemas/inspectors.py b/rest_framework/schemas/inspectors.py index 89a1fc93a5..5c9659a57d 100644 --- a/rest_framework/schemas/inspectors.py +++ b/rest_framework/schemas/inspectors.py @@ -7,6 +7,7 @@ import re import warnings from collections import OrderedDict +from weakref import WeakKeyDictionary from django.db import models from django.utils.encoding import force_text, smart_text @@ -128,6 +129,10 @@ class ViewInspector(object): Provide subclass for per-view schema generation """ + + def __init__(self): + self.instance_schemas = WeakKeyDictionary() + def __get__(self, instance, owner): """ Enables `ViewInspector` as a Python _Descriptor_. @@ -144,9 +149,16 @@ def __get__(self, instance, owner): See: https://docs.python.org/3/howto/descriptor.html for info on descriptor usage. """ + if instance in self.instance_schemas: + return self.instance_schemas[instance] + self.view = instance return self + def __set__(self, instance, other): + self.instance_schemas[instance] = other + other.view = instance + @property def view(self): """View property.""" @@ -189,6 +201,7 @@ def __init__(self, manual_fields=None): * `manual_fields`: list of `coreapi.Field` instances that will be added to auto-generated fields, overwriting on `Field.name` """ + super(AutoSchema, self).__init__() if manual_fields is None: manual_fields = [] self._manual_fields = manual_fields @@ -455,6 +468,7 @@ def __init__(self, fields, description='', encoding=None): * `fields`: list of `coreapi.Field` instances. * `descripton`: String description for view. Optional. """ + super(ManualSchema, self).__init__() assert all(isinstance(f, coreapi.Field) for f in fields), "`fields` must be a list of coreapi.Field instances" self._fields = fields self._description = description @@ -474,9 +488,13 @@ def get_link(self, path, method, base_url): ) -class DefaultSchema(object): +class DefaultSchema(ViewInspector): """Allows overriding AutoSchema using DEFAULT_SCHEMA_CLASS setting""" def __get__(self, instance, owner): + result = super(DefaultSchema, self).__get__(instance, owner) + if not isinstance(result, DefaultSchema): + return result + inspector_class = api_settings.DEFAULT_SCHEMA_CLASS assert issubclass(inspector_class, ViewInspector), "DEFAULT_SCHEMA_CLASS must be set to a ViewInspector (usually an AutoSchema) subclass" inspector = inspector_class() From 3feea10bae65fcefa4c0f9723a2d61f257f62588 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Thu, 31 May 2018 12:03:13 -0400 Subject: [PATCH 3/4] Fix schema disabling for extra actions --- rest_framework/schemas/generators.py | 4 ++++ rest_framework/schemas/inspectors.py | 3 ++- tests/test_schemas.py | 18 ++++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/rest_framework/schemas/generators.py b/rest_framework/schemas/generators.py index 3d46251b99..8794c9967c 100644 --- a/rest_framework/schemas/generators.py +++ b/rest_framework/schemas/generators.py @@ -218,6 +218,10 @@ def should_include_endpoint(self, path, callback): if callback.cls.schema is None: return False + if 'schema' in callback.initkwargs: + if callback.initkwargs['schema'] is None: + return False + if path.endswith('.{format}') or path.endswith('.{format}/'): return False # Ignore .json style URLs. diff --git a/rest_framework/schemas/inspectors.py b/rest_framework/schemas/inspectors.py index 5c9659a57d..b90f60e084 100644 --- a/rest_framework/schemas/inspectors.py +++ b/rest_framework/schemas/inspectors.py @@ -157,7 +157,8 @@ def __get__(self, instance, owner): def __set__(self, instance, other): self.instance_schemas[instance] = other - other.view = instance + if other is not None: + other.view = instance @property def view(self): diff --git a/tests/test_schemas.py b/tests/test_schemas.py index c0d6deca1e..fdaee0c102 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -99,6 +99,10 @@ def custom_list_action(self, request): def custom_list_action_multiple_methods(self, request): return super(ExampleViewSet, self).list(self, request) + @action(detail=False, schema=None) + def excluded_action(self, request): + pass + def get_serializer(self, *args, **kwargs): assert self.request assert self.action @@ -745,6 +749,20 @@ def extra_action(self, pk, **kwargs): assert len(fields) == 2 assert "my_extra_field" in [f.name for f in fields] + @pytest.mark.skipif(not coreapi, reason='coreapi is not installed') + def test_viewset_action_with_null_schema(self): + class CustomViewSet(GenericViewSet): + @action(detail=True, schema=None) + def extra_action(self, pk, **kwargs): + pass + + router = SimpleRouter() + router.register(r'detail', CustomViewSet, base_name='detail') + + generator = SchemaGenerator() + view = generator.create_view(router.urls[0].callback, 'GET') + assert view.schema is None + @pytest.mark.skipif(not coreapi, reason='coreapi is not installed') def test_view_with_manual_schema(self): From 16bfa3521b5c9c0bd9e7f1b2bab9518a20dcb239 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Fri, 22 Jun 2018 17:34:58 -0400 Subject: [PATCH 4/4] Add docs note about disabling schemas for actions --- docs/api-guide/schemas.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/api-guide/schemas.md b/docs/api-guide/schemas.md index aaefe3db8f..25ce9df0d7 100644 --- a/docs/api-guide/schemas.md +++ b/docs/api-guide/schemas.md @@ -243,6 +243,14 @@ You may disable schema generation for a view by setting `schema` to `None`: ... schema = None # Will not appear in schema +This also applies to extra actions for `ViewSet`s: + + class CustomViewSet(viewsets.ModelViewSet): + + @action(detail=True, schema=None) + def extra_action(self, request, pk=None): + ... + --- **Note**: For full details on `SchemaGenerator` plus the `AutoSchema` and