Skip to content

Commit 5ee8654

Browse files
committed
Improve endpoint support
Adds support for the full name of the endpoint (i.e. prefixed with the blueprint name), as well as a utility to check if the current blueprints are active.
1 parent b1fc58b commit 5ee8654

File tree

8 files changed

+154
-10
lines changed

8 files changed

+154
-10
lines changed

justfile

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export REQUIREMENTS_TXT := env('REQUIREMENTS', '')
1010
[private]
1111
prepare:
1212
pip install --quiet --upgrade pip
13-
pip install --quiet -r requirements/pip-tools.txt
13+
[[ -f requirements/local.txt ]] && pip install -r requirements/pip-tools.txt || pip install --quiet pip-tools
1414

1515
# lock the requirements files
1616
compile: prepare
@@ -19,7 +19,7 @@ compile: prepare
1919
# Install dependencies
2020
sync: prepare
2121
pip-sync requirements/dev.txt
22-
[[ -f requirements/local.txt ]] && pip install -r requirements/local.txt
22+
[[ -f requirements/local.txt ]] && pip install -r requirements/local.txt || true
2323
tox -p auto --notest
2424

2525
alias install := sync

src/bootlace/endpoint.py

+20-1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,25 @@ def from_name(cls, name: str, **kwargs: Any) -> "Endpoint":
6161

6262
return cls(name=name, url_kwargs=KeywordArguments(**kwargs), ignore_query=ignore_query)
6363

64+
@property
65+
def full_name(self) -> str:
66+
"""The full name of the endpoint"""
67+
if self.context is not None and "." not in self.name:
68+
return f"{self.context.name}.{self.name}"
69+
70+
return self.name
71+
72+
@property
73+
def blueprint(self) -> str | None:
74+
"""The blueprint for the endpoint"""
75+
if "." in self.name:
76+
return ".".join(self.name.split(".")[:-1])
77+
78+
if self.context is not None:
79+
return self.context.name
80+
81+
return None
82+
6483
@property
6584
def url(self) -> str:
6685
"""The URL for the endpoint"""
@@ -77,7 +96,7 @@ def build(self, **kwds: Any) -> str:
7796
@property
7897
def active(self) -> bool:
7998
"""Whether the endpoint is active"""
80-
return is_active_endpoint(self.name, self.url_kwargs, self.ignore_query)
99+
return is_active_endpoint(self.full_name, self.url_kwargs, self.ignore_query)
81100

82101
def __call__(self, **kwds: Any) -> str:
83102
return self.build(**kwds)

src/bootlace/links.py

+5
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,8 @@ def url(self) -> str:
7070
def active(self) -> bool:
7171
"""Whether the link is active, based on the current request endpoint."""
7272
return self.endpoint.active
73+
74+
@property
75+
def blueprint(self) -> str | None:
76+
"""The blueprint for the endpoint."""
77+
return self.endpoint.blueprint

src/bootlace/nav/nav.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def __tag__(self) -> tags.html_tag:
5454

5555
if (link := getattr(active_endpoint, "link", None)) is not None:
5656
if (endpoint := getattr(link, "endpoint", None)) is not None:
57-
ul["data-endpoint"] = endpoint
57+
ul["data-endpoint"] = endpoint.full_name
5858

5959
for item in self.items:
6060
ul.add(self.li(as_tag(item), __pretty=False))

src/bootlace/util.py

+6
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ def converter(value: str | T) -> T:
276276
def is_active_endpoint(endpoint: str, url_kwargs: Mapping[str, Any], ignore_query: bool = True) -> bool:
277277
"""Check if the current request is for the given endpoint and URL kwargs"""
278278
if request.endpoint != endpoint:
279+
print(f"endpoint: {request.endpoint} != {endpoint}")
279280
return False
280281

