flask icon indicating copy to clipboard operation
flask copied to clipboard

Nested blueprint registration causes error when using test-scoped app fixtures

Open tachyondecay opened this issue 3 years ago • 1 comments

For apps that use nested blueprints, the way that Flask currently tracks how child blueprints have been registered causes both an error and a warning (that will soon be an error) when creating multiple app objects. This is a problem when testing.

For the below code, test_2 will fail because child has already been registered on parent, resulting in

ValueError: The name 'child' is already registered for this blueprint 'parent.child'. Use 'name=' to provide a unique name.

Additionally, the second call to parent.register_blueprint() emits a warning:

UserWarning: The setup method 'register_blueprint' can no longer be called on the blueprint 'parent'. It has already been registered at least once, any changes will not be applied consistently.
  Make sure all imports, decorators, functions, etc. needed to set up the blueprint are done before registering it.
  This warning will become an exception in Flask 2.3.
    parent.register_blueprint(child, url_prefix="/child")
import pytest

from flask import Blueprint, Flask

parent = Blueprint("parent", __name__)
child = Blueprint("child", __name__)


def create_app():
    app = Flask(__name__)

    parent.register_blueprint(child, url_prefix="/child")
    app.register_blueprint(parent, url_prefix="/parent")

    return app


@pytest.fixture
def app():
    app = create_app()
    yield app


def test_1(app):
    pass


def test_2(app):
    pass

User workarounds:

  • Session-scope the app fixture — this is what I'm currently doing but creates issues with test isolation in FlaskLoginClient from Flask-Login (but I worked around that too)
  • Create the blueprint objects inside create_app() — this isn't ideal because it makes registering routes on the blueprints more complex
  • Alter the parent blueprint's private attrs in the app fixture — adding these lines after the yield statement also fixes the problem for a given parent blueprint but is not a very flexible solution overall:
parent._blueprints = []
parent._got_registered_once = False

Environment:

  • Python version: 3.8.10
  • Flask version: 2.2.2

tachyondecay avatar Aug 25 '22 23:08 tachyondecay

What should be the expected behavior? Causes a warning (will soon be an error) looks like correct behavior. Do you think that there should be a method that makes these operations:

parent._blueprints = []
parent._got_registered_once = False

Maybe something like:

parent.reset_blueprint()

Or any another suggestion?

Thanks.

rasimandiran avatar Sep 07 '22 17:09 rasimandiran

Hi what do you mean by session scope the fixtures? please show an example

j0eTheRipper avatar Oct 24 '22 17:10 j0eTheRipper

Hi what do you mean by session scope the fixtures? please show an example

I've replied to you on Discord. Please limit discussion here to the issue itself and ask side questions there.

tachyondecay avatar Oct 24 '22 22:10 tachyondecay

@tachyondecay

The problem can be solve with a single line change which is causing the issue

import pytest

from flask import Blueprint, Flask

parent = Blueprint("parent", __name__)
child = Blueprint("child", __name__)


def create_app():
    app = Flask(__name__)

    parent.register_blueprint(child, url_prefix="/child")        <--- Cause of issue
    app.register_blueprint(parent, url_prefix="/parent")

    return app


@pytest.fixture
def app():
    app = create_app()
    yield app


def test_1(app):
    pass


def test_2(app):
    pass

The highlighted part is causing the issue since we are defining parent blueprint instance as global and registering child blueprint inside function so when test_1 is run it call create_app which register child over parent blueprint so it works perfectly but when test_2 call create_app again it tries to again register child blueprint to parent blueprint since it already registered it cause issue and error is thrown

To solve the problem we simple have to move the child blueprint registration to parent outside create_app function.

import pytest

from flask import Blueprint, Flask

parent = Blueprint("parent", __name__)
child = Blueprint("child", __name__)
parent.register_blueprint(child, url_prefix="/child")


def create_app():
    app = Flask(__name__)

    app.register_blueprint(parent, url_prefix="/parent")

    return app


@pytest.fixture
def app():
    app = create_app()
    yield app


def test_1(app):
    pass


def test_2(app):
    pass

ChandanChainani avatar Feb 03 '23 20:02 ChandanChainani

I think the problem here is illustrated by @ChandanChainani's comment/solution. Blueprints are global objects, you're mutating the same object each time you call create_app. In contrast, you're creating a new Flask instance inside that. If you were to define app outside the factory, you'd observe the same issue.

If the blueprints are always registered in the same way, you should move them outside the factory. If they change during setup, then maybe you want to create the "top" blueprint each time, like the Flask instance, then register the child blueprints before registering it on the app.

davidism avatar Feb 15 '23 15:02 davidism