diff --git a/docs/changelog.md b/docs/changelog.md index d6fa55b..82a4135 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -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). diff --git a/examples/bad_attribute.py b/examples/bad_attribute.py new file mode 100644 index 0000000..4d4a527 --- /dev/null +++ b/examples/bad_attribute.py @@ -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())) diff --git a/htpy/__init__.py b/htpy/__init__.py index 6799d49..0749603 100644 --- a/htpy/__init__.py +++ b/htpy/__init__.py @@ -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: @@ -256,11 +254,13 @@ 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, ) @@ -268,7 +268,7 @@ 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"" @@ -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: diff --git a/tests/test_attributes.py b/tests/test_attributes.py index c129c98..703a73f 100644 --- a/tests/test_attributes.py +++ b/tests/test_attributes.py @@ -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)