Skip to content

Commit

Permalink
Improve endpoint support
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
alexrudy committed Nov 28, 2024
1 parent b1fc58b commit 5ee8654
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 10 deletions.
4 changes: 2 additions & 2 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export REQUIREMENTS_TXT := env('REQUIREMENTS', '')
[private]
prepare:
pip install --quiet --upgrade pip
pip install --quiet -r requirements/pip-tools.txt
[[ -f requirements/local.txt ]] && pip install -r requirements/pip-tools.txt || pip install --quiet pip-tools

# lock the requirements files
compile: prepare
Expand All @@ -19,7 +19,7 @@ compile: prepare
# Install dependencies
sync: prepare
pip-sync requirements/dev.txt
[[ -f requirements/local.txt ]] && pip install -r requirements/local.txt
[[ -f requirements/local.txt ]] && pip install -r requirements/local.txt || true
tox -p auto --notest

alias install := sync
Expand Down
21 changes: 20 additions & 1 deletion src/bootlace/endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,25 @@ def from_name(cls, name: str, **kwargs: Any) -> "Endpoint":

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

@property
def full_name(self) -> str:
"""The full name of the endpoint"""
if self.context is not None and "." not in self.name:
return f"{self.context.name}.{self.name}"

return self.name

@property
def blueprint(self) -> str | None:
"""The blueprint for the endpoint"""
if "." in self.name:
return ".".join(self.name.split(".")[:-1])

if self.context is not None:
return self.context.name

return None

@property
def url(self) -> str:
"""The URL for the endpoint"""
Expand All @@ -77,7 +96,7 @@ def build(self, **kwds: Any) -> str:
@property
def active(self) -> bool:
"""Whether the endpoint is active"""
return is_active_endpoint(self.name, self.url_kwargs, self.ignore_query)
return is_active_endpoint(self.full_name, self.url_kwargs, self.ignore_query)

def __call__(self, **kwds: Any) -> str:
return self.build(**kwds)
Expand Down
5 changes: 5 additions & 0 deletions src/bootlace/links.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,8 @@ def url(self) -> str:
def active(self) -> bool:
"""Whether the link is active, based on the current request endpoint."""
return self.endpoint.active

@property
def blueprint(self) -> str | None:
"""The blueprint for the endpoint."""
return self.endpoint.blueprint
2 changes: 1 addition & 1 deletion src/bootlace/nav/nav.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def __tag__(self) -> tags.html_tag:

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

for item in self.items:
ul.add(self.li(as_tag(item), __pretty=False))
Expand Down
6 changes: 6 additions & 0 deletions src/bootlace/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ def converter(value: str | T) -> T:
def is_active_endpoint(endpoint: str, url_kwargs: Mapping[str, Any], ignore_query: bool = True) -> bool:
"""Check if the current request is for the given endpoint and URL kwargs"""
if request.endpoint != endpoint:
print(f"endpoint: {request.endpoint} != {endpoint}")
return False

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


def is_active_blueprint(blueprint: str) -> bool:
"""Check if the current request is for the given blueprint"""
return request.blueprint == blueprint


H = TypeVar("H", bound=tags.html_tag)


Expand Down
39 changes: 39 additions & 0 deletions tests/test_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,42 @@ def test_endpoint_bp_url(app: Flask, bp: Blueprint) -> None:

with app.test_request_context("/"):
assert endpoint.url == "/archive"


def test_endpoint_bp_url_no_context(app: Flask, bp: Blueprint) -> None:

endpoint = Endpoint(context=None, name=f"{bp.name}.archive")

with app.test_request_context("/"):
assert endpoint.url == "/archive"


def test_endpoint_attributes(app: Flask, bp: Blueprint) -> None:

endpoint = Endpoint(context=bp, name="archive")

