From 5bea3b6aa4b2343d41eb9310e7ff765d8034523e Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Sat, 11 Nov 2017 19:44:23 +0100 Subject: [PATCH 1/2] implement attributes using descriptor protocol --- NEWS.rst | 6 ++ htmlgen/attribute.py | 136 ++++++++++++++++++++++++------------------ htmlgen/attribute.pyi | 53 ++++++++++++---- htmlgen/form.py | 2 +- 4 files changed, 128 insertions(+), 69 deletions(-) diff --git a/NEWS.rst b/NEWS.rst index e83b7ba..4fd4464 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,6 +1,12 @@ News in version 0.99.1 ====================== +API-Incompatible Changes +------------------------ + + * html_attribute() at al. are now directly implemented using the descriptor + protocol, and not derived from property. + Improvements ------------ diff --git a/htmlgen/attribute.py b/htmlgen/attribute.py index e1a40c2..527ef5e 100644 --- a/htmlgen/attribute.py +++ b/htmlgen/attribute.py @@ -3,7 +3,8 @@ from htmlgen.timeutil import parse_rfc3339_partial_time -def html_attribute(attribute_name, default=None): +class html_attribute(object): + """Add an attribute to an HTML element. >>> from htmlgen import Element @@ -30,19 +31,22 @@ def html_attribute(attribute_name, default=None): """ - def get(self): - return self.get_attribute(attribute_name, default=default) + def __init__(self, attribute_name, default=None): + self._attribute_name = attribute_name + self._default = default + + def __get__(self, obj, _=None): + return obj.get_attribute(self._attribute_name, default=self._default) - def set_(self, value): - if value is None or value == default: - self.remove_attribute(attribute_name) + def __set__(self, obj, value): + if value is None or value == self._default: + obj.remove_attribute(self._attribute_name) else: - self.set_attribute(attribute_name, value) + obj.set_attribute(self._attribute_name, value) - return property(get, set_) +class boolean_html_attribute(object): -def boolean_html_attribute(attribute_name): """Add a boolean attribute to an HTML element. >>> from htmlgen import Element @@ -59,19 +63,21 @@ def boolean_html_attribute(attribute_name): """ - def get(self): - return self.get_attribute(attribute_name) == attribute_name + def __init__(self, attribute_name): + self._attribute_name = attribute_name + + def __get__(self, obj, _=None): + return obj.get_attribute(self._attribute_name) == self._attribute_name - def set_(self, value): + def __set__(self, obj, value): if value: - self.set_attribute(attribute_name, attribute_name) + obj.set_attribute(self._attribute_name, self._attribute_name) else: - self.remove_attribute(attribute_name) + obj.remove_attribute(self._attribute_name) - return property(get, set_) +class int_html_attribute(object): -def int_html_attribute(attribute_name, default=None): """Add an attribute to an HTML element that accepts only integers. >>> from htmlgen import Element @@ -98,22 +104,25 @@ def int_html_attribute(attribute_name, default=None): """ - def get(self): - value = self.get_attribute(attribute_name, default=default) + def __init__(self, attribute_name, default=None): + self._attribute_name = attribute_name + self._default = default + + def __get__(self, obj, _=None): + value = obj.get_attribute(self._attribute_name, default=self._default) if value is None: return None return int(value) - def set_(self, value): - if value is None or value == default: - self.remove_attribute(attribute_name) + def __set__(self, obj, value): + if value is None or value == self._default: + obj.remove_attribute(self._attribute_name) else: - self.set_attribute(attribute_name, str(value)) + obj.set_attribute(self._attribute_name, str(value)) - return property(get, set_) +class float_html_attribute(object): -def float_html_attribute(attribute_name, default=None): """Add an attribute to an HTML element that accepts only numbers. >>> from htmlgen import Element @@ -140,22 +149,25 @@ def float_html_attribute(attribute_name, default=None): """ - def get(self): - value = self.get_attribute(attribute_name, default=default) + def __init__(self, attribute_name, default=None): + self._attribute_name = attribute_name + self._default = default + + def __get__(self, obj, _=None): + value = obj.get_attribute(self._attribute_name, default=self._default) if value is None: return None return float(value) - def set_(self, value): - if value is None or value == default: - self.remove_attribute(attribute_name) + def __set__(self, obj, value): + if value is None or value == self._default: + obj.remove_attribute(self._attribute_name) else: - self.set_attribute(attribute_name, str(value)) + obj.set_attribute(self._attribute_name, str(value)) - return property(get, set_) +class time_html_attribute(object): -def time_html_attribute(attribute_name, default=None): """Add an attribute to an HTML element that accepts only time values. >>> from htmlgen import Element @@ -183,22 +195,25 @@ def time_html_attribute(attribute_name, default=None): """ - def get(self): - value = self.get_attribute(attribute_name) + def __init__(self, attribute_name, default=None): + self._attribute_name = attribute_name + self._default = default + + def __get__(self, obj, _=None): + value = obj.get_attribute(self._attribute_name) if value is None: - return default + return self._default return parse_rfc3339_partial_time(value) - def set_(self, value): - if value is None or value == default: - self.remove_attribute(attribute_name) + def __set__(self, obj, value): + if value is None or value == self._default: + obj.remove_attribute(self._attribute_name) else: - self.set_attribute(attribute_name, str(value)) + obj.set_attribute(self._attribute_name, str(value)) - return property(get, set_) +class list_html_attribute(object): -def list_html_attribute(attribute_name): """Add an attribute to an HTML element that accepts a list of strings. >>> from htmlgen import Element @@ -215,25 +230,29 @@ def list_html_attribute(attribute_name): """ - def get(self): - value = self.get_attribute(attribute_name) + def __init__(self, attribute_name): + self._attribute_name = attribute_name + + def __get__(self, obj, _=None): + value = obj.get_attribute(self._attribute_name) return value.split(",") if value else [] - def set_(self, value): + def __set__(self, obj, value): if value: - self.set_attribute(attribute_name, ",".join(value)) + obj.set_attribute(self._attribute_name, ",".join(value)) else: - self.remove_attribute(attribute_name) + obj.remove_attribute(self._attribute_name) + - return property(get, set_) +class data_attribute(html_attribute): + def __init__(self, data_name, default=None): + attribute_name = "data-" + data_name + super(data_attribute, self).__init__(attribute_name, default) -def data_attribute(data_name, default=None): - attribute_name = "data-" + data_name - return html_attribute(attribute_name, default) +class css_class_attribute(object): -def css_class_attribute(css_class): """Add a boolean attribute to an HTML element that add a CSS class. >>> from htmlgen import Element @@ -251,13 +270,14 @@ def css_class_attribute(css_class): """ - def get(self): - return self.has_css_class(css_class) + def __init__(self, css_class): + self._css_class = css_class - def set_(self, value): + def __get__(self, obj, _=None): + return obj.has_css_class(self._css_class) + + def __set__(self, obj, value): if value: - self.add_css_classes(css_class) + obj.add_css_classes(self._css_class) else: - self.remove_css_classes(css_class) - - return property(get, set_) + obj.remove_css_classes(self._css_class) diff --git a/htmlgen/attribute.pyi b/htmlgen/attribute.pyi index ac689ab..fceeb6f 100644 --- a/htmlgen/attribute.pyi +++ b/htmlgen/attribute.pyi @@ -1,10 +1,43 @@ -from typing import Any - -def html_attribute(attribute_name: str, default: Any = ...) -> property: ... -def boolean_html_attribute(attribute_name: str) -> property: ... -def int_html_attribute(attribute_name: str, default: Any = ...) -> property: ... -def float_html_attribute(attribute_name: str, default: Any = ...) -> property: ... -def time_html_attribute(attribute_name: str, default: Any = ...) -> property: ... -def list_html_attribute(attribute_name: str) -> property: ... -def data_attribute(data_name: str, default: Any = ...) -> property: ... -def css_class_attribute(css_class: str) -> property: ... +import datetime +from typing import Optional, List, Iterable + +from htmlgen.element import Element + + +class html_attribute(object): + def __init__(self, attribute_name: str, default: Optional[str] = ...) -> None: ... + def __get__(self, obj: Element, type: Optional[type] = ...) -> Optional[str]: ... + def __set__(self, obj: Element, value: Optional[str]) -> None: ... + +class boolean_html_attribute(object): + def __init__(self, attribute_name: str) -> None: ... + def __get__(self, obj: Element, type_: Optional[type] = ...) -> bool: ... + def __set__(self, obj: Element, value: bool) -> None: ... + +class int_html_attribute(object): + def __init__(self, attribute_name: str, default: Optional[int] = ...) -> None: ... + def __get__(self, obj: Element, type_: Optional[type] = ...) -> Optional[int]: ... + def __set__(self, obj: Element, value: Optional[int]) -> None: ... + +class float_html_attribute(object): + def __init__(self, attribute_name: str, default: Optional[float] = ...) -> None: ... + def __get__(self, obj: Element, type_: Optional[type] = ...) -> Optional[float]: ... + def __set__(self, obj: Element, value: Optional[float]) -> None: ... + +class time_html_attribute(object): + def __init__(self, attribute_name: str, default: Optional[datetime.time] = None) -> None: ... + def __get__(self, obj: Element, type_: Optional[type] = ...) -> Optional[datetime.time]: ... + def __set__(self, obj: Element, value: Optional[datetime.time]) -> None: ... + +class list_html_attribute(object): + def __init__(self, attribute_name: str) -> None: ... + def __get__(self, obj: Element, type_: Optional[type] = ...) -> List[str]: ... + def __set__(self, obj: Element, value: Iterable[str]) -> None: ... + +class data_attribute(html_attribute): + def __init__(self, data_name: str, default: Optional[str] = None) -> None: ... + +class css_class_attribute(object): + def __init__(self, css_class: str) -> None: ... + def __get__(self, obj: Element, type_: Optional[type] = ...) -> bool: ... + def __set__(self, obj: Element, value: bool) -> None: ... diff --git a/htmlgen/form.py b/htmlgen/form.py index 1a20938..244401b 100644 --- a/htmlgen/form.py +++ b/htmlgen/form.py @@ -85,7 +85,7 @@ def __init__(self, type_="text", name=""): value = html_attribute("value", default="") readonly = boolean_html_attribute("readonly") disabled = boolean_html_attribute("disabled") - type = html_attribute("type") # type: ignore + type = html_attribute("type") placeholder = html_attribute("placeholder") size = int_html_attribute("size") focus = boolean_html_attribute("autofocus") From 6a672afc35ce3afdb33b96e67481b5ea4c0d90eb Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Sat, 11 Nov 2017 19:45:24 +0100 Subject: [PATCH 2/2] fix import --- htmlgen/document.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/htmlgen/document.py b/htmlgen/document.py index 04e92e3..184cf6f 100644 --- a/htmlgen/document.py +++ b/htmlgen/document.py @@ -1,5 +1,6 @@ from htmlgen.attribute import html_attribute -from htmlgen.element import Generator, Element, NonVoidElement, VoidElement +from htmlgen.generator import Generator +from htmlgen.element import Element, NonVoidElement, VoidElement MIME_JAVASCRIPT = "text/javascript"