def modify_for_versioning(patterns, method, path, view, requested_version): assert view.versioning_class and view.request assert requested_version view.request.version = requested_version if issubclass(view.versioning_class, versioning.URLPathVersioning): version_param = view.versioning_class.version_param # substitute version variable to emulate request path = uritemplate.partial(path, var_dict={version_param: requested_version}) if isinstance(path, URITemplate): path = path.uri # emulate router behaviour by injecting substituted variable into view view.kwargs[version_param] = requested_version elif issubclass(view.versioning_class, versioning.NamespaceVersioning): try: view.request.resolver_match = get_resolver( urlconf=tuple(detype_pattern(p) for p in patterns) ).resolve(path) except Resolver404: error(f"namespace versioning path resolution failed for {path}. path will be ignored.") elif issubclass(view.versioning_class, versioning.AcceptHeaderVersioning): # Append the version into request accepted_media_type. # e.g "application/json; version=1.0" # To allow the AcceptHeaderVersioning negotiator going through. if not hasattr(view.request, 'accepted_renderer'): # Probably a mock request, content negotation was not performed, so, we do it now. negotiated = view.perform_content_negotiation(view.request) view.request.accepted_renderer, view.request.accepted_media_type = negotiated media_type = _MediaType(view.request.accepted_media_type) view.request.accepted_media_type = ( f'{media_type.full_type}; {view.versioning_class.version_param}={requested_version}' ) return path
def load_enum_name_overrides(): overrides = {} for name, choices in spectacular_settings.ENUM_NAME_OVERRIDES.items(): if isinstance(choices, str): choices = deep_import_string(choices) if not choices: warn( f'unable to load choice override for {name} from ENUM_NAME_OVERRIDES. ' f'please check module path string.' ) continue if inspect.isclass(choices) and issubclass(choices, Choices): choices = choices.choices if inspect.isclass(choices) and issubclass(choices, Enum): choices = [c.value for c in choices] if isinstance(choices, Iterable) and isinstance(choices[0], str): choices = [(c, c) for c in choices] overrides[list_hash(list(dict(choices).keys()))] = name if len(spectacular_settings.ENUM_NAME_OVERRIDES) != len(overrides): error( 'ENUM_NAME_OVERRIDES has duplication issues. encountered multiple names ' 'for the same choice set. enum naming might be unexpected.' ) return overrides
def load_enum_name_overrides(): overrides = {} for name, choices in spectacular_settings.ENUM_NAME_OVERRIDES.items(): if isinstance(choices, str): choices = deep_import_string(choices) if not choices: warn( f'unable to load choice override for {name} from ENUM_NAME_OVERRIDES. ' f'please check module path string.') continue if inspect.isclass(choices) and issubclass(choices, Choices): choices = choices.choices if inspect.isclass(choices) and issubclass(choices, Enum): choices = [c.value for c in choices] normalized_choices = [] for choice in choices: if isinstance(choice, str): normalized_choices.append( (choice, choice)) # simple choice list elif isinstance(choice[1], (list, tuple)): normalized_choices.extend( choice[1]) # categorized nested choices else: normalized_choices.append(choice) # normal 2-tuple form overrides[list_hash(list(dict(normalized_choices).keys()))] = name if len(spectacular_settings.ENUM_NAME_OVERRIDES) != len(overrides): error( 'ENUM_NAME_OVERRIDES has duplication issues. Encountered multiple names ' 'for the same choice set. Enum naming might be unexpected.') return overrides
def decorator(f): BaseSchema = ( # explicit manually set schema or previous view annotation getattr(f, 'schema', None) # previously set schema with @extend_schema on views methods or getattr(f, 'kwargs', {}).get('schema', None) # previously set schema with @extend_schema on @api_view or getattr(getattr(f, 'cls', None), 'kwargs', {}).get( 'schema', None) # the default or api_settings.DEFAULT_SCHEMA_CLASS) if not inspect.isclass(BaseSchema): BaseSchema = BaseSchema.__class__ def is_in_scope(ext_schema): version, _ = ext_schema.view.determine_version( ext_schema.view.request, **ext_schema.view.kwargs) version_scope = versions is None or version in versions method_scope = methods is None or ext_schema.method in methods return method_scope and version_scope class ExtendedSchema(BaseSchema): def get_operation(self, path, path_regex, path_prefix, method, registry): self.method = method if exclude and is_in_scope(self): return None if operation is not None and is_in_scope(self): return operation return super().get_operation(path, path_regex, path_prefix, method, registry) def get_operation_id(self): if operation_id and is_in_scope(self): return operation_id return super().get_operation_id() def get_override_parameters(self): if parameters and is_in_scope(self): return super().get_override_parameters() + parameters return super().get_override_parameters() def get_auth(self): if auth and is_in_scope(self): return auth return super().get_auth() def get_examples(self): if examples and is_in_scope(self): return examples return super().get_examples() def get_request_serializer(self): if request is not empty and is_in_scope(self): return request return super().get_request_serializer() def get_response_serializers(self): if responses is not empty and is_in_scope(self): return responses return super().get_response_serializers() def get_description(self): if description and is_in_scope(self): return description return super().get_description() def get_summary(self): if summary and is_in_scope(self): return str(summary) return super().get_summary() def is_deprecated(self): if deprecated and is_in_scope(self): return deprecated return super().is_deprecated() def get_tags(self): if tags is not None and is_in_scope(self): return tags return super().get_tags() if inspect.isclass(f): # either direct decoration of views, or unpacked @api_view from OpenApiViewExtension if operation_id is not None or operation is not None: error( f'using @extend_schema on viewset class {f.__name__} with parameters ' f'operation_id or operation will most likely result in a broken schema.' ) # reorder schema class MRO so that view method annotation takes precedence # over view class annotation. only relevant if there is a method annotation for view_method in get_view_methods(view=f, schema=BaseSchema): if 'schema' in getattr(view_method, 'kwargs', {}): view_method.kwargs['schema'] = type( 'ExtendedMetaSchema', (view_method.kwargs['schema'], ExtendedSchema), {}) # persist schema on class to provide annotation to derived view methods. # the second purpose is to serve as base for view multi-annotation f.schema = ExtendedSchema() return f elif callable(f) and hasattr(f, 'cls'): # 'cls' attr signals that as_view() was called, which only applies to @api_view. # keep a "unused" schema reference at root level for multi annotation convenience. setattr(f.cls, 'kwargs', {'schema': ExtendedSchema}) # set schema on method kwargs context to emulate regular view behaviour. for method in f.cls.http_method_names: setattr(getattr(f.cls, method), 'kwargs', {'schema': ExtendedSchema}) return f elif callable(f): # custom actions have kwargs in their context, others don't. create it so our create_view # implementation can overwrite the default schema if not hasattr(f, 'kwargs'): f.kwargs = {} # this simulates what @action is actually doing. somewhere along the line in this process # the schema is picked up from kwargs and used. it's involved my dear friends. # use class instead of instance due to descriptor weakref reverse collisions f.kwargs['schema'] = ExtendedSchema return f else: return f