def map_serializer(self, auto_schema, method: str): """ custom handling for @extend_schema's injection of PolymorphicProxySerializer """ serializer = self.target sub_components = [] for sub_serializer in serializer.serializers: sub_serializer = force_instance(sub_serializer) resolved_sub_serializer = auto_schema.resolve_serializer( method, sub_serializer) try: discriminator_field = sub_serializer.fields[ serializer.resource_type_field_name] resource_type = discriminator_field.to_representation(None) except: # noqa: E722 warn( f'sub-serializer {resolved_sub_serializer.name} of {serializer.component_name} ' f'must contain the discriminator field "{serializer.resource_type_field_name}". ' f'defaulting to sub-serializer name, but schema will likely not match the API.' ) resource_type = resolved_sub_serializer.name sub_components.append((resource_type, resolved_sub_serializer.ref)) return { 'oneOf': [ref for _, ref in sub_components], 'discriminator': { 'propertyName': serializer.resource_type_field_name, 'mapping': { resource_type: ref['$ref'] for resource_type, ref in sub_components } } }
def map_serializer(self, auto_schema, direction): sub_components = [] serializer = self.target for sub_model in serializer.model_serializer_mapping: sub_serializer = serializer._get_serializer_from_model_or_instance( sub_model) resource_type = serializer.to_resource_type(sub_model) ref = auto_schema.resolve_serializer(sub_serializer, direction).ref sub_components.append((resource_type, ref)) if not resource_type: warn( f'discriminator mapping key is empty for {sub_serializer.__class__}. ' f'this might lead to code generation issues.') return { 'oneOf': [ref for _, ref in sub_components], 'discriminator': { 'propertyName': serializer.resource_type_field_name, 'mapping': { resource_type: ref['$ref'] for resource_type, ref in sub_components }, } }
def _get_response_bodies(self): response_serializers = self.get_response_serializers() if is_serializer(response_serializers) or is_basic_type(response_serializers): if self.method == 'DELETE': return {'204': {'description': _('No response body')}} if self.method == 'POST' and getattr(self.view, 'action', None) == 'create': return {'201': self._get_response_for_code(response_serializers, '201')} return {'200': self._get_response_for_code(response_serializers, '200')} elif isinstance(response_serializers, dict): # custom handling for overriding default return codes with @extend_schema responses = {} for code, serializer in response_serializers.items(): if isinstance(code, tuple): code, media_types = str(code[0]), code[1:] else: code, media_types = str(code), None content_response = self._get_response_for_code(serializer, code, media_types) if code in responses: responses[code]['content'].update(content_response['content']) else: responses[code] = content_response return responses else: warn( f'could not resolve "{response_serializers}" for {self.method} {self.path}. ' f'Expected either a serializer or some supported override mechanism. ' f'defaulting to generic free-form object.' ) schema = build_basic_type(OpenApiTypes.OBJECT) schema['description'] = _('Unspecified response body') return {'200': self._get_response_for_code(schema, '200')}
def create_view(self, callback, method, request=None): """ customized create_view which is called when all routes are traversed. part of this is instatiating 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. """ 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: warn( 'Using not supported View class. Class must be derived from APIView ' 'or any of its subclasses like GenericApiView, GenericViewSet.' ) return view if hasattr(action, 'kwargs') and 'schema' in action.kwargs: # might already be properly set in case of @action but overwrite for all cases view.schema = action.kwargs['schema'] return view
def get_schema_operation_parameters(self, auto_schema, *args, **kwargs): if issubclass(self.target_class, SpectacularDjangoFilterBackendMixin): warn( 'DEPRECATED - Spectacular\'s DjangoFilterBackend is superseded by extension. you ' 'can simply restore this to the original class, extensions will take care of the ' 'rest.') model = get_view_model(auto_schema.view) if not model: return [] filterset_class = self.target.get_filterset_class( auto_schema.view, model.objects.none()) if not filterset_class: return [] parameters = [] for field_name, field in filterset_class.base_filters.items(): schema, description, enum = (self.resolve_filter_field( auto_schema, model, filterset_class, field)) parameters.append( build_parameter_type( name=field_name, required=field.extra['required'], location=OpenApiParameter.QUERY, description=description, schema=schema, enum=enum, )) return parameters
def _get_serializer_field_meta(self, field): if not isinstance(field, serializers.Field): warn( f'unable to extract field metadata from field "{field}" because it appears ' f'to be neither a Field nor a Serializer. Proper handling may require an ' f'Extension or @extend_serializer_field. Skipping metadata extraction. ' ) return {} meta = {} if field.read_only: meta['readOnly'] = True if field.write_only: meta['writeOnly'] = True if field.allow_null: meta['nullable'] = True if field.default is not None and field.default != empty and not callable( field.default): default = field.to_representation(field.default) if isinstance(default, set): default = list(default) meta['default'] = default if field.help_text: meta['description'] = str(field.help_text) return meta
def parse(self, request, public): """ Iterate endpoints generating per method path operations. """ result = {} self._initialise_endpoints() for path, path_regex, method, view in self._get_paths_and_endpoints( None if public else request): if not self.has_view_permissions(path, method, view): continue # mocked request to allow certain operations in get_queryset and get_serializer[_class] # without exceptions being raised due to no request. if not request: request = spectacular_settings.GET_MOCK_REQUEST( method, path, view, request) view.request = request if view.versioning_class and not is_versioning_supported( view.versioning_class): warn( f'using unsupported versioning class "{view.versioning_class}". view will be ' f'processed as unversioned view.') elif view.versioning_class: version = ( self.api_version # generator was explicitly versioned or getattr(request, 'version', None) # incoming request was versioned or view.versioning_class.default_version # fallback ) if not version: continue path = modify_for_versioning(self.inspector.patterns, method, path, view, version) if not operation_matches_version(view, version): continue assert isinstance(view.schema, AutoSchema), ( 'Incompatible AutoSchema used on View. Is DRF\'s DEFAULT_SCHEMA_CLASS ' 'pointing to "drf_spectacular.openapi.AutoSchema" or any other drf-spectacular ' 'compatible AutoSchema?') operation = view.schema.get_operation(path, path_regex, method, self.registry) # operation was manually removed via @extend_schema if not operation: continue # Normalise path for any provided mount url. if path.startswith('/'): path = path[1:] path = urljoin(self.url or '/', path) if spectacular_settings.CAMELIZE_NAMES: path, operation = camelize_operation(path, operation) result.setdefault(path, {}) result[path][method.lower()] = operation return result
def get_auth(self, path, method): """ Obtains authentication classes and permissions from view. If authentication is known, resolve security requirement for endpoint and security definition for the component section. For custom authentication subclass ``OpenApiAuthenticationExtension``. """ auths = [] for authenticator in self.view.get_authenticators(): scheme = OpenApiAuthenticationExtension.get_match(authenticator) if not scheme: warn( f'could not resolve authenticator {authenticator.__class__}. There ' f'was no OpenApiAuthenticationExtension registered for that class. ' f'Try creating one by subclassing it. Ignoring for now.') continue auths.append(scheme.get_security_requirement(self.view)) component = ResolvedComponent( name=scheme.name, type=ResolvedComponent.SECURITY_SCHEMA, object=authenticator.__class__, schema=scheme.get_security_definition(self.view)) if component not in self.registry: self.registry.register(component) perms = [p.__class__ for p in self.view.get_permissions()] if permissions.AllowAny in perms: auths.append({}) elif permissions.IsAuthenticatedOrReadOnly in perms and method not in ( 'PUT', 'PATCH', 'POST'): auths.append({}) return auths
def _get_response_bodies(self, path, method): response_serializers = self.get_response_serializers(path, method) if is_serializer(response_serializers) or is_basic_type( response_serializers): if method == 'DELETE': return {'204': {'description': 'No response body'}} return { '200': self._get_response_for_code(path, method, response_serializers) } elif isinstance(response_serializers, dict): # custom handling for overriding default return codes with @extend_schema return { str(code): self._get_response_for_code(path, method, serializer) for code, serializer in response_serializers.items() } else: warn( f'could not resolve "{response_serializers}" for {method} {path}. ' f'Expected either a serializer or some supported override mechanism. ' f'defaulting to generic free-form object.') schema = build_basic_type(OpenApiTypes.OBJECT) schema['description'] = 'Unspecified response body' return {'200': self._get_response_for_code(path, method, schema)}
def _process_override_parameters(self): result = [] for parameter in self.get_override_parameters(): if isinstance(parameter, OpenApiParameter): if is_basic_type(parameter.type): schema = build_basic_type(parameter.type) elif is_serializer(parameter.type): schema = self.resolve_serializer(parameter.type, direction=None).ref else: schema = parameter.type result.append(build_parameter_type( name=parameter.name, schema=schema, location=parameter.location, required=parameter.required, description=parameter.description, enum=parameter.enum, deprecated=parameter.deprecated, )) elif is_serializer(parameter): # explode serializer into separate parameters. defaults to QUERY location mapped = self._map_serializer(parameter, direction=None) for property_name, property_schema in mapped['properties'].items(): result.append(build_parameter_type( name=property_name, schema=property_schema, location=OpenApiParameter.QUERY, required=property_name in mapped.get('required', []) )) else: warn(f'could not resolve parameter annotation {parameter}. skipping.') return result
def get_schema_operation_parameters(self, auto_schema, *args, **kwargs): if issubclass(self.target_class, SpectacularDjangoFilterBackendMixin): warn( 'DEPRECATED - Spectacular\'s DjangoFilterBackend is superseded by extension. you ' 'can simply restore this to the original class, extensions will take care of the ' 'rest.') model = get_view_model(auto_schema.view) if not model: return [] filterset_class = self.target.get_filterset_class( auto_schema.view, model.objects.none()) if not filterset_class: return [] parameters = [] for field_name, field in filterset_class.base_filters.items(): path = field.field_name.split('__') model_field = follow_field_source(model, path) parameters.append( build_parameter_type( name=field_name, required=field.extra['required'], location=OpenApiParameter.QUERY, description=field.label if field.label is not None else field_name, schema=auto_schema._map_model_field(model_field, direction=None), enum=[c for c, _ in field.extra.get('choices', [])], )) return parameters
def _get_response_for_code(self, serializer): serializer = force_instance(serializer) if not serializer: return {'description': _('No response body')} elif isinstance(serializer, serializers.ListSerializer): if is_serializer(serializer.child): schema = self.resolve_serializer(serializer.child, 'response').ref else: schema = self._map_serializer_field(serializer.child, 'response') elif is_serializer(serializer): component = self.resolve_serializer(serializer, 'response') if not component.schema: return {'description': _('No response body')} schema = component.ref elif is_basic_type(serializer): schema = build_basic_type(serializer) elif isinstance(serializer, dict): # bypass processing and use given schema directly schema = serializer else: warn( f'could not resolve "{serializer}" for {self.method} {self.path}. Expected either ' f'a serializer or some supported override mechanism. defaulting to ' f'generic free-form object.') schema = build_basic_type(OpenApiTypes.OBJECT) schema['description'] = _('Unspecified response body') if self._is_list_view(serializer) and not get_override( serializer, 'many') is False: schema = build_array_type(schema) paginator = self._get_paginator() if paginator and is_serializer(serializer): paginated_name = f'Paginated{self._get_serializer_name(serializer, "response")}List' component = ResolvedComponent( name=paginated_name, type=ResolvedComponent.SCHEMA, schema=paginator.get_paginated_response_schema(schema), object=paginated_name, ) self.registry.register(component) schema = component.ref elif paginator: schema = paginator.get_paginated_response_schema(schema) return { 'content': { mt: { 'schema': schema } for mt in self.map_renderers('media_type') }, # Description is required by spec, but descriptions for each response code don't really # fit into our model. Description is therefore put into the higher level slots. # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#responseObject 'description': '' }
def _get_request_body(self): # only unsafe methods can have a body if self.method not in ('PUT', 'PATCH', 'POST'): return None serializer = force_instance(self.get_request_serializer()) request_body_required = False if is_list_serializer(serializer): if is_serializer(serializer.child): component = self.resolve_serializer(serializer.child, 'request') schema = build_array_type(component.ref) else: schema = build_array_type( self._map_serializer_field(serializer.child, 'request')) request_body_required = True elif is_serializer(serializer): if self.method == 'PATCH': serializer.partial = True component = self.resolve_serializer(serializer, 'request') if not component.schema: # serializer is empty so skip content enumeration return None schema = component.ref # request body is only required if any required property is not read-only readonly_props = [ p for p, s in component.schema.get('properties', {}).items() if s.get('readOnly') ] required_props = component.schema.get('required', []) request_body_required = any(req not in readonly_props for req in required_props) elif is_basic_type(serializer): schema = build_basic_type(serializer) if not schema: return None else: warn( f'could not resolve request body for {self.method} {self.path}. defaulting to generic ' 'free-form object. (maybe annotate a Serializer class?)') schema = build_object_type( additionalProperties={}, description='Unspecified request body', ) request_body = { 'content': { media_type: build_media_type_object( schema, self._get_examples(serializer, 'request', media_type)) for media_type in self.map_parsers() } } if request_body_required: request_body['required'] = request_body_required return request_body
def get_security_definition(self, auto_schema): from rest_framework_simplejwt.settings import api_settings from drf_spectacular.plumbing import warn if len(api_settings.AUTH_HEADER_TYPES) > 1: warn( f'OpenAPI3 can only have one "bearerFormat". JWT Settings specify ' f'{api_settings.AUTH_HEADER_TYPES}. Using the first one.') return { 'type': 'http', 'scheme': 'bearer', 'bearerFormat': api_settings.AUTH_HEADER_TYPES[0], }
def _resolve_path_parameters(self, variables): model = getattr(getattr(self.view, 'queryset', None), 'model', None) parameters = [] for variable in variables: schema = build_basic_type(OpenApiTypes.STR) description = '' resolved_parameter = resolve_regex_path_parameter( self.path_regex, variable, self.map_renderers('format'), ) if resolved_parameter: schema = resolved_parameter['schema'] elif not model: warn( f'could not derive type of path parameter "{variable}" because because it ' f'is untyped and {self.view.__class__} has no queryset. consider adding a ' f'type to the path (e.g. <int:{variable}>) or annotating the parameter ' f'type with @extend_schema. defaulting to "string".') else: try: model_field = model._meta.get_field(variable) schema = self._map_model_field(model_field, direction=None) # strip irrelevant meta data irrelevant_field_meta = [ 'readOnly', 'writeOnly', 'nullable', 'default' ] schema = { k: v for k, v in schema.items() if k not in irrelevant_field_meta } if 'description' not in schema and model_field.primary_key: description = get_pk_description(model, model_field) except django_exceptions.FieldDoesNotExist: warn( f'could not derive type of path parameter "{variable}" because ' f'model "{model}" did contain no such field. consider annotating ' f'parameter with @extend_schema. defaulting to "string".' ) parameters.append( build_parameter_type(name=variable, location=OpenApiParameter.PATH, description=description, schema=schema)) return parameters
def _map_response_type_hint(self, method): hint = get_override(method, 'field') or typing.get_type_hints(method).get('return') if is_serializer(hint) or is_field(hint): return self._map_serializer_field(force_instance(hint), 'response') try: return resolve_type_hint(hint) except UnableToProceedError: warn( f'unable to resolve type hint for function "{method.__name__}". consider ' f'using a type hint or @extend_schema_field. defaulting to string.' ) return build_basic_type(OpenApiTypes.STR)
def _get_serializer(self, path, method): view = self.view try: # 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() except Exception as exc: warn( 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})') return None
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: warn( 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_response_for_code(self, path, method, serializer): serializer = force_instance(serializer) if not serializer: return {'description': 'No response body'} elif isinstance(serializer, serializers.ListSerializer): schema = self.resolve_serializer(method, serializer.child).ref elif is_serializer(serializer): component = self.resolve_serializer(method, serializer) if not component: return {'description': 'No response body'} schema = component.ref elif is_basic_type(serializer): schema = build_basic_type(serializer) elif isinstance(serializer, dict): # bypass processing and use given schema directly schema = serializer else: warn( f'could not resolve "{serializer}" for {method} {path}. Expected either ' f'a serializer or some supported override mechanism. defaulting to ' f'generic free-form object.') schema = build_basic_type(OpenApiTypes.OBJECT) schema['description'] = 'Unspecified response body' if isinstance(serializer, serializers.ListSerializer) or is_list_view( path, method, self.view): # TODO i fear is_list_view is not covering all the cases schema = build_array_type(schema) paginator = self._get_paginator() if paginator: schema = paginator.get_paginated_response_schema(schema) return { 'content': { mt: { 'schema': schema } for mt in self.map_renderers(path, method) }, # Description is required by spec, but descriptions for each response code don't really # fit into our model. Description is therefore put into the higher level slots. # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#responseObject 'description': '' }
def _map_type_hint(self, method): hint = getattr(method, '_spectacular_annotation', None) or typing.get_type_hints(method).get('return') if is_serializer(hint) or is_field(hint): return self._map_serializer_field(force_instance(hint)) elif is_basic_type(hint): return build_basic_type(hint) elif getattr(hint, '__origin__', None) is typing.Union: if type(None) == hint.__args__[1] and len(hint.__args__) == 2: schema = build_basic_type(hint.__args__[0]) schema['nullable'] = True return schema else: warn(f'type hint {hint} not supported yet. defaulting to "string"') return build_basic_type(OpenApiTypes.STR) else: warn(f'type hint for function "{method.__name__}" is unknown. defaulting to string.') return build_basic_type(OpenApiTypes.STR)
def _resolve_path_parameters(self, variables): model = getattr(getattr(self.view, 'queryset', None), 'model', None) parameters = [] for idx, variable in enumerate(variables): schema = build_basic_type(OpenApiTypes.STR) description = '' required = True resolved_parameter = resolve_regex_path_parameter(self.path_regex, variable) if resolved_parameter: schema, required = resolved_parameter['schema'], resolved_parameter['required'] elif not model: warn( f'could not derive type of path parameter "{variable}" because ' f'{self.view.__class__} has no queryset. consider annotating the ' f'parameter type with @extend_schema. defaulting to "string".' ) else: try: model_field = model._meta.get_field(variable) schema = self._map_model_field(model_field, direction=None) # strip irrelevant meta data irrelevant_field_meta = ['readOnly', 'writeOnly', 'nullable', 'default'] schema = {k: v for k, v in schema.items() if k not in irrelevant_field_meta} if 'description' not in schema and model_field.primary_key: description = get_pk_description(model, model_field) except django_exceptions.FieldDoesNotExist: warn( f'could not derive type of path parameter "{variable}" because ' f'model "{model}" did contain no such field. consider annotating ' f'parameter with @extend_schema. defaulting to "string".' ) parameters.append({ "name": variable, "in": "path", "required": required, "description": description, 'schema': schema, }) return parameters
def _get_request_body(self): # only unsafe methods can have a body if self.method not in ('PUT', 'PATCH', 'POST'): return None serializer = force_instance(self.get_request_serializer()) request_body_required = False if isinstance(serializer, serializers.ListSerializer): component = self.resolve_serializer(serializer.child, 'request') schema = build_array_type(component.ref) request_body_required = True elif is_serializer(serializer): component = self.resolve_serializer(serializer, 'request') if not component: # serializer is empty so skip content enumeration return None schema = component.ref if component.schema.get('required', []): request_body_required = True elif is_basic_type(serializer): schema = build_basic_type(serializer) if not schema: return None else: warn( f'could not resolve request body for {self.method} {self.path}. defaulting to generic ' 'free-form object. (maybe annotate a Serializer class?)' ) schema = { 'type': 'object', 'additionalProperties': {}, # https://github.com/swagger-api/swagger-codegen/issues/1318 'description': 'Unspecified request body', } request_body = { 'content': { request_media_types: {'schema': schema} for request_media_types in self.map_parsers() } } if request_body_required: request_body['required'] = request_body_required return request_body
def parse(self, request, public): """ Iterate endpoints generating per method path operations. """ result = {} self._initialise_endpoints() for path, path_regex, method, view in self._get_paths_and_endpoints( None if public else request): if not self.has_view_permissions(path, method, view): continue if view.versioning_class and not is_versioning_supported( view.versioning_class): warn( f'using unsupported versioning class "{view.versioning_class}". view will be ' f'processed as unversioned view.') elif view.versioning_class: version = ( self.api_version # generator was explicitly versioned or getattr(request, 'version', None) # incoming request was versioned or view.versioning_class.default_version # fallback ) path = modify_for_versioning(self.inspector.patterns, method, path, view, version) if not version or not operation_matches_version(view, version): continue # beware that every access to schema yields a fresh object (descriptor pattern) operation = view.schema.get_operation(path, path_regex, method, self.registry) # operation was manually removed via @extend_schema if not operation: continue # Normalise path for any provided mount url. if path.startswith('/'): path = path[1:] path = urljoin(self.url or '/', path) result.setdefault(path, {}) result[path][method.lower()] = operation return result
def _resolve_path_parameters(self, variables): parameters = [] for variable in variables: schema = build_basic_type(OpenApiTypes.STR) description = '' resolved_parameter = resolve_regex_path_parameter( self.path_regex, variable, self.map_renderers('format'), ) if resolved_parameter: schema = resolved_parameter['schema'] elif get_view_model(self.view) is None: warn( f'could not derive type of path parameter "{variable}" because because it ' f'is untyped and obtaining queryset from {self.view.__class__} failed. ' f'consider adding a type to the path (e.g. <int:{variable}>) or annotating ' f'the parameter type with @extend_schema. defaulting to "string".' ) else: try: model = get_view_model(self.view) model_field = model._meta.get_field(variable) schema = self._map_model_field(model_field, direction=None) if 'description' not in schema and model_field.primary_key: description = get_pk_description(model, model_field) except django_exceptions.FieldDoesNotExist: warn( f'could not derive type of path parameter "{variable}" because ' f'model "{model}" did contain no such field. consider annotating ' f'parameter with @extend_schema. defaulting to "string".' ) parameters.append( build_parameter_type(name=variable, location=OpenApiParameter.PATH, description=description, schema=schema)) return parameters
def _process_override_parameters(self): result = {} for parameter in self.get_override_parameters(): if isinstance(parameter, OpenApiParameter): if is_basic_type(parameter.type): schema = build_basic_type(parameter.type) elif is_serializer(parameter.type): schema = self.resolve_serializer(parameter.type, 'request').ref else: schema = parameter.type if parameter.exclude: result[parameter.name, parameter.location] = None else: result[parameter.name, parameter.location] = build_parameter_type( name=parameter.name, schema=schema, location=parameter.location, required=parameter.required, description=parameter.description, enum=parameter.enum, deprecated=parameter.deprecated, style=parameter.style, explode=parameter.explode, default=parameter.default, examples=build_examples_list(parameter.examples), ) elif is_serializer(parameter): # explode serializer into separate parameters. defaults to QUERY location mapped = self._map_serializer(parameter, 'request') for property_name, property_schema in mapped['properties'].items(): result[property_name, OpenApiParameter.QUERY] = build_parameter_type( name=property_name, schema=property_schema, description=property_schema.pop('description', None), location=OpenApiParameter.QUERY, required=property_name in mapped.get('required', []), ) else: warn(f'could not resolve parameter annotation {parameter}. skipping.') return result
def _map_response_type_hint(self, method): hint = get_override(method, 'field') or typing.get_type_hints(method).get('return') if is_serializer(hint) or is_field(hint): return self._map_serializer_field(force_instance(hint), 'response') elif is_basic_type(hint, allow_none=False): return build_basic_type(hint) elif getattr(hint, '__origin__', None) is typing.Union: if type(None) == hint.__args__[1] and len(hint.__args__) == 2: schema = build_basic_type(hint.__args__[0]) schema['nullable'] = True return schema else: warn(f'type hint {hint} not supported yet. defaulting to "string"') return build_basic_type(OpenApiTypes.STR) else: warn( f'type hint for function "{method.__name__}" is unknown. consider using ' f'a type hint or @extend_schema_field. defaulting to string.' ) return build_basic_type(OpenApiTypes.STR)
def get_schema_operation_parameters(self, auto_schema, *args, **kwargs): if issubclass(self.target_class, SpectacularDjangoFilterBackendMixin): warn( 'DEPRECATED - Spectacular\'s DjangoFilterBackend is superseded by extension. you ' 'can simply restore this to the original class, extensions will take care of the ' 'rest.') model = get_view_model(auto_schema.view) if not model: return [] filterset_class = self.target.get_filterset_class( auto_schema.view, model.objects.none()) if not filterset_class: return [] return [ self.resolve_filter_field(auto_schema, model, filterset_class, field_name, filter_field) for field_name, filter_field in filterset_class.base_filters.items() ]
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: warn( 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: warn('Encountered unknown view base class. please report this issue. ignoring for now') except Exception as exc: warn( 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})' )
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.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)