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

Flask extension does not support blueprints #11

Open
lafrech opened this issue Nov 3, 2018 · 19 comments
Open

Flask extension does not support blueprints #11

lafrech opened this issue Nov 3, 2018 · 19 comments
Labels

Comments

@lafrech
Copy link
Member

lafrech commented Nov 3, 2018

Issue by deckar01
Wednesday Apr 27, 2016 at 17:42 GMT
Originally opened as marshmallow-code/apispec#68


I have organized my views with Flask blueprints, but am unable to document them in the view file, because it is not executed in the application context.

# app/views/example.py

from flask import Blueprint
example_view = Blueprint('example_view', __name__)

from app.spec import spec

@bp.route('example', methods=['GET'])
def get_example():
    """An example.
    ---
    get:
        description: Get an example
        responses:
            200:
                description: An example
                schema: ExampleSchema
    """
    return 'example', 200

spec.add_path(view=get_example)
  ...
  File "/usr/local/lib/python2.7/site-packages/apispec/core.py", line 170, in add_path
    self, path=path, operations=operations, **kwargs
  File "/usr/local/lib/python2.7/site-packages/apispec/ext/flask.py", line 62, in path_from_view
    rule = _rule_for_view(view)
  File "/usr/local/lib/python2.7/site-packages/apispec/ext/flask.py", line 38, in _rule_for_view
    view_funcs = current_app.view_functions
  File "/usr/local/lib/python2.7/site-packages/werkzeug/local.py", line 343, in __getattr__
    return getattr(self._get_current_object(), name)
  File "/usr/local/lib/python2.7/site-packages/werkzeug/local.py", line 302, in _get_current_object
    return self.__local()
  File "/usr/local/lib/python2.7/site-packages/flask/globals.py", line 34, in _find_app
    raise RuntimeError('working outside of application context')
RuntimeError: working outside of application context

Is there a way to keep the spec declarations with the blueprint? (It seems like there might not be.)

Do you think it would be useful to add the ability to add all the views from a blueprint at once?

I noticed that the flask extension seems to acknowledge that a view could contain multiple paths, but assumes it only contains one. apispec/ext/flask.py#L46

Maybe something like spec.add_paths() could be added to handle compound view objects?

@lafrech
Copy link
Member Author

lafrech commented Nov 3, 2018

Comment by deckar01
Wednesday Apr 27, 2016 at 19:43 GMT


After centralizing the specs into the app declaration I have to explicitly provide an app context.
http://stackoverflow.com/questions/31444036/runtimeerror-working-outside-of-application-context

@lafrech
Copy link
Member Author

lafrech commented Nov 3, 2018

Comment by Trii
Thursday Apr 28, 2016 at 14:45 GMT


@deckar01 I'd also wager that putting your spec.add_path setup in your blueprint directly could cause unexpected consequences if you ever decided to register a blueprint to your application multiple times.

@lafrech
Copy link
Member Author

lafrech commented Nov 3, 2018

Comment by deckar01
Thursday Apr 28, 2016 at 15:33 GMT


I ended up moving my spec declarations out of the execution path, so that the specs depend on the schema and views, but breaking changes to the specs do not affect the functionality of the app.

I break my specs up into files to mirror the schema/view structure and have a central spec instance that registers them within the flask app context. There is definitely some boiler plate that can be reduced, but it works.

It would be nice to be able to register a blueprint as a whole though. I'm not sure if it's possible, but it would be convenient if it was able to recognize the schema classes used by the blueprint and register those as well.

For now I will maintain a spec manifest that mirrors the API.

@lafrech
Copy link
Member Author

lafrech commented Nov 3, 2018

Comment by deckar01
Friday Apr 29, 2016 at 15:44 GMT


@TRII @sloria How would you feel about exposing an interface for adding multiple paths at once?

I image the interface would look something like:

def add_paths(self, paths=None, **kwargs):
    """Add path objects to the spec.

    https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#pathsObject

    :param List[Path]|None path: List of Path instances
    :param dict kwargs: parameters used by any path helpers see :meth:`register_paths_helper`
    """

This would avoid a lot of boilerplate code if the paths are already available as a list.

This would allow the flask extension to support adding all the paths in a blueprint at once (or even all the paths in an app).

@lafrech
Copy link
Member Author

lafrech commented Nov 3, 2018

Comment by sloria
Saturday Apr 30, 2016 at 20:38 GMT


@deckar01 If we were to add add_paths, users might expect add_parameters, definitions, etc. to exist. What would add_paths provide that add_path within a for loop wouldn't provide?

@lafrech
Copy link
Member Author

lafrech commented Nov 3, 2018

Comment by deckar01
Saturday Apr 30, 2016 at 21:26 GMT


