feat(blueprints): 添加蓝图URL前缀动态计算功能

添加对嵌套蓝图URL前缀的动态计算支持,包括:
1. 将url_prefix改为属性并添加setter
2. 新增full_url_prefix属性获取完整前缀
3. 添加get_registered_url_prefix方法获取特定注册的前缀
4. 添加_registrations字典存储注册信息
5. 新增测试用例验证功能
This commit is contained in:
myl 2026-05-03 21:07:30 +08:00
parent 7374c85dde
commit ee141077bb
2 changed files with 239 additions and 1 deletions

View file

@ -199,7 +199,7 @@ class Blueprint(Scaffold):
raise ValueError("'name' may not contain a dot '.' character.")
self.name = name
self.url_prefix = url_prefix
self._url_prefix = url_prefix
self.subdomain = subdomain
self.deferred_functions: list[DeferredSetupFunction] = []
@ -209,6 +209,57 @@ class Blueprint(Scaffold):
self.url_values_defaults = url_defaults
self.cli_group = cli_group
self._blueprints: list[tuple[Blueprint, dict[str, t.Any]]] = []
self._registrations: dict[str, dict[str, t.Any]] = {}
@property
def url_prefix(self) -> str | None:
"""The URL prefix for this blueprint as set during initialization.
To get the full URL prefix including parent blueprint prefixes after
registration, use :attr:`full_url_prefix` or
:meth:`get_registered_url_prefix`.
.. versionadded:: 0.7
"""
return self._url_prefix
@url_prefix.setter
def url_prefix(self, value: str | None) -> None:
self._url_prefix = value
@property
def full_url_prefix(self) -> str | None:
"""The full URL prefix for this blueprint including any parent blueprint
prefixes.
If the blueprint has been registered exactly once, returns the full
URL prefix. If the blueprint has not been registered or has been
registered multiple times, returns None.
To get the URL prefix for a specific registration when the blueprint
has been registered multiple times, use :meth:`get_registered_url_prefix`.
.. versionadded:: 3.1
"""
if len(self._registrations) == 1:
return next(iter(self._registrations.values())).get("url_prefix")
return None
def get_registered_url_prefix(self, name: str | None = None) -> str | None:
"""Get the full URL prefix for a specific registration of this blueprint.
:param name: The registration name. If not provided and the blueprint
has been registered exactly once, returns that registration's prefix.
:return: The full URL prefix for the registration, or None if not found.
.. versionadded:: 3.1
"""
if name is None:
if len(self._registrations) == 1:
return next(iter(self._registrations.values())).get("url_prefix")
return None
registration = self._registrations.get(name)
return registration.get("url_prefix") if registration else None
def _check_setup_finished(self, f_name: str) -> None:
if self._got_registered_once:
@ -320,6 +371,13 @@ class Blueprint(Scaffold):
self._got_registered_once = True
state = self.make_setup_state(app, options, first_bp_registration)
self._registrations[name] = {
"url_prefix": state.url_prefix,
"subdomain": state.subdomain,
"name": name,
"options": options.copy(),
}
if self.has_static_folder:
state.add_url_rule(
f"{self.static_url_path}/<path:filename>",

View file

@ -0,0 +1,180 @@
"""Test cases for nested blueprint URL prefix dynamic calculation."""
import pytest
import flask
def test_blueprint_url_prefix_original():
"""Test that url_prefix returns the original value set during initialization."""
bp = flask.Blueprint("test", __name__, url_prefix="/test")
assert bp.url_prefix == "/test"
assert bp._url_prefix == "/test"
def test_blueprint_full_url_prefix_before_registration():
"""Test that full_url_prefix returns None before registration."""
bp = flask.Blueprint("test", __name__, url_prefix="/test")
assert bp.full_url_prefix is None
def test_blueprint_full_url_prefix_after_single_registration():
"""Test that full_url_prefix returns the full path after single registration."""
app = flask.Flask(__name__)
bp = flask.Blueprint("test", __name__, url_prefix="/test")
@bp.route("/")
def index():
return "test"
app.register_blueprint(bp, url_prefix="/api")
assert bp.url_prefix == "/test"
assert bp.full_url_prefix == "/api/test"
assert bp.get_registered_url_prefix() == "/api/test"
def test_nested_blueprint_full_url_prefix():
"""Test that nested blueprints get the full URL prefix including parent prefixes."""
app = flask.Flask(__name__)
parent = flask.Blueprint("parent", __name__, url_prefix="/parent")
child = flask.Blueprint("child", __name__, url_prefix="/child")
grandchild = flask.Blueprint("grandchild", __name__, url_prefix="/grandchild")
@parent.route("/")
def parent_index():
return "parent"
@child.route("/")
def child_index():
return "child"
@grandchild.route("/")
def grandchild_index():
return "grandchild"
child.register_blueprint(grandchild)
parent.register_blueprint(child)
app.register_blueprint(parent, url_prefix="/api")
assert parent.url_prefix == "/parent"
assert parent.full_url_prefix == "/api/parent"
assert child.url_prefix == "/child"
assert child.full_url_prefix == "/api/parent/child"
assert grandchild.url_prefix == "/grandchild"
assert grandchild.full_url_prefix == "/api/parent/child/grandchild"
def test_nested_blueprint_url_rules():
"""Test that nested blueprints have correct URL rules."""
app = flask.Flask(__name__)
parent = flask.Blueprint("parent", __name__, url_prefix="/parent")
child = flask.Blueprint("child", __name__, url_prefix="/child")
@parent.route("/home")
def parent_home():
return "parent home"
@child.route("/home")
def child_home():
return "child home"
parent.register_blueprint(child)
app.register_blueprint(parent, url_prefix="/api")
client = app.test_client()
assert client.get("/api/parent/home").data == b"parent home"
assert client.get("/api/parent/child/home").data == b"child home"
def test_multiple_registrations():
"""Test behavior when blueprint is registered multiple times."""
app = flask.Flask(__name__)
bp = flask.Blueprint("test", __name__, url_prefix="/test")
@bp.route("/")
def index():
return flask.request.endpoint
app.register_blueprint(bp, url_prefix="/api1")
app.register_blueprint(bp, name="test2", url_prefix="/api2")
assert bp.url_prefix == "/test"
assert bp.full_url_prefix is None
assert bp.get_registered_url_prefix() is None
assert bp.get_registered_url_prefix("test") == "/api1/test"
assert bp.get_registered_url_prefix("test2") == "/api2/test"
client = app.test_client()
assert client.get("/api1/test/").data == b"test.index"
assert client.get("/api2/test/").data == b"test2.index"
def test_get_registered_url_prefix_with_name():
"""Test get_registered_url_prefix with specific name."""
app = flask.Flask(__name__)
bp = flask.Blueprint("test", __name__, url_prefix="/test")
app.register_blueprint(bp, url_prefix="/api")
assert bp.get_registered_url_prefix("test") == "/api/test"
assert bp.get_registered_url_prefix("nonexistent") is None
def test_blueprint_without_url_prefix():
"""Test blueprint without initial url_prefix."""
app = flask.Flask(__name__)
bp = flask.Blueprint("test", __name__)
@bp.route("/")
def index():
return "test"
app.register_blueprint(bp, url_prefix="/api")
assert bp.url_prefix is None
assert bp.full_url_prefix == "/api"
assert bp.get_registered_url_prefix() == "/api"
def test_nested_with_partial_prefixes():
"""Test nested blueprints with some missing prefixes."""
app = flask.Flask(__name__)
parent = flask.Blueprint("parent", __name__)
child = flask.Blueprint("child", __name__, url_prefix="/child")
grandchild = flask.Blueprint("grandchild", __name__)
@parent.route("/")
def parent_index():
return "parent"
@child.route("/")
def child_index():
return "child"
@grandchild.route("/")
def grandchild_index():
return "grandchild"
child.register_blueprint(grandchild, url_prefix="/gc")
parent.register_blueprint(child)
app.register_blueprint(parent, url_prefix="/api")
assert parent.full_url_prefix == "/api"
assert child.full_url_prefix == "/api/child"
assert grandchild.full_url_prefix == "/api/child/gc"
client = app.test_client()
assert client.get("/api/").data == b"parent"
assert client.get("/api/child/").data == b"child"
assert client.get("/api/child/gc/").data == b"grandchild"
if __name__ == "__main__":
pytest.main([__file__, "-v"])