def __init__(self, *args, **kwargs): self.registry = ComponentRegistry() self.api_version = kwargs.pop('api_version', None) self.inspector = None super().__init__(*args, **kwargs)
class SchemaGenerator(BaseSchemaGenerator): endpoint_inspector_cls = EndpointEnumerator def __init__(self, *args, **kwargs): self.registry = ComponentRegistry() self.api_version = kwargs.pop('api_version', None) self.inspector = None super().__init__(*args, **kwargs) 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) # drf-yasg compatibility feature. makes the view aware that we are running # schema generation and not a real request. view.swagger_fake_view = True # 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 _initialise_endpoints(self): if self.endpoints is None: self.inspector = self.endpoint_inspector_cls( self.patterns, self.urlconf) self.endpoints = self.inspector.get_api_endpoints() def _get_paths_and_endpoints(self, request): """ Generate (path, method, view) given (path, method, callback) for paths. """ view_endpoints = [] for path, path_regex, method, callback in self.endpoints: view = self.create_view(callback, method, request) path = self.coerce_path(path, method, view) view_endpoints.append((path, path_regex, method, view)) return view_endpoints def parse(self, input_request, public): """ Iterate endpoints generating per method path operations. """ result = {} self._initialise_endpoints() endpoints = self._get_paths_and_endpoints( None if public else input_request) if spectacular_settings.SCHEMA_PATH_PREFIX is None: # estimate common path prefix if none was given. only use it if we encountered more # than one view to prevent emission of erroneous and unnecessary fallback names. non_trivial_prefix = len( set([view.__class__ for _, _, _, view in endpoints])) > 1 if non_trivial_prefix: path_prefix = os.path.commonpath( [path for path, _, _, _ in endpoints]) else: path_prefix = '/' else: path_prefix = spectacular_settings.SCHEMA_PATH_PREFIX if not path_prefix.startswith('^'): path_prefix = '^' + path_prefix # make sure regex only matches from the start for path, path_regex, method, view in endpoints: if not self.has_view_permissions(path, method, view): continue if input_request: request = input_request else: # mocked request to allow certain operations in get_queryset and get_serializer[_class] # without exceptions being raised due to no request. request = spectacular_settings.GET_MOCK_REQUEST( method, path, view, input_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), ( f'Incompatible AutoSchema used on View {view.__class__}. Is DRF\'s ' f'DEFAULT_SCHEMA_CLASS pointing to "drf_spectacular.openapi.AutoSchema" ' f'or any other drf-spectacular compatible AutoSchema?') with add_trace_message(getattr(view, '__class__', view).__name__): operation = view.schema.get_operation(path, path_regex, path_prefix, method, self.registry) # operation was manually removed via @extend_schema if not operation: continue if spectacular_settings.SCHEMA_PATH_PREFIX_TRIM: path = re.sub(pattern=path_prefix, repl='', string=path, flags=re.IGNORECASE) if not path.startswith('/'): path = '/' + path if spectacular_settings.CAMELIZE_NAMES: path, operation = camelize_operation(path, operation) result.setdefault(path, {}) result[path][method.lower()] = operation return result def get_schema(self, request=None, public=False): """ Generate a OpenAPI schema. """ reset_generator_stats() result = build_root_object( paths=self.parse(request, public), components=self.registry.build( spectacular_settings.APPEND_COMPONENTS), version=self.api_version or getattr(request, 'version', None), ) for hook in spectacular_settings.POSTPROCESSING_HOOKS: result = hook(result=result, generator=self, request=request, public=public) return sanitize_result_object(normalize_result_object(result))
def __init__(self, *args, **kwargs): self.registry = ComponentRegistry() super().__init__(*args, **kwargs)
class SchemaGenerator(BaseSchemaGenerator): endpoint_inspector_cls = EndpointEnumerator def __init__(self, *args, **kwargs): self.registry = ComponentRegistry() super().__init__(*args, **kwargs) 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 _get_paths_and_endpoints(self, request): """ Generate (path, method, view) given (path, method, callback) for paths. """ view_endpoints = [] for path, path_regex, method, callback in self.endpoints: view = self.create_view(callback, method, request) path = self.coerce_path(path, method, view) view_endpoints.append((path, path_regex, method, view)) return view_endpoints def parse(self, request=None): """ Iterate endpoints generating per method path operations. """ result = {} self._initialise_endpoints() for path, path_regex, method, view in self._get_paths_and_endpoints( request): if not self.has_view_permissions(path, method, view): 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 get_schema(self, request=None, public=False): """ Generate a OpenAPI schema. """ reset_generator_stats() return build_root_object( paths=self.parse(None if public else request), components=self.registry.build( spectacular_settings.APPEND_COMPONENTS), )
class SchemaGenerator(BaseSchemaGenerator): endpoint_inspector_cls = EndpointEnumerator def __init__(self, *args, **kwargs): self.registry = ComponentRegistry() self.api_version = kwargs.pop('api_version', None) self.inspector = None super().__init__(*args, **kwargs) 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) 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 # 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 _initialise_endpoints(self): if self.endpoints is None: self.inspector = self.endpoint_inspector_cls( self.patterns, self.urlconf) self.endpoints = self.inspector.get_api_endpoints() def _get_paths_and_endpoints(self, request): """ Generate (path, method, view) given (path, method, callback) for paths. """ view_endpoints = [] for path, path_regex, method, callback in self.endpoints: view = self.create_view(callback, method, request) path = self.coerce_path(path, method, view) view_endpoints.append((path, path_regex, method, view)) return view_endpoints 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_schema(self, request=None, public=False): """ Generate a OpenAPI schema. """ reset_generator_stats() result = build_root_object( paths=self.parse(request, public), components=self.registry.build( spectacular_settings.APPEND_COMPONENTS), ) for hook in spectacular_settings.POSTPROCESSING_HOOKS: result = hook(result=result, generator=self, request=request, public=public) return sanitize_result_object(normalize_result_object(result))
class SchemaGenerator(BaseSchemaGenerator): def __init__(self, *args, **kwargs): self.registry = ComponentRegistry() super().__init__(*args, **kwargs) 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_endpoints(self, request): """ sorted endpoints by operation """ self._initialise_endpoints() _, endpoints = self._get_paths_and_endpoints(request) if spectacular_settings.OPERATION_SORTER == 'alpha': return sorted(endpoints, key=alpha_operation_sorter) else: # default to DRF method sorting return endpoints def parse(self, request=None): """ Iterate endpoints generating per method path operations. """ result = {} for path, method, view in self.get_endpoints(request): if not self.has_view_permissions(path, method, view): continue # beware that every access to schema yields a fresh object (descriptor pattern) operation = view.schema.get_operation(path, 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 get_schema(self, request=None, public=False): """ Generate a OpenAPI schema. """ reset_generator_stats() return build_root_object( paths=self.parse(None if public else request), components=self.registry.build( spectacular_settings.APPEND_COMPONENTS), )