Skip to content

Commit 5fd2f02

Browse files
committed
Add enum_attribute
1 parent 16bdcf8 commit 5fd2f02

File tree

4 files changed

+120
-8
lines changed

4 files changed

+120
-8
lines changed

NEWS.md

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
1-
# News in version 2.1.0
2-
3-
## API Additions
4-
5-
* Add `autocomplete` attribute to `Input`, `TextArea`, `Select`, and `Form`.
6-
71
# News in version 2.0.0
82

93
## API Additions
104

115
* Add `Button.disabled`.
6+
* Add `autocomplete` attribute to `Input`, `TextArea`, `Select`, and `Form`.
7+
* Add `enum_attribute`.
128

139
## Incompatible Changes
1410

htmlgen/attribute.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import datetime
2+
from enum import Enum
23

34
from htmlgen.timeutil import parse_rfc3339_partial_time
45

@@ -273,3 +274,55 @@ def __set__(self, obj, value):
273274
obj.add_css_classes(self._css_class)
274275
else:
275276
obj.remove_css_classes(self._css_class)
277+
278+
279+
class enum_attribute:
280+
"""Add an attribute to an HTML element that only allows limited values.
281+
282+
>>> from enum import Enum
283+
>>> from htmlgen import Element
284+
>>> class MyEnum(Enum):
285+
... FOO = "foo"
286+
... BAR = "bar"
287+
>>> class MyElement(Element):
288+
... value = enum_attribute("value", MyEnum)
289+
>>> element = MyElement("div")
290+
>>> element.value
291+
>>> str(element)
292+
'<div></div>'
293+
>>> element.value = MyEnum.FOO
294+
>>> str(element)
295+
'<div value="foo"></div>'
296+
297+
If the optional default argument is given, the attribute will not be
298+
included if the value matches it.
299+
300+
>>> class MyElement(Element):
301+
... value = enum_attribute("value", MyEnum, default=MyEnum.FOO)
302+
>>> element = MyElement("div")
303+
>>> element.value
304+
MyEnum.FOO
305+
>>> str(element)
306+
'<div></div>'
307+
"""
308+
309+
def __init__(self, attribute_name, enum, default=None):
310+
if not issubclass(enum, Enum):
311+
raise TypeError("enum must be an Enum class")
312+
self._attribute_name = attribute_name
313+
self._enum = enum
314+
self._default = default
315+
316+
def __get__(self, obj, _=None):
317+
value = obj.get_attribute(self._attribute_name, None)
318+
if value is None:
319+
return self._default
320+
return self._enum(value)
321+
322+
def __set__(self, obj, value):
323+
if value is None:
324+
obj.remove_attribute(self._attribute_name)
325+
elif not isinstance(value, self._enum):
326+
raise TypeError("value must be an {}".format(self._enum))
327+
else:
328+
obj.set_attribute(self._attribute_name, value.value)

htmlgen/attribute.pyi

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import datetime
2-
from typing import Optional, List, Iterable
2+
from enum import Enum
3+
from typing import Generic, Iterable, List, Optional, Type, TypeVar
34

45
from htmlgen.element import Element
56

7+
_E = TypeVar("_E", bound=Enum)
8+
69
class html_attribute:
710
def __init__(
811
self, attribute_name: str, default: Optional[str] = ...
@@ -62,3 +65,12 @@ class css_class_attribute:
6265
def __init__(self, css_class: str) -> None: ...
6366
def __get__(self, obj: Element, type_: Optional[type] = ...) -> bool: ...
6467
def __set__(self, obj: Element, value: bool) -> None: ...
68+
69+
class enum_attribute(Generic[_E]):
70+
def __init__(
71+
self, attribute_name: str, enum: Type[_E], default: Optional[_E] = ...
72+
) -> None: ...
73+
def __get__(
74+
self, obj: Element, type: Optional[type] = ...
75+
) -> Optional[_E]: ...
76+
def __set__(self, obj: Element, value: Optional[_E]) -> None: ...

test_htmlgen/attribute.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import datetime
2+
from enum import Enum
23
from unittest import TestCase
34

4-
from asserts import assert_true, assert_false, assert_is_none, assert_equal
5+
from asserts import assert_true, assert_false, assert_is_none, assert_equal, \
6+
assert_raises
57

68
from htmlgen.attribute import (
79
html_attribute,
@@ -12,6 +14,7 @@
1214
list_html_attribute,
1315
data_attribute,
1416
css_class_attribute,
17+
enum_attribute,
1518
)
1619
from htmlgen.element import Element
1720

@@ -226,3 +229,51 @@ class MyElement(Element):
226229
assert_true(element.has_css_class("my-class"))
227230
element.attr = True
228231
assert_true(element.has_css_class("my-class"))
232+
233+
234+
class TestEnum(Enum):
235+
FOO = "foo"
236+
BAR = "bar"
237+
238+
239+
class EnumAttributeTest(TestCase):
240+
def test_enum(self):
241+
class MyElement(Element):
242+
attr = enum_attribute("attr", TestEnum)
243+
244+
element = MyElement("div")
245+
assert_is_none(element.attr)
246+
assert_equal("<div></div>", str(element))
247+
element.attr = TestEnum.BAR
248+
assert_equal(TestEnum.BAR, element.attr)
249+
assert_equal('<div attr="bar"></div>', str(element))
250+
element.attr = None
251+
assert_is_none(element.attr)
252+
assert_equal('<div></div>', str(element))
253+
254+
def test_default(self):
255+
class MyElement(Element):
256+
attr = enum_attribute("attr", TestEnum, default=TestEnum.FOO)
257+
258+
element = MyElement("div")
259+
assert_equal(TestEnum.FOO, element.attr)
260+
assert_equal("<div></div>", str(element))
261+
element.attr = TestEnum.BAR
262+
assert_equal(TestEnum.BAR, element.attr)
263+
assert_equal('<div attr="bar"></div>', str(element))
264+
element.attr = None
265+
assert_equal(TestEnum.FOO, element.attr)
266+
assert_equal("<div></div>", str(element))
267+
268+
def test_not_an_enum(self):
269+
with assert_raises(TypeError):
270+
class MyElement(Element):
271+
attr = enum_attribute("attr", "foo") # type: ignore
272+
273+
def test_invalid_value(self):
274+
class MyElement(Element):
275+
attr = enum_attribute("attr", TestEnum)
276+
277+
element = MyElement("div")
278+
with assert_raises(TypeError):
279+
element.attr = "foo" # type: ignore

0 commit comments

Comments
 (0)