281282
if request.url_rule is None: # pragma: no cover
@@ -291,6 +292,11 @@ def is_active_endpoint(endpoint: str, url_kwargs: Mapping[str, Any], ignore_quer
291292
return url == request.path
292293

293294

295+
def is_active_blueprint(blueprint: str) -> bool:
296+
"""Check if the current request is for the given blueprint"""
297+
return request.blueprint == blueprint
298+
299+
294300
H = TypeVar("H", bound=tags.html_tag)
295301

296302

tests/test_endpoint.py

+39
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,42 @@ def test_endpoint_bp_url(app: Flask, bp: Blueprint) -> None:
5454

5555
with app.test_request_context("/"):
5656
assert endpoint.url == "/archive"
57+
58+
59+
def test_endpoint_bp_url_no_context(app: Flask, bp: Blueprint) -> None:
60+
61+
endpoint = Endpoint(context=None, name=f"{bp.name}.archive")
62+
63+
with app.test_request_context("/"):
64+
assert endpoint.url == "/archive"
65+
66+
67+
def test_endpoint_attributes(app: Flask, bp: Blueprint) -> None:
68+
69+
endpoint = Endpoint(context=bp, name="archive")
70+
71+
with app.test_request_context("/archive"):
72+
assert endpoint.full_name == "bp.archive"
73+
assert endpoint.blueprint == "bp"
74+
assert endpoint.url == "/archive"
75+
assert endpoint.active is True
76+
assert endpoint() == "/archive"
77+
assert endpoint(query="a") == "/archive?query=a"
78+
79+
with app.test_request_context("/"):
80+
assert endpoint.active is False
81+
82+
with app.test_request_context("/archive?query=a"):
83+
assert endpoint.active is True
84+
85+
endpoint = Endpoint(context=None, name="home")
86+
assert endpoint.blueprint is None
87+
88+
89+
def test_endpoint_active_context_with_fullname(app: Flask, bp: Blueprint) -> None:
90+
91+
endpoint = Endpoint(context=bp, name="bp.archive")
92+
93+
with app.test_request_context("/archive"):
94+
assert endpoint.active is True
95+
assert endpoint.blueprint == "bp"

tests/test_links.py

+6-5
Original file line numberDiff line numberDiff line change
@@ -32,20 +32,21 @@ def test_view(app: Flask) -> None:
3232
print(f"{built=}")
3333
print(f"{request.path=}")
3434

35-
assert view.active, "View should be active"
36-
assert view.enabled, "View should be enabled"
35+
assert view.active, f"{view} should be active"
36+
assert view.enabled, f"{view} should be enabled"
37+
assert view.blueprint is None
3738

3839
expected = '<a href="/">Test</a>'
3940
assert_same_html(expected, render(view))
4041

4142
with app.test_request_context("/"):
4243
view = View(text="Test", endpoint=Endpoint(name="other", ignore_query=False))
43-
assert not view.active, "View should not be active"
44+
assert not view.active, f"{view} should not be active"
4445

4546
with app.test_request_context("/foo"):
4647
view = View(text="Test", endpoint="index")
47-
assert not view.active, "View should not be active"
48+
assert not view.active, f"{view} should not be active"
4849

4950
with app.test_request_context("/static/foo"):
5051
view = View(text="Test", endpoint=Endpoint(name="static", url_kwargs={"filename": "foo.txt"}))
51-
assert not view.active, "View should not be active"
52+
assert not view.active, f"{view} should not be active"

tests/test_util.py

+75-1
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,50 @@
22

33
import pytest
44
from dominate import tags
5+
from flask import Blueprint
6+
from flask import Flask
57

68
from bootlace.util import as_tag
9+
from bootlace.util import is_active_blueprint
10+
from bootlace.util import is_active_endpoint
711
from bootlace.util import Tag
812

913

14+
@pytest.fixture
15+
def app(app: Flask) -> Flask:
16+
17+
@app.route("/")
18+
def home() -> str:
19+
return "Home"
20+
21+
@app.route("/about")
22+
def about() -> str:
23+
return "About"
24+
25+
@app.route("/contact")
26+
def contact() -> str:
27+
return "Contact"
28+
29+
return app
30+
31+
32+
@pytest.fixture
33+
def bp(app: Flask) -> Blueprint:
34+
bp = Blueprint("bp", __name__)
35+
36+
@bp.route("/archive")
37+
def archive() -> str:
38+
return "Archive"
39+
40+
@bp.route("/post/<id>")
41+
def post(id: str) -> str:
42+
return "Post"
43+
44+
app.register_blueprint(bp)
45+
46+
return bp
47+
48+
1049
class Taggable:
1150
def __tag__(self) -> tags.html_tag:
1251
return tags.div()
@@ -29,7 +68,7 @@ def test_as_tag(tag: Any, expected: str) -> None:
2968

3069
def test_as_tag_warning() -> None:
3170
with pytest.warns(UserWarning):
32-
assert as_tag(1).render() == "1\n<!--Rendered type int not supported-->\n" # type: ignore
71+
assert as_tag(1).render() == "1\n<!--Rendered type int not supported-->\n"
3372

3473

3574
def test_classes() -> None:
@@ -102,3 +141,38 @@ def test_tag_configurator() -> None:
102141
a.classes.discard("test")
103142

104143
assert as_tag(a).render() == '<a class="other" href="/test"></a>'
144+
145+
146+
@pytest.mark.usefixtures("bp")
147+
@pytest.mark.parametrize(
148+
"uri,endpoint,kwargs,expected",
149+
[
150+
("/", "home", {}, True),
151+
("/about", "home", {}, False),
152+
("/post/a", "bp.post", {"id": "a"}, True),
153+
("/post/b", "bp.post", {"id": "a"}, False),
154+
("/archive", "bp.archive", {}, True),
155+
],
156+
)
157+
def test_is_active_endpoint(app: Flask, uri: str, endpoint: str, kwargs: dict[str, str], expected: bool) -> None:
158+
159+
with app.test_request_context(uri):
160+
print(f"Testing {uri} -> {endpoint} with {kwargs}")
161+
assert is_active_endpoint(endpoint, kwargs) is expected
162+
163+
164+
@pytest.mark.usefixtures("bp")
165+
@pytest.mark.parametrize(
166+
"uri,blueprint,expected",
167+
[
168+
("/", None, True),
169+
("/about", "bp", False),
170+
("/post/a", "bp", True),
171+
("/archive", "bp", True),
172+
],
173+
)
174+
def test_is_active_blueprint(app: Flask, uri: str, blueprint: str, expected: bool) -> None:
175+
176+
with app.test_request_context(uri):
177+
print(f"Testing {uri} -> {blueprint}")
178+
assert is_active_blueprint(blueprint) is expected

0 commit comments

Comments
 (0)