Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug] Not correct openapi json for ReDoc with dynamically generated classes #74

Closed
charoduke opened this issue Apr 27, 2022 · 4 comments

Comments

@charoduke
Copy link

charoduke commented Apr 27, 2022

Hello. I make class views dynamically from SA models for REST api. And in ReDoc all summary for methods like last class.

bug_2022-04-27_10-18-48

Class view generate like this:

....................
views.append(type(f"VI_{name}",
                               (HTTPMethodView,),
                               {'model': model, 
                               '_url': f'/{name}/<id:int>',
                               'get': op.summary(f"Get obj {name}")(viewItem_get),
                               'put': op.summary(f"Update obj {name}")(viewItem_put),
                               'patch': op.summary(f"Change obj {name}")(viewItem_patch),
                               'delete': op.summary(f"Delete obj {name}")(viewItem_delete)}))

......................... 
for view in views:
    blueprint.add_route(view.as_view(), view._url)

I find way to generate custom summary and all openapi fields for every my dynamic class-view.

I use Manjaro + firefox. Sanic v22.3.1 and sanic-ext==22.3.1

I try deepcopy, copy all objects and copy func-methods for set unique names for every gen but it not help all.

@ahopkins
Copy link
Member

I guess I am not sure what the problem is. What would the expected outcome be?

@charoduke
Copy link
Author

I guess I am not sure what the problem is. What would the expected outcome be?

I'm trying just use template op.summary(f"Get obj {name}") with list of names.

in https://github.com/sanic-org/sanic-ext/blob/main/sanic_ext/extensions/openapi/openapi.py line 412-422

if summary:
      func = glbl["summary"](summary)(func)

There 'func' is key to collect data.

But I have many dynamic view classes from same func-methods

type(f"VI_{name}",
     (HTTPMethodView,),
     {get': op.summary(f"Get obj {name}")(viewItem_get),....

Where viewItem_get is func.

so ...
I have one last summary for all definitions in json of openapi. With last 'name' for all.

@ahopkins
Copy link
Member

I think I see what you are after here. I am not sure that we are likely to change this as it would potentially have a lot of complications for a narrow use case. Especially since I think there are a few alternatives that will get you there.

Using partial

Perhaps the easiest solution is to just wrap your functions in partial. That creates the function as a new/unique instance.

from functools import partial

from sanic import Blueprint, Sanic
from sanic.views import HTTPMethodView
from sanic_ext import openapi

app = Sanic(__name__)
bp = Blueprint("auto")
app.blueprint(bp)


def get(*_):
    ...


def put(*_):
    ...


def patch(*_):
    ...


def delete(*_):
    ...


for name in ["foo", "bar"]:
    view = type(
        f"VI_{name}",
        (HTTPMethodView,),
        {
            "_url": f"/{name}",
            "get": openapi.summary(f"get - {name}")(partial(get)),
            "put": openapi.summary(f"put - {name}")(partial(put)),
            "patch": openapi.summary(f"patch - {name}")(partial(patch)),
            "delete": openapi.summary(f"delete - {name}")(partial(delete)),
        },
    )
    bp.add_route(view.as_view(), view._url)

image

Factory

Personally, I would (and do) opt for this approach as it is much easier to read.

from sanic import Blueprint, Sanic
from sanic.views import HTTPMethodView
from sanic_ext import openapi

app = Sanic(__name__)
bp = Blueprint("auto")
app.blueprint(bp)


def create_view(name, bp):
    class CustomView(HTTPMethodView, attach=bp, uri=f"/{name}"):
        @openapi.summary(f"get - {name}")
        def get(*_):
            ...

        @openapi.summary(f"put - {name}")
        def put(*_):
            ...

        @openapi.summary(f"patch - {name}")
        def patch(*_):
            ...

        @openapi.summary(f"delete - {name}")
        def delete(*_):
            ...


for name in ["foo", "bar"]:
    create_view(name, bp)

If you are doing this, you probably also want to se operation so that each one has a unique explicit name:

        @openapi.summary(f"get - {name}")
        @openapi.operation(f"get_{name}")
        def get(*_):
            ...

        @openapi.summary(f"put - {name}")
        @openapi.operation(f"put_{name}")
        def put(*_):
            ...

        @openapi.summary(f"patch - {name}")
        @openapi.operation(f"patch_{name}")
        def patch(*_):
            ...

        @openapi.summary(f"delete - {name}")
        @openapi.operation(f"delete_{name}")
        def delete(*_):
            ...

Or, just combine it:

        @openapi.definition(
            summary=f"get - {name}",
            operation=f"get_{name}",
        )
        def get(*_):
            ...

        @openapi.definition(
            summary=f"put - {name}",
            operation=f"put_{name}",
        )
        def put(*_):
            ...

        @openapi.definition(
            summary=f"patch - {name}",
            operation=f"patch_{name}",
        )
        def patch(*_):
            ...

        @openapi.definition(
            summary=f"delete - {name}",
            operation=f"delete_{name}",
        )
        def delete(*_):
            ...

I am closing this as I think you can achieve what you want already. LMK if this works for you or not.

@charoduke
Copy link
Author

charoduke commented Jun 1, 2022

Thank you! It now work for me. I add functools.update_wrapper for copy doc from original, example:

def gen_func(func, summary=None, description=None, operation=None):
    f = partial(func)
    if description is None:
        update_wrapper(f, func)
        description = f.__doc__
    else:
        f.__doc__ = description
    return op.definition(summary=summary, description=description, operation=operation)(f)

.............

    views.append(type(f"VIs_{name}",
                      (HTTPMethodView,),
                      {'model': model,          
                       '_url': f'/{name}/',
                       'get': gen_func(viewItems_get,
                                       summary=f'List of obj {name}',
                                       description=f"List of objects {name}",
                                       operation=f'get_{name}')}))

Also next I add data of body and response.

2022-06-01_11-11-47

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants