Skip to content

Commit

Permalink
Enforce attributes being strings.
Browse files Browse the repository at this point in the history
  • Loading branch information
pelme committed Aug 16, 2024
1 parent b4baa7b commit 82c8415
Show file tree
Hide file tree
Showing 3 changed files with 30 additions and 3 deletions.
3 changes: 3 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
#42](https://github.com/pelme/htpy/issues/42).
- Run tests on Python 3.13 RC (no changes were required, earlier versions
should work fine too). [PR #45](https://github.com/pelme/htpy/pull/45).
- Attributes that are not strings will now be rejected runtime. Attributes have
been typed as strings previously but this is now also enforced during runtime.
If you need to pass non-strings as attribute values, wrap them in str() calls.

## 24.8.0 - 2024-08-03
- Allow conditional rendering based on `bool`. [PR #40](https://github.com/pelme/htpy/pull/41).
Expand Down
13 changes: 10 additions & 3 deletions htpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,10 @@ def _kwarg_attribute_name(name: str) -> str:

def _generate_attrs(raw_attrs: dict[str, Attribute]) -> Iterable[tuple[str, Attribute]]:
for key, value in raw_attrs.items():
if value in (False, None):
if not isinstance(key, str): # pyright: ignore [reportUnnecessaryIsInstance]
raise ValueError("Attribute key must be a string")

if value is False or value is None:
continue

if key == "class":
Expand All @@ -91,6 +94,9 @@ def _generate_attrs(raw_attrs: dict[str, Attribute]) -> Iterable[tuple[str, Attr
yield _force_escape(key), True

else:
if not isinstance(value, str | _HasHtml):
raise ValueError(f"Attribute value must be a string , got {value!r}")

yield _force_escape(key), _force_escape(value)


Expand Down Expand Up @@ -118,9 +124,9 @@ def iter_node(x: Node) -> Iterator[str]:

if isinstance(x, BaseElement):
yield from x
elif isinstance(x, str) or hasattr(x, "__html__"):
elif isinstance(x, str | _HasHtml):
yield str(_escape(x))
elif isinstance(x, Iterable):
elif isinstance(x, Iterable): # pyright: ignore [reportUnnecessaryIsInstance]
for child in x:
yield from iter_node(child)
else:
Expand Down Expand Up @@ -234,6 +240,7 @@ def comment(text: str) -> _Markup:
return _Markup(f"<!-- {escaped_text} -->")


@t.runtime_checkable
class _HasHtml(t.Protocol):
def __html__(self) -> str: ...

Expand Down
17 changes: 17 additions & 0 deletions tests/test_attributes.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import typing as t

import pytest
from markupsafe import Markup

Expand Down Expand Up @@ -177,3 +179,18 @@ def test_class_priority() -> None:
def test_attribute_priority() -> None:
result = div({"foo": "a"}, foo="b")
assert str(result) == """<div foo="b"></div>"""


@pytest.mark.parametrize("not_an_attr", [1234, b"foo", object(), object, 1, 0, None])
def test_invalid_attribute_key(not_an_attr: t.Any) -> None:
with pytest.raises(ValueError, match="Attribute key must be a string"):
str(div({not_an_attr: "foo"}))


@pytest.mark.parametrize(
"not_an_attr",
[1234, b"foo", object(), object, 1, 0],
)
def test_invalid_attribute_value(not_an_attr: t.Any) -> None:
with pytest.raises(ValueError, match="Attribute value must be a string"):
str(div(foo=not_an_attr))

0 comments on commit 82c8415

Please sign in to comment.