Skip to content

Commit ee14107

Browse files
author
myl
committed
feat(blueprints): 添加蓝图URL前缀动态计算功能
添加对嵌套蓝图URL前缀的动态计算支持,包括: 1. 将url_prefix改为属性并添加setter 2. 新增full_url_prefix属性获取完整前缀 3. 添加get_registered_url_prefix方法获取特定注册的前缀 4. 添加_registrations字典存储注册信息 5. 新增测试用例验证功能
1 parent 7374c85 commit ee14107

2 files changed

Lines changed: 239 additions & 1 deletion

File tree

src/flask/sansio/blueprints.py

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ def __init__(
199199
raise ValueError("'name' may not contain a dot '.' character.")
200200

201201
self.name = name
202-
self.url_prefix = url_prefix
202+
self._url_prefix = url_prefix
203203
self.subdomain = subdomain
204204
self.deferred_functions: list[DeferredSetupFunction] = []
205205

@@ -209,6 +209,57 @@ def __init__(
209209
self.url_values_defaults = url_defaults
210210
self.cli_group = cli_group
211211
self._blueprints: list[tuple[Blueprint, dict[str, t.Any]]] = []
212+
self._registrations: dict[str, dict[str, t.Any]] = {}
213+
214+
@property
215+
def url_prefix(self) -> str | None:
216+
"""The URL prefix for this blueprint as set during initialization.
217+
218+
To get the full URL prefix including parent blueprint prefixes after
219+
registration, use :attr:`full_url_prefix` or
220+
:meth:`get_registered_url_prefix`.
221+
222+
.. versionadded:: 0.7
223+
"""
224+
return self._url_prefix
225+
226+
@url_prefix.setter
227+
def url_prefix(self, value: str | None) -> None:
228+
self._url_prefix = value
229+
230+
@property
231+
def full_url_prefix(self) -> str | None:
232+
"""The full URL prefix for this blueprint including any parent blueprint
233+
prefixes.
234+
235+
If the blueprint has been registered exactly once, returns the full
236+
URL prefix. If the blueprint has not been registered or has been
237+
registered multiple times, returns None.
238+
239+
To get the URL prefix for a specific registration when the blueprint
240+
has been registered multiple times, use :meth:`get_registered_url_prefix`.
241+
242+
.. versionadded:: 3.1
243+
"""
244+
if len(self._registrations) == 1:
245+
return next(iter(self._registrations.values())).get("url_prefix")
246+
return None
247+
248+
def get_registered_url_prefix(self, name: str | None = None) -> str | None:
249+
"""Get the full URL prefix for a specific registration of this blueprint.
250+
251+
:param name: The registration name. If not provided and the blueprint
252+
has been registered exactly once, returns that registration's prefix.
253+
:return: The full URL prefix for the registration, or None if not found.
254+
255+
.. versionadded:: 3.1
256+
"""
257+
if name is None:
258+
if len(self._registrations) == 1:
259+
return next(iter(self._registrations.values())).get("url_prefix")
260+
return None
261+
registration = self._registrations.get(name)
262+
return registration.get("url_prefix") if registration else None
212263

213264
def _check_setup_finished(self, f_name: str) -> None:
214265
if self._got_registered_once:
@@ -320,6 +371,13 @@ def register(self, app: App, options: dict[str, t.Any]) -> None:
320371
self._got_registered_once = True
321372
state = self.make_setup_state(app, options, first_bp_registration)
322373

374+
self._registrations[name] = {
375+
"url_prefix": state.url_prefix,
376+
"subdomain": state.subdomain,
377+
"name": name,
378+
"options": options.copy(),
379+
}
380+
323381
if self.has_static_folder:
324382
state.add_url_rule(
325383
f"{self.static_url_path}/<path:filename>",

test_nested_blueprint_prefix.py

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
"""Test cases for nested blueprint URL prefix dynamic calculation."""
2+
import pytest
3+
import flask
4+
5+
6+
def test_blueprint_url_prefix_original():
7+
"""Test that url_prefix returns the original value set during initialization."""
8+
bp = flask.Blueprint("test", __name__, url_prefix="/test")
9+
10+
assert bp.url_prefix == "/test"
11+
assert bp._url_prefix == "/test"
12+
13+
14+
def test_blueprint_full_url_prefix_before_registration():
15+
"""Test that full_url_prefix returns None before registration."""
16+
bp = flask.Blueprint("test", __name__, url_prefix="/test")
17+
18+
assert bp.full_url_prefix is None
19+
20+
21+
def test_blueprint_full_url_prefix_after_single_registration():
22+
"""Test that full_url_prefix returns the full path after single registration."""
23+
app = flask.Flask(__name__)
24+
bp = flask.Blueprint("test", __name__, url_prefix="/test")
25+
26+
@bp.route("/")
27+
def index():
28+
return "test"
29+
30+
app.register_blueprint(bp, url_prefix="/api")
31+
32+
assert bp.url_prefix == "/test"
33+
assert bp.full_url_prefix == "/api/test"
34+
assert bp.get_registered_url_prefix() == "/api/test"
35+
36+
37+
def test_nested_blueprint_full_url_prefix():
38+
"""Test that nested blueprints get the full URL prefix including parent prefixes."""
39+
app = flask.Flask(__name__)
40+
41+
parent = flask.Blueprint("parent", __name__, url_prefix="/parent")
42+
child = flask.Blueprint("child", __name__, url_prefix="/child")
43+
grandchild = flask.Blueprint("grandchild", __name__, url_prefix="/grandchild")
44+
45+
@parent.route("/")
46+
def parent_index():
47+
return "parent"
48+
49+
@child.route("/")
50+
def child_index():
51+
return "child"
52+
53+
@grandchild.route("/")
54+
def grandchild_index():
55+
return "grandchild"
56+
57+
child.register_blueprint(grandchild)
58+
parent.register_blueprint(child)
59+
app.register_blueprint(parent, url_prefix="/api")
60+
61+
assert parent.url_prefix == "/parent"
62+
assert parent.full_url_prefix == "/api/parent"
63+
64+
assert child.url_prefix == "/child"
65+
assert child.full_url_prefix == "/api/parent/child"
66+
67+
assert grandchild.url_prefix == "/grandchild"
68+
assert grandchild.full_url_prefix == "/api/parent/child/grandchild"
69+
70+
71+
def test_nested_blueprint_url_rules():
72+
"""Test that nested blueprints have correct URL rules."""
73+
app = flask.Flask(__name__)
74+
75+
parent = flask.Blueprint("parent", __name__, url_prefix="/parent")
76+
child = flask.Blueprint("child", __name__, url_prefix="/child")
77+
78+
@parent.route("/home")
79+
def parent_home():
80+
return "parent home"
81+
82+
@child.route("/home")
83+
def child_home():
84+
return "child home"
85+
86+
parent.register_blueprint(child)
87+
app.register_blueprint(parent, url_prefix="/api")
88+
89+
client = app.test_client()
90+
91+
assert client.get("/api/parent/home").data == b"parent home"
92+
assert client.get("/api/parent/child/home").data == b"child home"
93+
94+
95+
def test_multiple_registrations():
96+
"""Test behavior when blueprint is registered multiple times."""
97+
app = flask.Flask(__name__)
98+
bp = flask.Blueprint("test", __name__, url_prefix="/test")
99+
100+
@bp.route("/")
101+
def index():
102+
return flask.request.endpoint
103+
104+
app.register_blueprint(bp, url_prefix="/api1")
105+
app.register_blueprint(bp, name="test2", url_prefix="/api2")
106+
107+
assert bp.url_prefix == "/test"
108+
assert bp.full_url_prefix is None
109+
assert bp.get_registered_url_prefix() is None
110+
assert bp.get_registered_url_prefix("test") == "/api1/test"
111+
assert bp.get_registered_url_prefix("test2") == "/api2/test"
112+
113+
client = app.test_client()
114+
assert client.get("/api1/test/").data == b"test.index"
115+
assert client.get("/api2/test/").data == b"test2.index"
116+
117+
118+
def test_get_registered_url_prefix_with_name():
119+
"""Test get_registered_url_prefix with specific name."""
120+
app = flask.Flask(__name__)
121+
bp = flask.Blueprint("test", __name__, url_prefix="/test")
122+
123+
app.register_blueprint(bp, url_prefix="/api")
124+
125+
assert bp.get_registered_url_prefix("test") == "/api/test"
126+
assert bp.get_registered_url_prefix("nonexistent") is None
127+
128+
129+
def test_blueprint_without_url_prefix():
130+
"""Test blueprint without initial url_prefix."""
131+
app = flask.Flask(__name__)
132+
bp = flask.Blueprint("test", __name__)
133+
134+
@bp.route("/")
135+
def index():
136+
return "test"
137+
138+
app.register_blueprint(bp, url_prefix="/api")
139+
140+
assert bp.url_prefix is None
141+
assert bp.full_url_prefix == "/api"
142+
assert bp.get_registered_url_prefix() == "/api"
143+
144+
145+
def test_nested_with_partial_prefixes():
146+
"""Test nested blueprints with some missing prefixes."""
147+
app = flask.Flask(__name__)
148+
149+
parent = flask.Blueprint("parent", __name__)
150+
child = flask.Blueprint("child", __name__, url_prefix="/child")
151+
grandchild = flask.Blueprint("grandchild", __name__)
152+
153+
@parent.route("/")
154+
def parent_index():
155+
return "parent"
156+
157+
@child.route("/")
158+
def child_index():
159+
return "child"
160+
161+
@grandchild.route("/")
162+
def grandchild_index():
163+
return "grandchild"
164+
165+
child.register_blueprint(grandchild, url_prefix="/gc")
166+
parent.register_blueprint(child)
167+
app.register_blueprint(parent, url_prefix="/api")
168+
169+
assert parent.full_url_prefix == "/api"
170+
assert child.full_url_prefix == "/api/child"
171+
assert grandchild.full_url_prefix == "/api/child/gc"
172+
173+
client = app.test_client()
174+
assert client.get("/api/").data == b"parent"
175+
assert client.get("/api/child/").data == b"child"
176+
assert client.get("/api/child/gc/").data == b"grandchild"
177+
178+
179+
if __name__ == "__main__":
180+
pytest.main([__file__, "-v"])

0 commit comments

Comments
 (0)