Skip to content

Commit

Permalink
Raise errors for invalid attributes directly on __call__.
Browse files Browse the repository at this point in the history
This commit transforms attributes to strings directly when being called
rather at rendering time. This makes exceptions for invalid attributes
appear directly leading to clearer stack traces.

Fixes #49.
  • Loading branch information
pelme committed Sep 8, 2024
1 parent fa20d50 commit b457d12
Show file tree
Hide file tree
Showing 4 changed files with 35 additions and 12 deletions.
3 changes: 3 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Changelog

## next
- Raise errors directly on invalid attributes. This avoids cryptic stack traces for invalid attributes. [Issue #49](https://github.com/pelme/htpy/issues/49) [PR #55](https://github.com/pelme/htpy/pull/55).

## 24.8.3 - 2024-08-28
- Support passing htpy elements directly to Starlette responses. Document Starlette support. [PR #50](https://github.com/pelme/htpy/pull/50).
- Allow passing ints to attributes and children [PR #52](https://github.com/pelme/htpy/pull/52).
Expand Down
20 changes: 20 additions & 0 deletions examples/bad_attribute.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from htpy import div


def good_component_a():
return div[good_component_b()]


def good_component_b():
return div[good_component_c()]


def good_component_c():
return div[bad_component()]


def bad_component():
return div(a=object())


print(str(good_component_a()))
22 changes: 11 additions & 11 deletions htpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,11 +213,9 @@ def __getattr__(name: str) -> Element:
class BaseElement:
__slots__ = ("_name", "_attrs", "_children")

def __init__(
self, name: str, attrs: dict[str, Attribute] | None = None, children: Node = None
) -> None:
def __init__(self, name: str, attrs_str: str = "", children: Node = None) -> None:
self._name = name
self._attrs = attrs or {}
self._attrs = attrs_str
self._children = children

def __str__(self) -> _Markup:
Expand Down Expand Up @@ -256,19 +254,21 @@ def __call__(self: BaseElementSelf, *args: t.Any, **kwargs: t.Any) -> BaseElemen

return self.__class__(
self._name,
{
**(_id_class_names_from_css_str(id_class) if id_class else {}),
**attrs,
**{_kwarg_attribute_name(k): v for k, v in kwargs.items()},
},
_attrs_string(
{
**(_id_class_names_from_css_str(id_class) if id_class else {}),
**attrs,
**{_kwarg_attribute_name(k): v for k, v in kwargs.items()},
}
),
self._children,
)

def __iter__(self) -> Iterator[str]:
return self._iter_context({})

def _iter_context(self, ctx: dict[Context[t.Any], t.Any]) -> Iterator[str]:
yield f"<{self._name}{_attrs_string(self._attrs)}>"
yield f"<{self._name}{self._attrs}>"
yield from _iter_node_context(self._children, ctx)
yield f"</{self._name}>"

Expand Down Expand Up @@ -301,7 +301,7 @@ def _iter_context(self, ctx: dict[Context[t.Any], t.Any]) -> Iterator[str]:

class VoidElement(BaseElement):
def _iter_context(self, ctx: dict[Context[t.Any], t.Any]) -> Iterator[str]:
yield f"<{self._name}{_attrs_string(self._attrs)}>"
yield f"<{self._name}{self._attrs}>"


def render_node(node: Node) -> _Markup:
Expand Down
2 changes: 1 addition & 1 deletion tests/test_attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,4 +198,4 @@ def test_invalid_attribute_key(not_an_attr: t.Any) -> None:
)
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))
div(foo=not_an_attr)

0 comments on commit b457d12

Please sign in to comment.