pandabear · GitHub

Prior to version Flask 2.0.2, it was possible to register blueprints multiple of times with different url prefix during Flask App factory. Such as:

    # Flask 0.10.1
    from my_project.api.v1.user import user_api_v1
    app.register_blueprint(user_api_v1,
                           url_prefix='/api/latest/users')
    app.register_blueprint(user_api_v1,
                           url_prefix='/api/v1/users')
    from my_project.api.v1.recipe import recipe_api_v1
    app.register_blueprint(recipe_api_v1,
                           url_prefix='/api/latest/recipes')
    app.register_blueprint(recipe_api_v1,
                           url_prefix='/api/v1/recipes')

But with latest Flask version, the above was updated into this:

    from my_project.api.v1.user import user_api_v1
    app.register_blueprint(user_api_v1,
                           name='users_latest',
                           url_prefix='/api/latest/users')
    app.register_blueprint(user_api_v1,
                           name='users_v1',
                           url_prefix='/api/v1/users')
    from my_project.api.v1.recipe import recipe_api_v1
    app.register_blueprint(recipe_api_v1,
                           name='recipes_latest',
                           url_prefix='/api/latest/recipes')
    app.register_blueprint(recipe_api_v1,
                           name='recipes_v1',
                           url_prefix='/api/v1/recipes')

Basically, adding unique name. However, this change caused an issue that the error- and before_request-handlers that was registered to blueprints do not get mapped to the second blueprint.

This was the result for the new Flask App:

# App's url map
Map([<Rule '/api/latest/recipes/' (GET, HEAD, OPTIONS) -> recipes_latest.get_all>,
 <Rule '/api/latest/users/' (GET, HEAD, OPTIONS) -> users_latest.get_all>,
 <Rule '/api/v1/recipes/' (GET, HEAD, OPTIONS) -> recipes_v1.get_all>,
 <Rule '/api/v1/users/' (GET, HEAD, OPTIONS) -> users_v1.get_all>,
 <Rule '/api/latest/recipes/<id>' (POST, OPTIONS) -> recipes_latest.post>,
 <Rule '/api/v1/recipes/<id>' (POST, OPTIONS) -> recipes_v1.post>,
 <Rule '/api/latest/recipes/<id>' (GET, HEAD, OPTIONS) -> recipes_latest.get>,
 <Rule '/api/v1/recipes/<id>' (GET, HEAD, OPTIONS) -> recipes_v1.get>,
 <Rule '/static/<filename>' (GET, HEAD, OPTIONS) -> static>])
# App's before_request_funcs
defaultdict(<class 'list'>,
            {None: [<function requires_staff_permission at 0x7fe42b9be840>],
             'recipes_latest': [<function _require_chef_permission at 0x7fe429a67f28>],
             'users_latest': [<function _require_manager_permission at 0x7fe42acb90d0>]})
# App's error_handler_spec
defaultdict(<function Scaffold.__init__.<locals>.<lambda> at 0x7fe42ad0aa60>,
            {None: defaultdict(<class 'dict'>,
                               {None: {<class 'Exception'>: <function generic_error_handler.<locals>.error_handler at 0x7fe42ad0ab70>}}),
             'recipes_latest': defaultdict(<class 'dict'>,
                                           {None: {<class 'my_project.api.v1.recipe.MissingParameter'>: <function handle_missing_argument at 0x7fe429a68510>,
                                                   <class 'my_project.api.v1.recipe.MongoDocumentNotFound'>: <function handle_no_mongo_document at 0x7fe429a68400>,
                                                   <class 'my_project.api.v1.recipe.UnsupportedParameter'>: <function handle_invalid_argument at 0x7fe429a68488>}}),
             'users_latest': defaultdict(<class 'dict'>,
                                         {None: {<class 'my_project.api.v1.user.UnsupportedParameter'>: <function handle_invalid_argument at 0x7fe42acb92f0>}})})

What this means is that when request is done to GET /api/v1, it will not trigger any handlers, because handlers are registered to x_latest and /api/v1 was registered with x_v1 blueprint name.

The behaviour I'd expect after using app.register_blueprint for same blueprint object with different name is that the new registered name should also have same registered error- and before request-handler mapped like so:

# Expected App's before_request_funcs
defaultdict(<class 'list'>,
            {None: [<function requires_staff_permission at 0x7fe42b9be840>],
             'recipes_latest': [<function _require_chef_permission at 0x7fe429a67f28>],
             'recipes_v1': [<function _require_chef_permission at 0x7fe429a67f28>],
             'users_latest': [<function _require_manager_permission at 0x7fe42acb90d0>],
             'users_v1': [<function _require_manager_permission at 0x7fe42acb90d0>]})
# Expected App's error_handler_spec
defaultdict(<function Scaffold.__init__.<locals>.<lambda> at 0x7fe42ad0aa60>,
            {None: defaultdict(<class 'dict'>,
                               {None: {<class 'Exception'>: <function generic_error_handler.<locals>.error_handler at 0x7fe42ad0ab70>}}),
             'recipes_latest': defaultdict(<class 'dict'>,
                                           {None: {<class 'my_project.api.v1.recipe.MissingParameter'>: <function handle_missing_argument at 0x7fe429a68510>,
                                                   <class 'my_project.api.v1.recipe.MongoDocumentNotFound'>: <function handle_no_mongo_document at 0x7fe429a68400>,
                                                   <class 'my_project.api.v1.recipe.UnsupportedParameter'>: <function handle_invalid_argument at 0x7fe429a68488>}}),
             'recipes_v1': defaultdict(<class 'dict'>,
                                           {None: {<class 'my_project.api.v1.recipe.MissingParameter'>: <function handle_missing_argument at 0x7fe429a68510>,
                                                   <class 'my_project.api.v1.recipe.MongoDocumentNotFound'>: <function handle_no_mongo_document at 0x7fe429a68400>,
                                                   <class 'my_project.api.v1.recipe.UnsupportedParameter'>: <function handle_invalid_argument at 0x7fe429a68488>}}),
             'users_latest': defaultdict(<class 'dict'>,
                                         {None: {<class 'my_project.api.v1.iser.UnsupportedParameter'>: <function handle_invalid_argument at 0x7fe42acb92f0>}}),
             'users_v1': defaultdict(<class 'dict'>,
                                         {None: {<class 'my_project.api.v1.iser.UnsupportedParameter'>: <function handle_invalid_argument at 0x7fe42acb92f0>}})})

Environment:

  • Python version: 3.6.8
  • Flask version: 2.0.2

Read the original on github.com ↗