Skip to content

Commit

Permalink
Merge pull request #1065 from tfranzel/list_helper
Browse files Browse the repository at this point in the history
add helper to disable viewset list detection #1064
  • Loading branch information
tfranzel authored Aug 30, 2023
2 parents 097f9d3 + c565a38 commit 3b028c2
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 4 deletions.
25 changes: 25 additions & 0 deletions docs/faq.rst
Original file line number Diff line number Diff line change
Expand Up @@ -414,3 +414,28 @@ response is a binary blob without further details on its structure.
"Content-Disposition": "attachment; filename=out.bin",
},
)
My ``ViewSet`` ``list`` does not return a list, but a single object.
--------------------------------------------------------------------
Generally, it is bad practice to use a ``ViewSet.list`` method to return single object,
because DRF specifically does a list conversion in the background for this method and only
this method. Using ``ApiView`` or ``GenericAPIView`` for this use-case would be cleaner.
However, if you insist on this behavior, you can circumvent the list detection by
creating a one-off copy of your serializer and marking it as forced non-list.
It is important to create a **copy** as
:py:func:`@extend_schema_serializer <drf_spectacular.utils.extend_schema_serializer>`
modifies the given serializer.
.. code-block:: python
from drf_spectacular.helpers import forced_singular_serializer
class YourViewSet(viewsets.ModelViewSet):
serializer_class = SimpleSerializer
queryset = SimpleModel.objects.none()
@extend_schema(responses=forced_singular_serializer(SimpleSerializer))
def list(self):
pass
14 changes: 13 additions & 1 deletion drf_spectacular/helpers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from django.utils.module_loading import import_string


def lazy_serializer(path):
def lazy_serializer(path: str):
""" simulate initiated object but actually load class and init on first usage """

class LazySerializer:
Expand All @@ -28,3 +28,15 @@ def __repr__(self):
return self.__getattr__('__repr__')()

return LazySerializer


def forced_singular_serializer(serializer_class):
from drf_spectacular.drainage import set_override
from drf_spectacular.utils import extend_schema_serializer

patched_serializer_class = type(serializer_class.__name__, (serializer_class,), {})

extend_schema_serializer(many=False)(patched_serializer_class)
set_override(patched_serializer_class, 'suppress_collision_warning', True)

return patched_serializer_class
8 changes: 6 additions & 2 deletions drf_spectacular/plumbing.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
from rest_framework.utils.serializer_helpers import ReturnDict, ReturnList
from uritemplate import URITemplate

from drf_spectacular.drainage import cache, error, warn
from drf_spectacular.drainage import cache, error, get_override, warn
from drf_spectacular.settings import spectacular_settings
from drf_spectacular.types import (
DJANGO_PATH_CONVERTER_MAPPING, OPENAPI_TYPE_MAPPING, PYTHON_TYPE_MAPPING, OpenApiTypes,
Expand Down Expand Up @@ -688,7 +688,11 @@ def __contains__(self, component):
query_class = query_obj if inspect.isclass(query_obj) else query_obj.__class__
registry_class = query_obj if inspect.isclass(registry_obj) else registry_obj.__class__

if query_class != registry_class:
suppress_collision_warning = (
get_override(registry_class, 'suppress_collision_warning', False)
or get_override(query_class, 'suppress_collision_warning', False)
)
if query_class != registry_class and not suppress_collision_warning:
warn(
f'Encountered 2 components with identical names "{component.name}" and '
f'different classes {query_class} and {registry_class}. This will very '
Expand Down
31 changes: 30 additions & 1 deletion tests/test_regressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from rest_framework.views import APIView

from drf_spectacular.extensions import OpenApiSerializerExtension
from drf_spectacular.helpers import forced_singular_serializer
from drf_spectacular.hooks import preprocess_exclude_path_format
from drf_spectacular.openapi import AutoSchema
from drf_spectacular.renderers import OpenApiJsonRenderer, OpenApiYamlRenderer
Expand Down Expand Up @@ -3014,7 +3015,7 @@ def test_slug_related_field_to_model_property(no_warnings):
class M10(models.Model):
@property
def property_field(self) -> float:
return 42
return 42 # pragma: no cover

class M11(models.Model):
field = models.ForeignKey(M10, on_delete=models.CASCADE)
Expand Down Expand Up @@ -3183,3 +3184,31 @@ class X3ViewSet(X2ViewSet):
assert '/x1/' not in schema['paths']
assert '/x2/' in schema['paths']
assert '/x3/' in schema['paths']


def test_disable_viewset_list_handling_as_one_off(no_warnings):

class X1ViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = SimpleSerializer
queryset = SimpleModel.objects.none()

@extend_schema(responses=forced_singular_serializer(SimpleSerializer))
def list(self):
pass # pragma: no cover

class X2ViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = SimpleSerializer
queryset = SimpleModel.objects.none()

schema1 = generate_schema('/x', X1ViewSet)
schema2 = generate_schema('/x', X2ViewSet)

# both list and retrieve are single-object
schema_list = get_response_schema(schema1['paths']['/x/']['get'])
schema_retrieve = get_response_schema(schema1['paths']['/x/{id}/']['get'])
assert schema_list == schema_retrieve == {'$ref': '#/components/schemas/Simple'}
# this patch does not bleed into other usages of the same serializer class
assert get_response_schema(schema2['paths']['/x/']['get']) == {
'type': 'array',
'items': {'$ref': '#/components/schemas/Simple'}
}

0 comments on commit 3b028c2

Please sign in to comment.