📜 ⬆️ ⬇️

Over Engineering in Documenting ViewSets Django REST Framework

It happens in our lives, dear colleagues, that you want to do as easier, but it turns out like a beginner. And, interestingly, there are quite a few powerful tools that offer a simple solution in exchange for a soul. I mean, the price of abstraction is disproportionate to the beauty of its use. For me, the example of such an unequal exchange was the Django Rest Framework 3.4.0, its ViewSets mechanism and the need to display detailed documentation on the API being developed.

Let's start with a simple one: my favorite format for working with DRF is to write only APIView descendants. On the one hand, this is a repeating code, and on the other, it is quite a concise solution with a predictable and controllable user case. First of all, with a 95% probability, we will not hang several serializers on one endpoint. Second, we can fine tune the URL binding. But, over time, you begin to think: have I done everything correctly? Maybe it's time to move away from the idea of ​​conservatism proven over the years REST? Moreover, DRF has a fairly good abstraction layer: ViewSets .

The idea of ​​ViewSets is simple: we have a serviced model, and we don’t need to write our endpoints or describe them as separate classes. A class that registers views, binds urls, etc. is enough. Those. This is a lot of templates packed in a box tied with a blue ribbon. The task was relatively standard:
')
1. There is a custom user profile.
2. It has additional fields.
3. During registration, we use REST and manually determine which fields are required and which are not (override model fields at the DRF level).
4. Login is generated automatically.
5. The profile has a connection with an invite, and an invite is associated with the organization that invited this invite.

After some thought, it was decided to make 2 or 3 serializer. Absolutely there is a separate serializer on create. Separate - on the view. Perhaps, but not the fact that the third is needed - on update (change). The classic REST application scheme would look like this:

serializers.py

class UserCreateSerializer(serializers.ModelSerializer): pass class UserViewSerializer(serializers.ModelSerializer): pass class UserUpdateSerializer(serializers.ModelSerializer): pass 


views.py

 class UserCreateView(APIView): pass class UserDetailsView(APIView): pass class UserUpdateView(APIView): pass 


After a little refactoring, we can get one APIView:

views.py

 class UserApiView(APIView): def get(self, request, *args, **kwargs): return self.__list_view(request) if 'pk' not in self.kwargs else self.__detail_view(request) def post(self, request, *args, **kwargs): return self.__create_view(request) if 'pk' not in self.kwargs else self.__update_view(request) 


As you can see, there is no special need for ViewSet. The trace of the request is exactly one line, but the functions get, post, put, and others like them are available to us. In addition, if we suddenly do not like the result, we can always return to the format of three separate classes of endpoints. This method has another advantage: when you install an application for automatic documentation ( Swagger or DRF Docs ), you get a predictable conclusion: either three endpoints or one endpoint with the three methods described.

However, let's move on to the ViewSet abstraction:

