Nested blueprint registration causes error when using test-scoped app fixtures
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
FlaskLoginClientfrom 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
yieldstatement 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
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.
Hi what do you mean by session scope the fixtures? please show an example
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
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
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.