axnsan12 / drf-yasg

Automated generation of real Swagger/OpenAPI 2.0 schemas from Django REST Framework code.
https://drf-yasg.readthedocs.io/en/stable/
Other
3.42k stars 439 forks source link

Issue with swagger_auto_schema and inherited class methods #674

Open tonial opened 3 years ago

tonial commented 3 years ago

Hello.

I'm having an issue that seems to be related with CPython memory allocation. I am wondering if there's a way to work around my issue with drf_yasg.

I have a series of APIViews that all have the same template :

class MyClass(rest_framework.views.APIView):
    @swagger_auto_schema(
        operation_id='graph_my_class',
        request_body=GraphFiltersSerializer,  # always the same
        responses={200: MyClassResponseSerializer(many=True)},  # may differs for each class
    )
    def post(self, request):
        filters = deserialize_filters(request)
        graph_data = self.graph_as_json(filters)
        serialized_data = MyClassResponseSerializer(graph_data, many=True).data
        return Response(serialized_data)

    def graph_as_json(self, filters):
        return something

Since it's a bit painfull to always duplicate the post/swagger_auto_schema within each class. I tried so create a common mixin :

class GraphApiView(rest_framework.views.APIView):
    serializer = None

    @classmethod
    def get_name(cls):
        name_pattern = re.compile(r'(?<!^)(?=[A-Z])')
        return f"graph_{name_pattern.sub('_', cls.__name__).lower()}"

    @classmethod
    def as_view(cls, **kwargs):
        return swagger_auto_schema(
            method='post',
            operation_id=cls.get_name(),
            request_body=GraphFiltersSerializer,
            responses={200: cls.serializer(many=True)},  # pylint: disable=not-callable
        )(super().as_view(**kwargs))

    def post(self, request):
        filters = deserialize_filters(request)
        graph_data = self.graph_as_json(filters)
        serialized_data = self.serializer(graph_data, many=True).data  # pylint: disable=not-callable
        return Response(serialized_data)

class MyClass(GraphApiView):
    serializer = MyClassResponseSerializer

    def graph_as_json(self, filters):
        return something

Sadly all endpoints have the same name and schema in the swagger.yml file generated with generate_swagger command. After digging in the code, I found that for all classes, we have the same object in memory for callback.cls.post (with callback been the objects that we can find here and here).

It's the same object with the same memory allocation, so whenever the code set the _swagger_auto_schema attribute to on callback.cls.post, it's set on all callbacks.cls.post. So all endpoints will have the swagger schema of the last class found in the url pattern.

I found a work around :

class GraphApiView(rest_framework.views.APIView):
    serializer = None

    @classmethod
    def get_name(cls):
        [unchanged]

    @classmethod
    def as_view(cls, **kwargs):
        [unchanged]

    def post(self, request):
        raise NotImplementedError

    def _post(self, request):
        filters = deserialize_filters(request)
        graph_data = self.graph_as_json(filters)
        serialized_data = self.serializer(graph_data, many=True).data  # pylint: disable=not-callable
        return Response(serialized_data)

class MyClass(GraphApiView):
    serializer = MyClassResponseSerializer

    def post(self, request):
        return self._post(request)

    def graph_as_json(self, filters):
        return something

I think it's possible to fix this issue by changing where drf_yasg sets the _swagger_auto_schema attribute. I'm interested in submitting a PR (or at least to try)

My questions are :

TmLev commented 3 years ago

I think the project is being abandoned again, maybe drf-spectacular doesn't have this problem.

tonial commented 3 years ago

Thank's for your answer. I'll look into it