Skip to content

Commit 6511b52

Browse files
Ryan P Kilbycarltongibson
authored andcommitted
Fix schemas for extra actions (#5992)
* Add failing test for extra action schemas * Add ViewInspector setter to store instances * Fix schema disabling for extra actions * Add docs note about disabling schemas for actions
1 parent b23cdaf commit 6511b52

File tree

4 files changed

+76
-4
lines changed

4 files changed

+76
-4
lines changed

docs/api-guide/schemas.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,14 @@ You may disable schema generation for a view by setting `schema` to `None`:
243243
...
244244
schema = None # Will not appear in schema
245245

246+
This also applies to extra actions for `ViewSet`s:
247+
248+
class CustomViewSet(viewsets.ModelViewSet):
249+
250+
@action(detail=True, schema=None)
251+
def extra_action(self, request, pk=None):
252+
...
253+
246254
---
247255

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

rest_framework/schemas/generators.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,10 @@ def should_include_endpoint(self, path, callback):
218218
if callback.cls.schema is None:
219219
return False
220220

221+
if 'schema' in callback.initkwargs:
222+
if callback.initkwargs['schema'] is None:
223+
return False
224+
221225
if path.endswith('.{format}') or path.endswith('.{format}/'):
222226
return False # Ignore .json style URLs.
223227

@@ -365,9 +369,7 @@ def create_view(self, callback, method, request=None):
365369
"""
366370
Given a callback, return an actual view instance.
367371
"""
368-
view = callback.cls()
369-
for attr, val in getattr(callback, 'initkwargs', {}).items():
370-
setattr(view, attr, val)
372+
view = callback.cls(**getattr(callback, 'initkwargs', {}))
371373
view.args = ()
372374
view.kwargs = {}
373375
view.format_kwarg = None

rest_framework/schemas/inspectors.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import re
88
import warnings
99
from collections import OrderedDict
10+
from weakref import WeakKeyDictionary
1011

1112
from django.db import models
1213
from django.utils.encoding import force_text, smart_text
@@ -128,6 +129,10 @@ class ViewInspector(object):
128129
129130
Provide subclass for per-view schema generation
130131
"""
132+
133+
def __init__(self):
134+
self.instance_schemas = WeakKeyDictionary()
135+
131136
def __get__(self, instance, owner):
132137
"""
133138
Enables `ViewInspector` as a Python _Descriptor_.
@@ -144,9 +149,17 @@ def __get__(self, instance, owner):
144149
See: https://docs.python.org/3/howto/descriptor.html for info on
145150
descriptor usage.
146151
"""
152+
if instance in self.instance_schemas:
153+
return self.instance_schemas[instance]
154+
147155
self.view = instance
148156
return self
149157

158+
def __set__(self, instance, other):
159+
self.instance_schemas[instance] = other
160+
if other is not None:
161+
other.view = instance
162+
150163
@property
151164
def view(self):
152165
"""View property."""
@@ -189,6 +202,7 @@ def __init__(self, manual_fields=None):
189202
* `manual_fields`: list of `coreapi.Field` instances that
190203
will be added to auto-generated fields, overwriting on `Field.name`
191204
"""
205+
super(AutoSchema, self).__init__()
192206
if manual_fields is None:
193207
manual_fields = []
194208
self._manual_fields = manual_fields
@@ -455,6 +469,7 @@ def __init__(self, fields, description='', encoding=None):
455469
* `fields`: list of `coreapi.Field` instances.
456470
* `descripton`: String description for view. Optional.
457471
"""
472+
super(ManualSchema, self).__init__()
458473
assert all(isinstance(f, coreapi.Field) for f in fields), "`fields` must be a list of coreapi.Field instances"
459474
self._fields = fields
460475
self._description = description
@@ -474,9 +489,13 @@ def get_link(self, path, method, base_url):
474489
)
475490

476491

477-
class DefaultSchema(object):
492+
class DefaultSchema(ViewInspector):
478493
"""Allows overriding AutoSchema using DEFAULT_SCHEMA_CLASS setting"""
479494
def __get__(self, instance, owner):
495+
result = super(DefaultSchema, self).__get__(instance, owner)
496+
if not isinstance(result, DefaultSchema):
497+
return result
498+
480499
inspector_class = api_settings.DEFAULT_SCHEMA_CLASS
481500
assert issubclass(inspector_class, ViewInspector), "DEFAULT_SCHEMA_CLASS must be set to a ViewInspector (usually an AutoSchema) subclass"
482501
inspector = inspector_class()

tests/test_schemas.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,10 @@ def custom_list_action_multiple_methods_delete(self, request):
105105
"""Deletion description."""
106106
raise NotImplementedError
107107

108+
@action(detail=False, schema=None)
109+
def excluded_action(self, request):
110+
pass
111+
108112
def get_serializer(self, *args, **kwargs):
109113
assert self.request
110114
assert self.action
@@ -735,6 +739,45 @@ class CustomView(APIView):
735739
assert len(fields) == 2
736740
assert "my_extra_field" in [f.name for f in fields]
737741

742+
@pytest.mark.skipif(not coreapi, reason='coreapi is not installed')
743+
def test_viewset_action_with_schema(self):
744+
class CustomViewSet(GenericViewSet):
745+
@action(detail=True, schema=AutoSchema(manual_fields=[
746+
coreapi.Field(
747+
"my_extra_field",
748+
required=True,
749+
location="path",
750+
schema=coreschema.String()
751+
),
752+
]))
753+
def extra_action(self, pk, **kwargs):
754+
pass
755+
756+
router = SimpleRouter()
757+
router.register(r'detail', CustomViewSet, base_name='detail')
758+
759+
generator = SchemaGenerator()
760+
view = generator.create_view(router.urls[0].callback, 'GET')
761+
link = view.schema.get_link('/a/url/{id}/', 'GET', '')
762+
fields = link.fields
763+
764+
assert len(fields) == 2
765+
assert "my_extra_field" in [f.name for f in fields]
766+
767+
@pytest.mark.skipif(not coreapi, reason='coreapi is not installed')
768+
def test_viewset_action_with_null_schema(self):
769+
class CustomViewSet(GenericViewSet):
770+
@action(detail=True, schema=None)
771+
def extra_action(self, pk, **kwargs):
772+
pass
773+
774+
router = SimpleRouter()
775+
router.register(r'detail', CustomViewSet, base_name='detail')
776+
777+
generator = SchemaGenerator()
778+
view = generator.create_view(router.urls[0].callback, 'GET')
779+
assert view.schema is None
780+
738781
@pytest.mark.skipif(not coreapi, reason='coreapi is not installed')
739782
def test_view_with_manual_schema(self):
740783

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy