Drf-yasg: can not use custom DEFAULT_PAGINATOR_INSPECTORS

Created on 13 Jun 2019  路  9Comments  路  Source: axnsan12/drf-yasg

Hi,

I can not set a custom paginator inspector as default using this DEFAULT_PAGINATOR_INSPECTORS option that is in the docs:
https://drf-yasg.readthedocs.io/en/stable/settings.html#default-paginator-inspectors.

Here are the step to reproduce. I have a simple api module in which I created a file swagger.py with the following :

from drf_yasg.inspectors import PaginatorInspector
from drf_yasg import openapi
from collections import OrderedDict


api_info = openapi.Info(
    title="API Title", default_version="v1.0", description="This API contains some cool endpoints."
)


class PageNumberPaginatorInspectorClass(PaginatorInspector):
    def get_paginated_response(self, paginator, response_schema):
        paged_schema = openapi.Schema(
            type=openapi.TYPE_OBJECT,
            properties=OrderedDict((
                ('count', openapi.Schema(type=openapi.TYPE_INTEGER)),
                ('elements', openapi.Schema(type=openapi.TYPE_INTEGER)),
                ('results', response_schema),
            )),
            required=['results']
        )

        return paged_schema

Here is my django config in settings.py for swagger:

SWAGGER_SETTINGS = {
    "SECURITY_DEFINITIONS": {"basic": {"type": "basic"}},
    "DEFAULT_INFO": "api.swagger.api_info",
    "DEFAULT_PAGINATOR_INSPECTORS": [
        'api.swagger.PageNumberPaginatorInspectorClass',
    ]
}

When generating the swagger file with the CLI, it throws the following error:

  [2019-06-13 13:43:01 +0000] [92] [ERROR] Exception in worker process
 Traceback (most recent call last):
   File "/usr/lib/python3.6/site-packages/rest_framework/settings.py", line 184, in import_from_string
     return getattr(module, class_name)
 AttributeError: module 'api.swagger' has no attribute 'PageNumberPaginatorInspectorClass'

 During handling of the above exception, another exception occurred:

 Traceback (most recent call last):
   File "/usr/lib/python3.6/site-packages/rest_framework/settings.py", line 183, in import_from_string
     module = import_module(module_path)
   File "/usr/lib/python3.6/importlib/__init__.py", line 126, in import_module
     return _bootstrap._gcd_import(name[level:], package, level)
   File "<frozen importlib._bootstrap>", line 994, in _gcd_import
   File "<frozen importlib._bootstrap>", line 971, in _find_and_load
   File "<frozen importlib._bootstrap>", line 955, in _find_and_load_unlocked
   File "<frozen importlib._bootstrap>", line 665, in _load_unlocked
   File "<frozen importlib._bootstrap_external>", line 678, in exec_module
   File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
   File "/usr/src/app/api/swagger.py", line 1, in <module>
     from drf_yasg.inspectors import PaginatorInspector
   File "/usr/lib/python3.6/site-packages/drf_yasg/inspectors/__init__.py", line 16, in <module>
     ViewInspector.paginator_inspectors = swagger_settings.DEFAULT_PAGINATOR_INSPECTORS
   File "/usr/lib/python3.6/site-packages/drf_yasg/app_settings.py", line 121, in __getattr__
     val = perform_import(val, attr)
   File "/usr/lib/python3.6/site-packages/rest_framework/settings.py", line 172, in perform_import
     return [import_from_string(item, setting_name) for item in val]
   File "/usr/lib/python3.6/site-packages/rest_framework/settings.py", line 172, in <listcomp>
     return [import_from_string(item, setting_name) for item in val]
   File "/usr/lib/python3.6/site-packages/rest_framework/settings.py", line 187, in import_from_string
     raise ImportError(msg)
 ImportError: Could not import 'api.swagger.PageNumberPaginatorInspectorClass' for API setting 'DEFAULT_PAGINATOR_INSPECTORS'. AttributeError: module 'api.swagger' has no attribute 'PageNumberPaginatorInspectorClass'.

Am I doing something wrong or is there a bug here?
How come api.swagger.api_info is correctly imported but api.swagger.PageNumberPaginatorInspectorClass is not?

Most helpful comment

Ok I found out the issue. Thank you very much for your help and reactivity.

It seems that drf-yasg can not import two different defaults from the same file.

cf. in the given settings example, DEFAULT_INFO and DEFAULT_PAGINATOR_INSPECTORS both read from the file swagger.py (api.swagger.xxxxxxx)

When I move api_info into its own file info.py and the inspector in its own file inspector.py with the following settings everything works:

SWAGGER_SETTINGS = {
    "SECURITY_DEFINITIONS": {"basic": {"type": "basic"}},
    "DEFAULT_INFO": "api.info.api_info",
    "DEFAULT_PAGINATOR_INSPECTORS": [
        'api.inspector.PageNumberPaginatorInspectorClass',
    ]
}

All 9 comments

It's probably a circular import issue. Try breaking things up in separate modules.

Thanks for the reply. Given how simple and little the sample code I provided is and given that it only imports code from the external libs rest-framework and yours, I do not feel like I can break up 'things'. What things?

Perhaps you import django's Paginators in the same file where PaginatorInspector is defined, making it hard to use the paginator inspector while extending a django's paginator (which is probably often the case).

Looking at the stack trace, it's a cyclic import:

api.swagger -> from drf_yasg.generators import OpenAPISchemaGenerator -> from .inspectors.field import get_basic_type_info, get_queryset_field, get_queryset_from_view -> ViewInspector.paginator_inspectors = swagger_settings.DEFAULT_PAGINATOR_INSPECTORS -> return [import_from_string(item, setting_name) for item in val] (== import api.swagger.PageNumberPaginatorInspectorClass) -> ImportError

Which suggets that the file contains more than you've shown, i.e. an OpenAPISchemaGenerator subclass also.

You could argue that this is a bug in drf-yasg and that imports from settings should be made lazily, but simply moving PageNumberPaginatorInspectorClass to a file which does not import drf_yasg.generators will fix your problem.

Oops, you are right about the stack trace, I got it mixed up when pasting it here.

I updated and simplified the example and stack trace in the original post to match the given example, where there is nothing else in the file than what is shown.

The stack trace seems to say that the import error happens when importing the paginator inspector:
from drf_yasg.inspectors import PaginatorInspector

Well then, that's strange. Using https://github.com/axnsan12/drf-yasg/commit/acc204e4ea6f16f8541eed1e940e9b7e4ebe6ea6 I can't reproduce the error.

Ok I found out the issue. Thank you very much for your help and reactivity.

It seems that drf-yasg can not import two different defaults from the same file.

cf. in the given settings example, DEFAULT_INFO and DEFAULT_PAGINATOR_INSPECTORS both read from the file swagger.py (api.swagger.xxxxxxx)

When I move api_info into its own file info.py and the inspector in its own file inspector.py with the following settings everything works:

SWAGGER_SETTINGS = {
    "SECURITY_DEFINITIONS": {"basic": {"type": "basic"}},
    "DEFAULT_INFO": "api.info.api_info",
    "DEFAULT_PAGINATOR_INSPECTORS": [
        'api.inspector.PageNumberPaginatorInspectorClass',
    ]
}

I'm having this problem and can't for the life of me break the circular dependency.

I have a file in content/inspectors.py that contains only this:

from drf_yasg.inspectors.base import FilterInspector, NotHandled, openapi


class URLParamFilterInspector(FilterInspector):
    def get_filter_parameters(self, filter):
        if not filter.__name__ == "URLParamFilter":
            return NotHandled

        return [
            openapi.Parameter(
                filter.field_name, openapi.IN_QUERY, type=openapi.TYPE_STRING
            )
        ]

And in settings/base.py:

SWAGGER_SETTINGS = {
    "DEFAULT_INFO": {
        "title": "API",
    },
    "SECURITY_DEFINITIONS": {
        "JWT": {"type": "apiKey", "name": "Authorization", "in": "header"},
    },
    "DEFAULT_MODEL_RENDERING": ["example", "model"],
    "DEFAULT_FILTER_INSPECTORS": ["content.inspectors.URLParamFilterInspector"],
}

I fire up ./manage.py shell and try to run from content.inspectors import URLParamFilterInspector and get this error:

Traceback (most recent call last):
  File "/usr/local/lib/python3.9/code.py", line 90, in runcode
    exec(code, self.locals)
  File "<console>", line 1, in <module>
  File "/usr/src/app/content/inspectors.py", line 1, in <module>
    from drf_yasg.inspectors.base import FilterInspector, NotHandled, openapi
  File "/usr/local/lib/python3.9/site-packages/drf_yasg/inspectors/__init__.py", line 15, in <module>
    ViewInspector.filter_inspectors = swagger_settings.DEFAULT_FILTER_INSPECTORS
  File "/usr/local/lib/python3.9/site-packages/drf_yasg/app_settings.py", line 122, in __getattr__
    val = perform_import(val, attr)
  File "/usr/local/lib/python3.9/site-packages/rest_framework/settings.py", line 168, in perform_import
    return [import_from_string(item, setting_name) for item in val]
  File "/usr/local/lib/python3.9/site-packages/rest_framework/settings.py", line 168, in <listcomp>
    return [import_from_string(item, setting_name) for item in val]
  File "/usr/local/lib/python3.9/site-packages/rest_framework/settings.py", line 180, in import_from_string
    raise ImportError(msg)
ImportError: Could not import 'content.inspectors.URLParamFilterInspector' for API setting 'DEFAULT_FILTER_INSPECTORS'. ImportError: Module "content.inspectors" does not define a "URLParamFilterInspector" attribute/class.

I also get this error when attempting to start the development server normally, using ./manage.py runserver.

I'm not sure how I'm meant to subclass FilterInspector, it appears that there's an inescapable circular dependency caused by the line ViewInspector.filter_inspectors = swagger_settings.DEFAULT_FILTER_INSPECTORS. It will always try and load the filter inspectors before I can define one.

If I try and cheese it like so:

from drf_yasg import openapi


class URLParamFilterInspector:
    def get_filter_parameters(self, filter):
        if not filter.__name__ == "URLParamFilter":
            return None

        return openapi.Parameter(
            filter.field_name, openapi.IN_QUERY, type=openapi.TYPE_STRING
        )

I get the error:

AssertionError: inspectors must subclass BaseInspector

Added more cheese but it didn't work:

def URLParamFilterInspector():
    from drf_yasg import openapi
    from drf_yasg.inspectors import FilterInspector, NotHandled

    class _URLParamFilterInspector(FilterInspector):
        def get_filter_parameters(self, filter):
            if not filter.__name__ == "URLParamFilter":
                return NotHandled

            return openapi.Parameter(
                filter.field_name, openapi.IN_QUERY, type=openapi.TYPE_STRING
            )

    return _URLParamFilterInspector

Error:

AssertionError: inspector must be a class, not an object

You've made it difficult to trick your project into doing what I want to do 馃槃

Was this page helpful?
0 / 5 - 0 ratings

Related issues

phihag picture phihag  路  3Comments

iamhssingh picture iamhssingh  路  3Comments

therefromhere picture therefromhere  路  3Comments

jaumard picture jaumard  路  5Comments

hnykda picture hnykda  路  3Comments