views.py

 class UserViewSet(mixins.ListModelMixin, mixins.CreateModelMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet): serializer_classes = { 'list': UserViewSerializer, 'get': UserViewSerializer, 'create': UserCreateSerializer, 'update': UserUpdateSerializer, 'set_password': UserEditSerializer, #   ? 'activate': UserEditSerializer } def list(self, request, *args, **kwargs): serializer_class = self.serializer_classes['list'] pass def create(self, request, *args, **kwargs): serializer_class = self.serializer_classes['create'] 


First, the abundance of code is striking. It is much more than in the version with one endpoint and three methods. Secondly, we see a rather pleasant declarativeness of the code, which, by the way, we will soon have a knife on the pope.

So our problem is that Swagger and DRF Docs will not work correctly with this viewpoint.

I didn’t dig into Swagger code, but I think I’m not going to sin if I say that it gets endpoint methods like this:

1. Get urlpattern
2. Endpoint = urlpattern.callback
3. Methods = endpoint.available_methods

Pay attention to the fact that the callback is requested without creating an instance, or by calling the as_view method, which receives the request argument. Let's test our theory:

views.py

 class UserViewSet(mixins.ListModelMixin, mixins.CreateModelMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet): serializer_classes = { 'list': UserViewSerializer, 'get': UserViewSerializer, 'create': UserCreateSerializer, 'update': UserUpdateSerializer, 'set_password': UserEditSerializer, #   ? 'activate': UserEditSerializer } def get_serializer_class(self): logger.warn(self.request) logger.warn(self.actions) return UserViewSerializer # Actions here... 


We get a 500 error with the information that the UserViewSet object does not have a request attribute. If we remove the problematic line, we get the second error: this object does not have the actions attribute. This happens because ViewSetMixin exposes actions when there is a request, although it would be more logical to make a list of available actions in the form of classproperty (after all, when inheriting miksin, standard actions are fixed by the name and conditions of triggering).

But now we are not interested in what would happen if the grandmother had mudiki (Dahl’s dictionary, if I'm not mistaken). We have an interface that cannot be documented. Here is the grief!

I did not manage to document the Swagger interface. The problem crutch lies in the very get_serializer_class () method that you saw in the previous snippet. Both Swagger and DRF Docs use it to get the current serializer. We can assume that our code should look like this:

views.py

 class UserViewSet(mixins.ListModelMixin, mixins.CreateModelMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet): serializer_classes = { 'list': UserViewSerializer, 'get': UserViewSerializer, 'create': UserCreateSerializer, 'update': UserUpdateSerializer, 'set_password': UserEditSerializer, #   ? 'activate': UserEditSerializer } def get_serializer_class(self): return self.serializer_classes.get(self.action, UserViewSerializer) # Actions here... 


But we remember that at the time of triggering get_serializer_class, self.action does not exist as an attribute. This causes a 500 error and does not allow the use of this case. Having studied both solutions (Swagger, DRF Docs), I stopped at the last one. And then I got another problem:

- today is July 27, 2016, and the DRF Docs code from the wizard branch is different from the DRF Docs code, which is placed via pypi or by downloading the GIT repository.

I don’t know if this is a glitch, but, apparently, git gives the code marked as release 0.0.11, and the developers had the audacity to update the wizard without release. Fail!

The problem is being solved with a crutch - replacing api_endpoint.py in the package. You understand perfectly well that this is not an option. Here I have two ways of developing the code: either I will wait until the developers roll out a new release, or return to the inheritance option from APIView. Today there is no time and strength to do it. Understanding the code of this file (which is working in the wizard), I came across two interesting fragments. Here is the first one:

api_endpoint.py

  def __get_serializer_class__(self): if hasattr(self.callback.cls, 'serializer_class'): return self.callback.cls.serializer_class if hasattr(self.callback.cls, 'get_serializer_class'): return self.callback.cls.get_serializer_class(self.pattern.callback.cls()) 


The fact is that our implementation of ViewSet will always contain property serializer_class = None . It would be logical to swap checks in order to prioritize the dynamic shift of the serializer.

The second point:

api_endpoint.py

  view_methods = [force_str(m).upper() for m in self.callback.cls.http_method_names if hasattr(self.callback.cls, m)] return viewset_methods + view_methods 


Now, if you stick a stopper between these two lines and try to get self.callback.actions, then you will get the dictionary that we lack for work. Of course, it was possible to connect to the development and add a separate logic to document the actions ... but we don’t need it for free. Now I am waiting for the DRF Docs developers to accept the issue with the first problem (serializer_class = None) and hope for a quick release. If it doesn't happen, go back to the APIView version. As for the method of getting the serializer, it looks like this:

views.py

 class UserViewSet(mixins.ListModelMixin, mixins.CreateModelMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet): serializer_classes = { 'list': UserViewSerializer, 'get': UserViewSerializer, 'create': UserCreateSerializer, 'update': UserUpdateSerializer, 'set_password': UserEditSerializer, #   ? 'activate': UserEditSerializer } def get_serializer_class(self): if not hasattr(self, 'action'): action = 'create' if 'POST' in self.allowed_methods else 'list' else: action = self.action return self.serializer_classes.get(action, UserViewSerializer) # Actions here... 


It remains to hope that the update method will not create problems when adding. Another small remark: I had to add the post method:

views.py

 class UserViewSet(mixins.ListModelMixin, mixins.CreateModelMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet): #... def post(self, *args, **kwargs): return super().post(*args, **kwargs) # Actions here... 


Without it, DRF Docs could not get allowed_methods, and Swagger also had problems.

So, dear colleagues, when addressing the high level of framework abstraction, I ran into an architectural problem. It boils down to a simple conclusion: "I am guilty myself." Although, the question is, of course, controversial, because ViewSets is a convenient and official tool. However, it can be seen with the naked eye that the issue of registering actions in a class is not worked out. Hence the reluctance of documentary developers to handle actions normally. The outcome of the situation is simple: today it is easier to use separate API Views than the presentation templates for the model. At least, in most well-known REST engines or frameworks that can create REST, you most likely will not see such abstractions. And a very big question: are they needed at all?

Source: https://habr.com/ru/post/306538/


All Articles