Skip to content

Commit 7d6fe4f

Browse files
authored
Allow passing multiple attrs args into components (#117)
* Allow passing multiple attrs args into components * Fix and simplify typing for BaseElement __init__ * Remove unneeded list call * Add multiple dictionaries of attributes to docs
1 parent a960fed commit 7d6fe4f

File tree

4 files changed

+39
-26
lines changed

4 files changed

+39
-26
lines changed

docs/changelog.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Changelog
22

3+
## NEXT
4+
- Allow multiple attribute dictionaries when defining `Elements`.
5+
36
## 25.4.2 - 2025-04-16
47
- Fix import of `@deprecated()` annotation on Python >= 3.13. It is part of the `warnings` module, not the `typing` module. Fixes [issue #106]. [PR #107]
58

docs/usage.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,17 @@ dynamically.
298298

299299
```
300300

301+
You can also specify multiple dictionaries containing attributes. This is
302+
especially useful if you have variables with a common preset group of
303+
attributes, or are using helper functions to create a dictionary of attributes.
304+
305+
```pycon title="Using multiple dictionaries with attributes"
306+
>>> from htpy import button
307+
>>> print(button({"disabled": True}, {"hx-post": "/foo"}))
308+
<button disabled hx-post="/foo"></button>
309+
310+
```
311+
301312
### Boolean/Empty Attributes
302313

303314
In HTML, boolean attributes such as `disabled` are considered "true" when they

htpy/__init__.py

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -265,34 +265,28 @@ def __str__(self) -> _Markup:
265265

266266
@t.overload
267267
def __call__(
268-
self: BaseElementSelf, id_class: str, attrs: Mapping[str, Attribute], **kwargs: Attribute
268+
self: BaseElementSelf,
269+
id_class: str,
270+
/,
271+
*attrs: Mapping[str, Attribute],
272+
**kwargs: Attribute,
269273
) -> BaseElementSelf: ...
270274
@t.overload
271275
def __call__(
272-
self: BaseElementSelf, id_class: str = "", **kwargs: Attribute
276+
self: BaseElementSelf, /, *attrs: Mapping[str, Attribute], **kwargs: Attribute
273277
) -> BaseElementSelf: ...
274-
@t.overload
275-
def __call__(
276-
self: BaseElementSelf, attrs: Mapping[str, Attribute], **kwargs: Attribute
277-
) -> BaseElementSelf: ...
278-
@t.overload
279-
def __call__(self: BaseElementSelf, **kwargs: Attribute) -> BaseElementSelf: ...
280-
def __call__(self: BaseElementSelf, *args: t.Any, **kwargs: t.Any) -> BaseElementSelf:
281-
id_class = ""
282-
attrs: Mapping[str, Attribute] = {}
283-
284-
if len(args) == 1:
285-
if isinstance(args[0], str):
286-
# element(".foo")
287-
id_class = args[0]
288-
attrs = {}
289-
else:
290-
# element({"foo": "bar"})
291-
id_class = ""
292-
attrs = args[0]
293-
elif len(args) == 2:
294-
# element(".foo", {"bar": "baz"})
295-
id_class, attrs = args
278+
def __call__(self: BaseElementSelf, /, *args: t.Any, **kwargs: t.Any) -> BaseElementSelf:
279+
id_class: str = ""
280+
attr_dicts: t.Sequence[Mapping[str, Attribute]]
281+
attrs: dict[str, Attribute] = {}
282+
283+
if args and not isinstance(args[0], Mapping):
284+
id_class, *attr_dicts = args
285+
else:
286+
attr_dicts = args
287+
288+
for attr_dict in attr_dicts:
289+
attrs.update(attr_dict)
296290

297291
return self.__class__(
298292
self._name,

tests/test_attributes.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,15 +187,20 @@ def test_id_class_bad_format() -> None:
187187

188188

189189
def test_id_class_bad_type() -> None:
190-
with pytest.raises(TypeError, match="id/class strings must be str. got {'oops': 'yes'}"):
191-
div({"oops": "yes"}, {}) # type: ignore
190+
with pytest.raises(TypeError, match="id/class strings must be str. got 3"):
191+
div(3, {}) # type: ignore
192192

193193

194194
def test_id_class_and_kwargs(render: RenderFixture) -> None:
195195
result = div("#theid", for_="hello", data_foo="<bar")
196196
assert render(result) == ['<div id="theid" for="hello" data-foo="&lt;bar">', "</div>"]
197197

198198

199+
def test_multiple_attrs(render: RenderFixture) -> None:
200+
result = div({"a": "1"}, {"a": "2", "b": "2"}, {"c": "3"})
201+
assert render(result) == ['<div a="2" b="2" c="3">', "</div>"]
202+
203+
199204
def test_attrs_and_kwargs(render: RenderFixture) -> None:
200205
result = div({"a": "1", "for": "a"}, for_="b", b="2")
201206
assert render(result) == ['<div a="1" for="b" b="2">', "</div>"]

0 commit comments

Comments
 (0)