Repository: madelyneriksen/dashml

Issue:
Rendering for non-string types.
# Overview

DashML should be able to render non-string types.

## How

* Most classes should probably be cast to a string, as a form of "least surprise".
* Boolean attributes should obey HTML5 boolean rules, which means `true` should keep the attribute and `false` should remove it.

This will be implemented in `core.swap_attributes`.

Discussion:

Patch:
diff --git a/README.md b/README.md
index 7f89aaf..56f6bb1 100644
--- a/README.md
+++ b/README.md
@@ -242,7 +242,6 @@ python bench.py # or `make bench`
 DashML could not be built without these libraries:
 
 * [lxml](https://lxml.de) for creating a _fast_ XML library.
-* [Pallets Projects](https://palletsprojects.com/) for creating MarkupSafe
 
 ## License
 
diff --git a/dashml/core.py b/dashml/core.py
index 3f44b5c..bc5a3ad 100644
--- a/dashml/core.py
+++ b/dashml/core.py
@@ -8,21 +8,17 @@ else:
     VarArg = lambda x: t.List[x]
     KwArg = lambda x: t.Dict[str, x]
 
-from lxml.etree import Element
+from lxml.etree import _Element as Element
 import lxml.html as html
 from lxml.builder import E as raw_builder
 
-import markupsafe
-
 
 __all__ = ["_", "render", "unsafe_from_string"]
 
 
 T = t.TypeVar("T")
-
-
-Child = t.Union[Element, str, None]
-Prop = t.Union[str, int, bool, None]
+Child = t.Union[Element, str, int, float, None]
+Prop = t.Union[str, int, float, bool, None]
 
 
 class Builder:
@@ -65,23 +61,18 @@ def render(ele: Element) -> str:
     Returns:
         (str) Rendered utf-8 string of the element.
     """
-    raw: bytes = html.tostring(ele)
-    return raw.decode("utf-8")
+    return html.tostring(ele).decode("utf-8")
 
 
 _ = Builder()
 
 
-@singledispatch
 def safe(var: Child) -> Child:
     """Mark a value as safe."""
-    return var
-
-
-@safe.register
-def __safe_string(var: str) -> str:
-    """Escape a string."""
-    return str(markupsafe.escape(var))  # pragma: no cover
+    if isinstance(var, Element):
+        return var
+    else:
+        return str(var)
 
 
 # Like `className` or `htmlFor` in React.
@@ -98,10 +89,19 @@ def swap_attributes(attrs: t.Dict[str, Prop]) -> None:
     class_name and html_for.
     """
     for key, value in attrs.items():
+        if isinstance(value, bool) or value is None:
+            # Convert booleans/Nonetypes into HTML5 compatible booleans.
+            if value:
+                attrs[key] = ""
+            else:
+                del attrs[key]
+                continue
         if key.startswith("data_") or key.startswith("aria_"):
-            attrs[key.replace("_", "-")] = attrs.pop(key)
+            attrs[key.replace("_", "-")] = str(attrs.pop(key))
         elif key in RESERVED_PAIRS:
-            attrs[RESERVED_PAIRS[key]] = attrs.pop(key)
+            attrs[RESERVED_PAIRS[key]] = str(attrs.pop(key))
+        else:
+            attrs[key] = str(value)
 
 
 def unsafe_from_string(unsafe_string: str) -> Element:
diff --git a/dashml/stubs/lxml/etree.pyi b/dashml/stubs/lxml/etree.pyi
index cd35f86..f6b3467 100644
--- a/dashml/stubs/lxml/etree.pyi
+++ b/dashml/stubs/lxml/etree.pyi
@@ -1,3 +1,5 @@
 import typing as t
 
-class Element: ...
+class _Element: ...
+
+def Element() -> _Element: ...
diff --git a/docs/quickstart.md b/docs/quickstart.md
index 319b6fe..be6a18f 100644
--- a/docs/quickstart.md
+++ b/docs/quickstart.md
@@ -8,7 +8,7 @@ Like most Python packages, DashML is available on the Python Package Index for i
 pip install dashml
 ```
 
-When you install DashML, its two dependencies [MarkupSafe](https://pypi.org/project/MarkupSafe/) and [lxml](https://pypi.org/project/lxml/) will be installed as well.
+When you install DashML, its dependency [lxml](https://pypi.org/project/lxml/) will be installed as well.
 
 ## Create A Component
 
diff --git a/requirements.txt b/requirements.txt
index ab3bad2..ab90481 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,1 @@
 lxml
-markupsafe
diff --git a/setup.py b/setup.py
index 43ca931..7b15847 100644
--- a/setup.py
+++ b/setup.py
@@ -17,7 +17,6 @@ setuptools.setup(
     packages=["dashml"],
     install_requires=[
         "lxml",
-        "markupsafe",
     ],
     classifiers=[
         "Programming Language :: Python :: 3",


Test patch:
diff --git a/dashml/tests/test_html_escaping.py b/dashml/tests/test_html_escaping.py
index 0101ed1..8a47864 100644
--- a/dashml/tests/test_html_escaping.py
+++ b/dashml/tests/test_html_escaping.py
@@ -8,3 +8,23 @@ def test_render_script_fails():
     """Gratuitous test of the classic alert."""
     value = render(_.p("<script>alert('Hello, world!')</script>"))
     assert value == ("<p>&lt;script&gt;alert('Hello, world!')&lt;/script&gt;</p>")
+
+
+def test_render_boolean():
+    """Test the rendering of boolean attributes."""
+    val = render(_.input(type="checkbox", checked=True))
+    assert val == '<input type="checkbox" checked>'
+
+    val = render(_.option("California", selected=True))
+    assert val == "<option selected>California</option>"
+
+
+def test_render_numbers():
+    val = render(_.p(8))
+    assert val == "<p>8</p>"
+
+    val = render(_.p(8.8))
+    assert val == "<p>8.8</p>"
+
+    val = render(_.div(data_number=8))
+    assert val == '<div data-number="8"></div>'
diff --git a/dashml/tests/test_html_rendering.py b/dashml/tests/test_html_rendering.py
index 91e2dbb..17ed220 100644
--- a/dashml/tests/test_html_rendering.py
+++ b/dashml/tests/test_html_rendering.py
@@ -1,6 +1,8 @@
 """Generic tests for rendering DashML components."""
 
 
+import pytest
+
 from dashml import _, render