def decorator(view): view_methods = {m.__name__: m for m in get_view_methods(view)} for method_name, method_decorator in kwargs.items(): if method_name not in view_methods: warn( f'@extend_schema_view argument "{method_name}" was not found on view ' f'{view.__name__}. method override for "{method_name}" will be ignored.' ) continue method = view_methods[method_name] # the context of derived methods must not be altered, as it belongs to the other # class. create a new context via the wrapping_decorator so the schema can be safely # stored in the wrapped_method. methods belonging to the view can be safely altered. if method_name in view.__dict__: method_decorator(method) else: setattr(view, method_name, wrapping_decorator(method_decorator, method)) return view
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