Closed andreykryazhev closed 4 months ago
Hello,
so mixins do actually work and the decoration inside a mixin will get picked up properly. However your expectation does not align with python semantics.
You correctly mix in a class with a decoration, but then proceed to override that (decorated) mixed-in method with your own. Essentially hiding the decoration because now your EmployeeViewSet. list
will get used, which makes sense because that is what is used when calling list
on the instance.
Just imagine wanting to replace/override the decoration and spectacular would use some magic to resurrect that decoration that you explicitly overridden. That would be very bad.
I would recommend to rename list
to EmployeeViewSet._list
and call that instead of super()
. That way you are not overriding list but still have you generic mixed in functionality.
Not gonna comment on the meta class stuff because it is complicated and dragons are ahead.
Thank you very much for your answer.
The cause for exception was a code like this:
class EmployeeViewSet(viewsets.ModelViewSet):
...
@action(["get"], detail=False)
def profile(self, request: Request, *args: P.args, **kwargs: P.kwargs) -> Response:
...
@profile.mapping.delete
def delete_me(self, request: Request, *args: P.args, **kwargs: P.kwargs) -> Response:
...
Mapping actions somehow leaded to exception from above.
Looks like this code does not lead to any errors:
class MyCompanyBaseViewSetMcs(type):
def __new__(mcs: MyCompanyBaseViewSetMcs, name: str, bases: tuple, attrs: dict):
new_class = super().__new__(mcs, name, bases, attrs)
mcs._collect_mixin_docs(new_class)
return new_class
@classmethod
def _collect_mixin_docs(cls, decorated_class: MyCompanyBaseView) -> None:
action_attrs = cls._collect_action_attrs(decorated_class)
for action in action_attrs:
for klass in decorated_class.mro():
# if decorated class does not contain some action
# it will be picked from mixin without any issues
if klass is decorated_class and not hasattr(klass, action):
break
if klass is decorated_class or not hasattr(klass, action):
continue
# in other way try to collect schema from docs-mixin
# and put it manually to action in decorated class
action_func = getattr(klass, action)
has_schema = "schema" in getattr(action_func, "kwargs", {})
if has_schema:
original_function = getattr(decorated_class, action)
if not hasattr(original_function, "kwargs"):
original_function.kwargs = {}
original_function.kwargs["schema"] = action_func.kwargs.pop("schema")
break
@classmethod
def _collect_action_attrs(cls, decorated_class: MyCompanyBaseView) -> set[str]:
action_attrs = set(itertools.chain(*AutoSchema.method_mapping.items()))
# by unknown reason "list" is not included to "AutoSchema.method_mapping"
action_attrs |= {"list", }
if hasattr(decorated_class, "get_extra_actions"):
action_attrs |= {action.__name__ for action in decorated_class.get_extra_actions()}
# also collect mappings
mapped_actions = set()
for action in action_attrs:
if not hasattr(decorated_class, action):
continue
func = getattr(decorated_class, action)
if hasattr(func, "mapping"):
mapped_actions |= set(func.mapping.values())
action_attrs |= mapped_actions
return action_attrs
So, look like technically it works (but it require much more testing, of course). But if authors/maintainers from spectacular
team think this approach is dangerous and risky and do not approve it then of course we refuse it too.
Thank you very much for your suggested way with renaming action
to _action
. I am not sure it will cover all cases: actions, actions mappings, etc. But it is definitely what I need to check since this way is much much more safe.
We finally ended with much simpler in-box-solution (extend_schema_view
):
docs.py
from drf_spectacular.utils import OpenApiParameter, extend_schema, extend_schema_view
EmployeeViewSetDocs = extend_schema_view(
list=extend_schema(
parameters=[
OpenApiParameter(name="company_id", required=True, type=int),
OpenApiParameter(name="search", required=False, type=str),
],
request=ListEmployeeSerializer,
responses={status.HTTP_200_OK: ListEmployeeSerializer},
),
create=extend_schema(
request=CreateEmployeeSerializer,
responses={status.HTTP_200_OK: CreateEmployeeSerializer},
parameters=[CompanyIdParam],
),
...
)
And we using it this way:
views.py
@EmployeeViewSetDocs
class EmployeeViewSet(viewsets.ModelViewSet, MyCompanyBaseView):
queryset = Employee.objects.all()
def create(self, request: Request, *args: P.args, **kwargs: P.kwargs) -> Response:
...
def list(self, request: Request, *args: P.args, **kwargs: P.kwargs) -> Response:
...
As I can see it greatly works with actions too (except action mappings
but it is not a big deal).
Hello, I would like to add documentation to some mixin classes in order to clean main views.py a little bit.
In other words, instead of this:
I would like to try this:
This does not work by default and I tried to use metaclass to automatically pass schema from mixin (
EmployeeViewSetDocsMixin
) to targeted class (EmployeeViewSet
). Here is what I did:After that
EmployeeViewSet
looks so:But in this case I get the following error when start swagger page:
The issue that
drf_spectacular
expects instance but not a class. But as I can see inextend_schema
the very class in needed to pass: https://github.com/tfranzel/drf-spectacular/blob/cc916372a0e3fafc3af48f94dd1a985f5fc466e9/drf_spectacular/utils.py#L565.Is it possible to resolve (or bypass this check somehow)? Just in case I tried temporary:
Of course it is just for checking reason, but as I can see this change finally allows me to see documentation on swagger-page from mixins. But is there a way to do it right way?
Versions:
drf-spectacular = "0.26.2"
django = "4.2"
djangorestframework = "3.14.0"