The ability for extensions to abstract registering collections of paths. I admit that a list of paths in userspace can easily be iterated, but navigating the internal structure of the path collections in a framework like flask is non-obvious.

In my use case 100% of my API needs to be documented. Every single view needing a duplicate spec declaration in a separate file creates a lot of opportunities for me or other developers to omit spec definitions. Allowing bulk registration reduces boilerplate and avoids pitfalls.

I understand the argument for maintaining a symmetrical interface. I don't have an argument for or against bulk parameter or definition registration, because I did not run into any issues with the standard registration pattern in my "growing" app.

Users who are scaling a flask app with blueprints are going to run into the same issue I did, because the only pattern suggested in the docs does not work with blueprints. The point of blueprints is to keep views organized in self contained collections, but now my views have to be explicitly imported in other files.

@lafrech
Copy link
Member Author

lafrech commented Nov 3, 2018

Comment by sloria
Sunday May 01, 2016 at 23:02 GMT


but now my views have to be explicitly imported in other files.

Can you please clarify how add_paths would address this problem?

@lafrech
Copy link
Member Author

lafrech commented Nov 3, 2018

Comment by deckar01
Monday May 02, 2016 at 00:58 GMT


A flask app with blueprints starts out looking something like:

from flask import Flask
from my_app.views.blueprint1 import blueprint1

app = Flask(__name__)

app.register_blueprint(blueprint1)

Blueprints are not defined in the flask application context, so the paths must be register to apispec in the flask app context:

from flask import Flask
from apispec import APISpec
from views.blueprint1 import blueprint1, view1, view2, view3, ...

spec = APISpec( ... )
app = Flask(__name__)

app.register_blueprint(blueprint1)

spec.add_path(view=view1)
spec.add_path(view=view2)
spec.add_path(view=view3)
...

This defeats the benefit of blueprints encapsulating view specific details away from the core application logic.

My current alternative is to import the app into a spec file and explicitly register the views within the app context. My core app logic is no longer aware of the blueprints implementation details, but I am still leaking those details outside of the blueprint.

Having the flask extension be able to register blueprints would correct the app blueprint encapsulation issue:

from flask import Flask
from apispec import APISpec
from views.blueprint1 import blueprint1

spec = APISpec( ... )
app = Flask(__name__)

app.register_blueprint(blueprint1)
spec.add_paths(views=blueprint1)

Having the flask extension add all the paths in an application at once would be icing on the cake.

from flask import Flask
from apispec import APISpec
from views.blueprint1 import blueprint1
from views.blueprint2 import blueprint2
from views.blueprint3 import blueprint3
...

spec = APISpec( ... )
app = Flask(__name__)

app.register_blueprint(blueprint1)
app.register_blueprint(blueprint2)
app.register_blueprint(blueprint3)
...

spec.add_paths(views=app)

This is really about providing a blessed pattern for large apps that have turned to blueprints for organization, then find the apispec docs no longer apply.

Maybe add_paths() is outside the scope of this project. An alternative is a wrapper library like apispec-flask-blueprints (following the example of marshmallow-jsonapi).

@lafrech
Copy link
Member Author

lafrech commented Nov 3, 2018

Comment by sloria
Monday May 02, 2016 at 01:54 GMT


Ah, I see; the benefit would be that plugins could implement a paths_helper that would abstract extracting paths. Thanks for the clarification.

An add_paths method is within scope of apispec. The question is whether is worth the increased API surface area. You presented a valid use case, and I would gladly review and merge a PR for it.

@lafrech
Copy link
Member Author

lafrech commented Nov 3, 2018

Comment by deckar01
Tuesday Jul 19, 2016 at 21:52 GMT


I am going to take a look at implementing this since @lafrech has provided another example of view collections in flask.

@lafrech
Copy link
Member Author

lafrech commented Nov 3, 2018

Comment by lafrech
Thursday Jul 21, 2016 at 20:36 GMT


@deckar01:

app.register_blueprint(blueprint1)
spec.add_paths(views=blueprint1)

Notice a blueprint can be registered with a custom url_prefix:

app.register_blueprint(simple_page, url_prefix='/pages')

So you may need to do:

app.register_blueprint(blueprint1, url_prefix='/pages')
spec.add_paths(views=blueprint1, url_prefix='/pages')

@lafrech
Copy link
Member Author

lafrech commented Nov 3, 2018

Comment by tinproject
Wednesday Nov 16, 2016 at 18:26 GMT


I've come here because I try to add apispec to document an API that I have in a blueprint, I use flask view functions and not MethodViews, and ends up with a solution that believe interesting in this discussion.

As the url paths on flask are defined as Rules on Flask.url_map I define a helper based on path_from_view() that loads the path from a Rule object:

from apispec import Path, utils
from apispec.ext.flask import flaskpath2swagger

