Consistent component return type
This commit is contained in:
@@ -15,11 +15,8 @@
|
|||||||
- `games/templatetags/randomid.py` uses the same hash-based approach
|
- `games/templatetags/randomid.py` uses the same hash-based approach
|
||||||
- Fixes: caching (Popover output now cacheable), page consistency, thread safety
|
- Fixes: caching (Popover output now cacheable), page consistency, thread safety
|
||||||
|
|
||||||
### 1. Inconsistent return types
|
### 1. Inconsistent return types (completed)
|
||||||
`Div()`/`A()`/`Button()` return `str`, but `LinkedPurchase()`/`NameWithIcon()` return `SafeText`.
|
All component functions now return `SafeText` and are annotated accordingly. Redundant `mark_safe()` wrappers removed from `LinkedPurchase()` and `NameWithIcon()`.
|
||||||
Forces callers to remember `mark_safe()` wrapping.
|
|
||||||
|
|
||||||
**Fix**: Standardize — all component functions should return the same type.
|
|
||||||
|
|
||||||
### 2. Fragile A() URL resolution
|
### 2. Fragile A() URL resolution
|
||||||
Tries `reverse(url)` first, then falls back to literal string. Uses `type(url) is str`
|
Tries `reverse(url)` first, then falls back to literal string. Uses `type(url) is str`
|
||||||
|
|||||||
+11
-11
@@ -40,7 +40,7 @@ def Component(
|
|||||||
children: list[HTMLTag] | HTMLTag = [],
|
children: list[HTMLTag] | HTMLTag = [],
|
||||||
template: str = "",
|
template: str = "",
|
||||||
tag_name: str = "",
|
tag_name: str = "",
|
||||||
) -> HTMLTag:
|
) -> SafeText:
|
||||||
if not tag_name and not template:
|
if not tag_name and not template:
|
||||||
raise ValueError("One of template or tag_name is required.")
|
raise ValueError("One of template or tag_name is required.")
|
||||||
if isinstance(children, str):
|
if isinstance(children, str):
|
||||||
@@ -130,7 +130,7 @@ def A(
|
|||||||
attributes: list[HTMLAttribute] = [],
|
attributes: list[HTMLAttribute] = [],
|
||||||
children: list[HTMLTag] | HTMLTag = [],
|
children: list[HTMLTag] | HTMLTag = [],
|
||||||
url: str | Callable[..., Any] = "",
|
url: str | Callable[..., Any] = "",
|
||||||
):
|
) -> SafeText:
|
||||||
"""
|
"""
|
||||||
Returns the HTML tag "a".
|
Returns the HTML tag "a".
|
||||||
"url" can either be:
|
"url" can either be:
|
||||||
@@ -161,7 +161,7 @@ def Button(
|
|||||||
size: str = "base",
|
size: str = "base",
|
||||||
icon: bool = False,
|
icon: bool = False,
|
||||||
color: str = "blue",
|
color: str = "blue",
|
||||||
):
|
) -> SafeText:
|
||||||
return Component(
|
return Component(
|
||||||
template="cotton/button.html",
|
template="cotton/button.html",
|
||||||
attributes=attributes
|
attributes=attributes
|
||||||
@@ -178,7 +178,7 @@ def Button(
|
|||||||
def Div(
|
def Div(
|
||||||
attributes: list[HTMLAttribute] = [],
|
attributes: list[HTMLAttribute] = [],
|
||||||
children: list[HTMLTag] | HTMLTag = [],
|
children: list[HTMLTag] | HTMLTag = [],
|
||||||
):
|
) -> SafeText:
|
||||||
return Component(tag_name="div", attributes=attributes, children=children)
|
return Component(tag_name="div", attributes=attributes, children=children)
|
||||||
|
|
||||||
|
|
||||||
@@ -186,7 +186,7 @@ def Input(
|
|||||||
type: str = "text",
|
type: str = "text",
|
||||||
attributes: list[HTMLAttribute] = [],
|
attributes: list[HTMLAttribute] = [],
|
||||||
children: list[HTMLTag] | HTMLTag = [],
|
children: list[HTMLTag] | HTMLTag = [],
|
||||||
):
|
) -> SafeText:
|
||||||
return Component(
|
return Component(
|
||||||
tag_name="input", attributes=attributes + [("type", type)], children=children
|
tag_name="input", attributes=attributes + [("type", type)], children=children
|
||||||
)
|
)
|
||||||
@@ -197,7 +197,7 @@ def Form(
|
|||||||
method="get",
|
method="get",
|
||||||
attributes: list[HTMLAttribute] = [],
|
attributes: list[HTMLAttribute] = [],
|
||||||
children: list[HTMLTag] | HTMLTag = [],
|
children: list[HTMLTag] | HTMLTag = [],
|
||||||
):
|
) -> SafeText:
|
||||||
return Component(
|
return Component(
|
||||||
tag_name="form",
|
tag_name="form",
|
||||||
attributes=attributes + [("action", action), ("method", method)],
|
attributes=attributes + [("action", action), ("method", method)],
|
||||||
@@ -208,7 +208,7 @@ def Form(
|
|||||||
def Icon(
|
def Icon(
|
||||||
name: str,
|
name: str,
|
||||||
attributes: list[HTMLAttribute] = [],
|
attributes: list[HTMLAttribute] = [],
|
||||||
):
|
) -> SafeText:
|
||||||
try:
|
try:
|
||||||
result = Component(template=f"cotton/icon/{name}.html", attributes=attributes)
|
result = Component(template=f"cotton/icon/{name}.html", attributes=attributes)
|
||||||
except TemplateDoesNotExist:
|
except TemplateDoesNotExist:
|
||||||
@@ -254,7 +254,7 @@ def LinkedPurchase(purchase: Purchase) -> SafeText:
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
return mark_safe(A(url=link, children=[a_content]))
|
return A(url=link, children=[a_content])
|
||||||
|
|
||||||
|
|
||||||
def NameWithIcon(
|
def NameWithIcon(
|
||||||
@@ -297,17 +297,17 @@ def NameWithIcon(
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
return mark_safe(
|
return (
|
||||||
A(
|
A(
|
||||||
url=link,
|
url=link,
|
||||||
children=[content],
|
children=[content],
|
||||||
)
|
)
|
||||||
if create_link
|
if create_link
|
||||||
else content,
|
else content
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def PurchasePrice(purchase) -> str:
|
def PurchasePrice(purchase) -> SafeText:
|
||||||
return Popover(
|
return Popover(
|
||||||
popover_content=f"{floatformat(purchase.price)} {purchase.price_currency}",
|
popover_content=f"{floatformat(purchase.price)} {purchase.price_currency}",
|
||||||
wrapped_content=f"{floatformat(purchase.converted_price)} {purchase.converted_currency}",
|
wrapped_content=f"{floatformat(purchase.converted_price)} {purchase.converted_currency}",
|
||||||
|
|||||||
@@ -346,5 +346,59 @@ class TemplatetagRandomidTest(unittest.TestCase):
|
|||||||
self.assertTrue(all(c in "abcdef0123456789" for c in result))
|
self.assertTrue(all(c in "abcdef0123456789" for c in result))
|
||||||
|
|
||||||
|
|
||||||
|
class ComponentReturnTypeTest(unittest.TestCase):
|
||||||
|
"""Test that component functions return SafeText and render correctly."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
components.enable_cache()
|
||||||
|
components._render_cached.cache_clear()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
components._render_cached = components._render_cached_impl
|
||||||
|
|
||||||
|
def test_div_returns_safe_text(self):
|
||||||
|
result = components.Div([("class", "x")], "hello")
|
||||||
|
self.assertIsInstance(result, SafeText)
|
||||||
|
|
||||||
|
def test_div_deterministic(self):
|
||||||
|
r1 = components.Div([("class", "x")], "hello")
|
||||||
|
r2 = components.Div([("class", "x")], "hello")
|
||||||
|
self.assertEqual(r1, r2)
|
||||||
|
self.assertIn('<div class="x">hello</div>', r1)
|
||||||
|
|
||||||
|
def test_div_no_args(self):
|
||||||
|
result = components.Div(children="test")
|
||||||
|
self.assertIsInstance(result, SafeText)
|
||||||
|
self.assertIn('<div>test</div>', result)
|
||||||
|
|
||||||
|
def test_a_returns_safe_text(self):
|
||||||
|
result = components.A([], "link")
|
||||||
|
self.assertIsInstance(result, SafeText)
|
||||||
|
|
||||||
|
def test_a_literal_href(self):
|
||||||
|
result = components.A([], "x", url="/literal/path")
|
||||||
|
self.assertIn('href="/literal/path"', result)
|
||||||
|
|
||||||
|
def test_button_returns_safe_text(self):
|
||||||
|
result = components.Button([], "click")
|
||||||
|
self.assertIsInstance(result, SafeText)
|
||||||
|
self.assertIn("<button", result)
|
||||||
|
|
||||||
|
def test_button_default_colors(self):
|
||||||
|
result = components.Button([], "click")
|
||||||
|
self.assertIn("text-white bg-brand", result)
|
||||||
|
|
||||||
|
def test_name_with_icon_no_link(self):
|
||||||
|
result = components.NameWithIcon(name="Game", platform="Steam", linkify=False)
|
||||||
|
self.assertIsInstance(result, SafeText)
|
||||||
|
self.assertIn("Game", result)
|
||||||
|
self.assertNotIn("<a ", result)
|
||||||
|
|
||||||
|
def test_name_with_icon_no_trailing_comma(self):
|
||||||
|
result = components.NameWithIcon(name="Test", platform="Steam", linkify=False)
|
||||||
|
self.assertIsInstance(result, SafeText)
|
||||||
|
self.assertNotIsInstance(result, tuple)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user