with app.test_request_context("/archive"):
assert endpoint.full_name == "bp.archive"
assert endpoint.blueprint == "bp"
assert endpoint.url == "/archive"
assert endpoint.active is True
assert endpoint() == "/archive"
assert endpoint(query="a") == "/archive?query=a"

with app.test_request_context("/"):
assert endpoint.active is False

with app.test_request_context("/archive?query=a"):
assert endpoint.active is True

endpoint = Endpoint(context=None, name="home")
assert endpoint.blueprint is None


def test_endpoint_active_context_with_fullname(app: Flask, bp: Blueprint) -> None:

endpoint = Endpoint(context=bp, name="bp.archive")

with app.test_request_context("/archive"):
assert endpoint.active is True
assert endpoint.blueprint == "bp"
11 changes: 6 additions & 5 deletions tests/test_links.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,21 @@ def test_view(app: Flask) -> None:
print(f"{built=}")
print(f"{request.path=}")

assert view.active, "View should be active"
assert view.enabled, "View should be enabled"
assert view.active, f"{view} should be active"
assert view.enabled, f"{view} should be enabled"
assert view.blueprint is None

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

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

with app.test_request_context("/foo"):
view = View(text="Test", endpoint="index")
assert not view.active, "View should not be active"
assert not view.active, f"{view} should not be active"

with app.test_request_context("/static/foo"):
view = View(text="Test", endpoint=Endpoint(name="static", url_kwargs={"filename": "foo.txt"}))
assert not view.active, "View should not be active"
assert not view.active, f"{view} should not be active"
76 changes: 75 additions & 1 deletion tests/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,50 @@

import pytest
from dominate import tags
from flask import Blueprint
from flask import Flask

from bootlace.util import as_tag
from bootlace.util import is_active_blueprint
from bootlace.util import is_active_endpoint
from bootlace.util import Tag


@pytest.fixture
def app(app: Flask) -> Flask:

@app.route("/")
def home() -> str:
return "Home"

@app.route("/about")
def about() -> str:
return "About"

@app.route("/contact")
def contact() -> str:
return "Contact"

return app


@pytest.fixture
def bp(app: Flask) -> Blueprint:
bp = Blueprint("bp", __name__)

@bp.route("/archive")
def archive() -> str:
return "Archive"

@bp.route("/post/<id>")
def post(id: str) -> str:
return "Post"

app.register_blueprint(bp)

return bp


class Taggable:
def __tag__(self) -> tags.html_tag:
return tags.div()
Expand All @@ -29,7 +68,7 @@ def test_as_tag(tag: Any, expected: str) -> None:

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


def test_classes() -> None:
Expand Down Expand Up @@ -102,3 +141,38 @@ def test_tag_configurator() -> None:
a.classes.discard("test")

assert as_tag(a).render() == '<a class="other" href="/test"></a>'


@pytest.mark.usefixtures("bp")
@pytest.mark.parametrize(
"uri,endpoint,kwargs,expected",
[
("/", "home", {}, True),
("/about", "home", {}, False),
("/post/a", "bp.post", {"id": "a"}, True),
("/post/b", "bp.post", {"id": "a"}, False),
("/archive", "bp.archive", {}, True),
],
)
def test_is_active_endpoint(app: Flask, uri: str, endpoint: str, kwargs: dict[str, str], expected: bool) -> None:

with app.test_request_context(uri):
print(f"Testing {uri} -> {endpoint} with {kwargs}")
assert is_active_endpoint(endpoint, kwargs) is expected


@pytest.mark.usefixtures("bp")
@pytest.mark.parametrize(
"uri,blueprint,expected",
[
("/", None, True),
("/about", "bp", False),
("/post/a", "bp", True),
("/archive", "bp", True),
],
)
def test_is_active_blueprint(app: Flask, uri: str, blueprint: str, expected: bool) -> None:

with app.test_request_context(uri):
print(f"Testing {uri} -> {blueprint}")
assert is_active_blueprint(blueprint) is expected

0 comments on commit 5ee8654

Please sign in to comment.