def path_from_rule(spec, rule, **kwargs):
    """Path helper that allows passing a Flask url Rule object."""
    path = flaskpath2swagger(rule.rule)
    view = current_app.view_functions.get(rule.endpoint)
    # Get operations from view function docstring
    operations = utils.load_operations_from_docstring(view.__doc__)
    return Path(path=path, operations=operations)

spec.register_path_helper(path_from_rule)

I ignored the current_app.config['APPLICATION_ROOT'] in path_from_view() as I couldn't find a standard explanation about it's use.

Then I created a function that iter the rules on url_map and add the path for the matching rules:

def add_paths_for_blueprint(spec, blueprint, exclude=()):
    bp_name = blueprint.name
    for r in current_app.url_map.iter_rules():
        ep = r.endpoint.split('.')
        if len(ep) == 1:  # App endpoint, not processed here
            break
        elif len(ep) == 2:  # Blueprint endpoint
            prefix, endpoint = ep[0], ep[1]
            if prefix == bp_name and endpoint not in exclude:
                spec.add_path(rule=r)
        else:
            raise ValueError("Not valid endpoint?", r.endpoint)

@bp.route('/swagger.json')
def get_swagger_json():
    spec = APISpec(...)  # Edited for clarity
    ...  # do other operation to spec, add definitions, etc.
    add_paths_for_blueprint(spec, bp, exclude=['get_swagger_json'])
    return jsonify(spec.to_dict()), 200

And that's it, all the paths in my blueprint documented.

@lafrech
Copy link
Member Author

lafrech commented Nov 3, 2018

Comment by ccutch
Monday May 08, 2017 at 06:28 GMT


@tinproject Tried this out for myself almost worked perfectly, except for me the len(ep) == 1 condition should have resulted in a continue and not a break because there was an app route in between my blueprint routes. Not sure why that was the case but none the less thank you for this contribution!

@lafrech
Copy link
Member Author

lafrech commented Nov 3, 2018

Comment by tinproject
Tuesday May 09, 2017 at 18:41 GMT


I've been thinking about how can I solve this, a plugin helper that can add multiple paths at the same time is needed to properly document Flask apps.

In Flask a view function can have multiple url paths. A Rule in Flask has one url pattern associated with a flask endpoint, that is uniquely linked to a view function (or MethodView). Endpoints to view functions are one-to-one related, but url patterns to endpoints are many-to-one.

I'm currently implementing the add_paths function reference above but I'm not feel that is the better way, I'll open a new issue to discuss it.

@lafrech
Copy link
Member Author

lafrech commented Nov 3, 2018

Comment by andho
Tuesday Sep 18, 2018 at 15:14 GMT


@tinproject I noticed that your swagger.json is for the specific blueprint. Do you have a swagger.json for each blueprint?

@lafrech
Copy link
Member Author

lafrech commented Nov 3, 2018

Comment by tinproject
Tuesday Sep 18, 2018 at 20:47 GMT


@andho If I remember well I had all my API inside a blueprint, sharing the Flask App with 'normal' web endpoints in other blueprints, but only one blueprint having the whole API.

Nothing avoids you to create the spec at the app level and use the add_paths_for_blueprint function to add the paths from different blueprints to the spec.

@lafrech lafrech removed the plugins label Nov 3, 2018
@ergo
Copy link
Collaborator

ergo commented Nov 4, 2018

If I can add something, the idea of doing view introspection before application is created is not optimal.
For example in pyramid there is no global object you can interact on module level.
Please check out how I'm doing introspection in pyramid_apispec https://github.com/ergo/pyramid_apispec/blob/master/demo/app.py#L104 here, maybe some of the ideas would also apply to a flask application?

I CAN support multiple rules per view etc.

@lafrech lafrech added the flask Flask framework label Nov 7, 2018
@JavierLuna
Copy link

I attempted ( #27 ) to solve this problem creating a blueprint which adds all views to the spec.

The 'adding' part takes place when you register the blueprint:
app.register_blueprint(blueprint)

@killthekitten
Copy link

As long as you don't care about blueprint groupings, this code would iterate over all the routing rules and add paths accordingly, similar to what flask routes would do:

from apispec import APISpec
from apispec.ext.marshmallow import MarshmallowPlugin
from apispec_webframeworks.flask import FlaskPlugin
from flask import Flask

spec = APISpec(
    title="API",
    version="0.1.0",
    openapi_version="3.1.0",
    info={"description": "The API"},
    plugins=[MarshmallowPlugin(), FlaskPlugin()],
)


def init_apispec(app: Flask) -> None:
    with app.test_request_context():
        for rule in app.url_map.iter_rules():
            spec.path(view=app.view_functions[rule.endpoint])

Don't forget to add an init_apispec call to your application factory.

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

No branches or pull requests

4 participants