def create_view(self, callback, method, request=None): """ customized create_view which is called when all routes are traversed. part of this is instantiating views with default params. in case of custom routes (@action) the custom AutoSchema is injected properly through 'initkwargs' on view. However, when decorating plain views like retrieve, this initialization logic is not running. Therefore forcefully set the schema if @extend_schema decorator was used. """ override_view = OpenApiViewExtension.get_match(callback.cls) if override_view: callback.cls = override_view.view_replacement() view = super().create_view(callback, method, request) if isinstance(view, viewsets.GenericViewSet) or isinstance( view, viewsets.ViewSet): action = getattr(view, view.action) elif isinstance(view, views.APIView): action = getattr(view, method.lower()) else: error( 'Using not supported View class. Class must be derived from APIView ' 'or any of its subclasses like GenericApiView, GenericViewSet.' ) return view # in case of @extend_schema, manually init custom schema class here due to # weakref reverse schema.view bug for multi annotations. schema = getattr(action, 'kwargs', {}).get('schema', None) if schema and inspect.isclass(schema): view.schema = schema() return view
def _map_model_field(self, model_field, direction): assert isinstance(model_field, models.Field) # to get a fully initialized serializer field we use DRF's own init logic try: field_cls, field_kwargs = serializers.ModelSerializer().build_field( field_name=model_field.name, info=get_field_info(model_field.model), model_class=model_field.model, nested_depth=0, ) field = field_cls(**field_kwargs) except: # noqa field = None # For some cases, the DRF init logic either breaks (custom field with internal type) or # the resulting field is underspecified with regards to the schema (ReadOnlyField). if field and not anyisinstance(field, [serializers.ReadOnlyField, serializers.ModelField]): return self._map_serializer_field(field, direction) elif isinstance(model_field, models.ForeignKey): return self._map_model_field(model_field.target_field, direction) elif hasattr(models, model_field.get_internal_type()): # be graceful when the model field is not explicitly mapped to a serializer internal_type = getattr(models, model_field.get_internal_type()) field_cls = serializers.ModelSerializer.serializer_field_mapping[internal_type] return self._map_serializer_field(field_cls(), direction) else: error( f'could not resolve model field "{model_field}". failed to resolve through ' f'serializer_field_mapping, get_internal_type(), or any override mechanism. ' f'defaulting to "string"' ) return build_basic_type(OpenApiTypes.STR)
def create_view(self, callback, method, request=None): """ customized create_view which is called when all routes are traversed. part of this is instantiating views with default params. in case of custom routes (@action) the custom AutoSchema is injected properly through 'initkwargs' on view. However, when decorating plain views like retrieve, this initialization logic is not running. Therefore forcefully set the schema if @extend_schema decorator was used. """ override_view = OpenApiViewExtension.get_match(callback.cls) if override_view: original_cls = callback.cls callback.cls = override_view.view_replacement() view = super().create_view(callback, method, request) # callback.cls is hosted in urlpatterns and is therefore not an ephemeral modification. # restore after view creation so potential revisits have a clean state as basis. if override_view: callback.cls = original_cls if isinstance(view, viewsets.ViewSetMixin): action = getattr(view, view.action) elif isinstance(view, views.APIView): action = getattr(view, method.lower()) else: error( 'Using not supported View class. Class must be derived from APIView ' 'or any of its subclasses like GenericApiView, GenericViewSet.' ) return view action_schema = getattr(action, 'kwargs', {}).get('schema', None) if not action_schema: # there is no method/action customized schema so we are done here. return view # action_schema is either a class or instance. when @extend_schema is used, it # is always a class to prevent the weakref reverse "schema.view" bug for multi # annotations. The bug is prevented by delaying the instantiation of the schema # class until create_view (here) and not doing it immediately in @extend_schema. action_schema_class = get_class(action_schema) view_schema_class = get_class(callback.cls.schema) if not issubclass(action_schema_class, view_schema_class): # this handles the case of having a manually set custom AutoSchema on the # view together with extend_schema. In most cases, the decorator mechanics # prevent extend_schema from having access to the view's schema class. So # extend_schema is forced to use DEFAULT_SCHEMA_CLASS as fallback base class # instead of the correct base class set in view. We remedy this chicken-egg # problem here by rearranging the class hierarchy. mro = tuple( cls for cls in action_schema_class.__mro__ if cls not in api_settings.DEFAULT_SCHEMA_CLASS.__mro__ ) + view_schema_class.__mro__ action_schema_class = type('ExtendedRearrangedSchema', mro, {}) view.schema = action_schema_class() return view
def _map_model_field(self, model_field, direction): assert isinstance(model_field, models.Field) # to get a fully initialized serializer field we use DRF's own init logic try: field_cls, field_kwargs = serializers.ModelSerializer( ).build_field( field_name=model_field.name, info=get_field_info(model_field.model), model_class=model_field.model, nested_depth=0, ) field = field_cls(**field_kwargs) except: # noqa field = None # For some cases, the DRF init logic either breaks (custom field with internal type) or # the resulting field is underspecified with regards to the schema (ReadOnlyField). if field and isinstance(field, serializers.PrimaryKeyRelatedField): # special case handling only for _resolve_path_parameters() where neither queryset nor # parent is set by build_field. patch in queryset as _map_serializer_field requires it if not field.queryset: field.queryset = model_field.related_model.objects.none() return self._map_serializer_field(field, direction) elif field and not anyisinstance( field, [serializers.ReadOnlyField, serializers.ModelField]): return self._map_serializer_field(field, direction) elif isinstance(model_field, models.ForeignKey): return self._map_model_field(model_field.target_field, direction) elif hasattr(models, 'JSONField') and isinstance( model_field, models.JSONField): # fix for DRF==3.11 with django>=3.1 as it is not yet represented in the field_mapping return build_basic_type(OpenApiTypes.OBJECT) elif hasattr(models, model_field.get_internal_type()): # be graceful when the model field is not explicitly mapped to a serializer internal_type = getattr(models, model_field.get_internal_type()) field_cls = serializers.ModelSerializer.serializer_field_mapping.get( internal_type) if not field_cls: warn( f'model field "{model_field.get_internal_type()}" has no mapping in ' f'ModelSerializer. it may be a deprecated field. defaulting to "string"' ) return build_basic_type(OpenApiTypes.STR) return self._map_serializer_field(field_cls(), direction) else: error( f'could not resolve model field "{model_field}". failed to resolve through ' f'serializer_field_mapping, get_internal_type(), or any override mechanism. ' f'defaulting to "string"') return build_basic_type(OpenApiTypes.STR)
def _map_model_field(self, field): assert isinstance(field, models.Field) drf_mapping = serializers.ModelSerializer.serializer_field_mapping if field.__class__ in drf_mapping: # use DRF native field resolution - taken from ModelSerializer.get_fields() return self._map_serializer_field(drf_mapping[field.__class__]()) elif isinstance(field, models.ForeignKey): return self._map_model_field(field.target_field) else: error( f'could not resolve model field "{field}" due to missing mapping.' 'either your field is custom and not based on a known subclasses ' 'or we missed something. let us know.' ) return build_basic_type(OpenApiTypes.STR)
def _get_serializer(self): view = self.view try: if isinstance(view, GenericAPIView): # try to circumvent queryset issues with calling get_serializer. if view has NOT # overridden get_serializer, its safe to use get_serializer_class. if view.__class__.get_serializer == GenericAPIView.get_serializer: return view.get_serializer_class()() return view.get_serializer() elif isinstance(view, APIView): # APIView does not implement the required interface, but be lenient and make # good guesses before giving up and emitting a warning. if callable(getattr(view, 'get_serializer', None)): return view.get_serializer() elif callable(getattr(view, 'get_serializer_class', None)): return view.get_serializer_class()() elif hasattr(view, 'serializer_class'): return view.serializer_class else: error( f'Unable to guess serializer for {view.__class__.__name__}. This is graceful ' f'fallback handling for APIViews. Consider using GenericAPIView as view base ' f'class, if view is under your control. ignoring view for now. ' ) else: error( 'Encountered unknown view base class. please report this issue. ignoring for now' ) except Exception as exc: error( f'Exception raised while getting serializer from {view.__class__.__name__}. Hint: ' f'Is get_serializer_class() returning None or is get_queryset() not working without ' f'a request? Ignoring the view for now. (Exception: {exc})')