Skip to content

Commit

Permalink
Typing stub package for dominate (#5)
Browse files Browse the repository at this point in the history
Provides types internally used from dominate to increase mypy coverage
  • Loading branch information
alexrudy authored Mar 27, 2024
1 parent 7a63ce3 commit e59a608
Show file tree
Hide file tree
Showing 16 changed files with 113 additions and 37 deletions.
1 change: 1 addition & 0 deletions .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ ignore =
B902
max-line-length = 120
min_python_version = 3.11
exclude = src/dominate-stubs
14 changes: 4 additions & 10 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,23 +32,20 @@ source = "vcs"
version-file = "src/bootlace/_version.py"

[project.urls]
Documentation = "https://github.com/unknown/bootlace#readme"
Issues = "https://github.com/unknown/bootlace/issues"
Source = "https://github.com/unknown/bootlace"

Documentation = "https://github.com/alexrudy/bootlace#readme"
Issues = "https://github.com/alexrudy/bootlace/issues"
Source = "https://github.com/alexrudy/bootlace"

[tool.hatch.build.targets.sdist]
include = ["src/"]


[tool.pytest.ini_options]
testpaths = "tests"
filterwarnings = "error"
addopts = [
"--cov-report=term-missing",
"--cov-config=pyproject.toml",
"--cov=bootlace",
"--ignore=setup.py",
]
log_level = "NOTSET"

Expand Down Expand Up @@ -83,7 +80,7 @@ omit = ["src/bootlace/_version.py", "src/bootlace/testing/*"]
line-length = 120

[tool.mypy]
files = "src/bootlace"
files = "src/bootlace,stubs/dominate-stubs"
python_version = "3.11"
show_error_codes = true
allow_redefinition = true
Expand All @@ -106,9 +103,6 @@ ignore_missing_imports = true
module = "tests.*"
disallow_subclassing_any = false

[[tool.mypy.overrides]]
module = "dominate.*"
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = "html5lib.*"
Expand Down
4 changes: 2 additions & 2 deletions requirements/dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ colorama==0.4.6
# via tox
distlib==0.3.8
# via virtualenv
filelock==3.13.2
filelock==3.13.3
# via
# tox
# virtualenv
Expand Down Expand Up @@ -69,7 +69,7 @@ more-itertools==10.2.0
# via
# jaraco-classes
# jaraco-functools
nh3==0.2.15
nh3==0.2.17
# via readme-renderer
nodeenv==1.8.0
# via pre-commit
Expand Down
5 changes: 3 additions & 2 deletions src/bootlace/breadcrumbs.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import attrs
from dominate import tags
from dominate.dom_tag import dom_tag
from dominate.util import text
from flask import Blueprint
from flask import current_app
Expand Down Expand Up @@ -129,7 +130,7 @@ def url(self) -> str:
"""The URL for the breadcrumb"""
return self.link.url

def __tag__(self) -> tags.html_tag:
def __tag__(self) -> dom_tag:
if self.active:
return text(self.title)

Expand Down Expand Up @@ -161,7 +162,7 @@ def push(self, crumb: Breadcrumb) -> None:
"""Add a new crumb to the beginning of the list"""
self.crumbs.insert(0, crumb)

def __tag__(self) -> tags.html_tag:
def __tag__(self) -> dom_tag:
if not self.crumbs:
return text("")

Expand Down
4 changes: 2 additions & 2 deletions src/bootlace/icon.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import attrs
from dominate import svg
from dominate import tags
from dominate.dom_tag import dom_tag
from flask import url_for


Expand Down Expand Up @@ -36,7 +36,7 @@ def url(self) -> str:
"""The URL for the SVG source for the icon"""
return url_for(self.endpoint, filename=self.filename, _anchor=self.name)

def __tag__(self) -> tags.html_tag:
def __tag__(self) -> dom_tag:
classes = ["bi", "me-1", "pe-none", "align-self-center"]
return svg.svg(
svg.use(xlink_href=self.url),
Expand Down
7 changes: 4 additions & 3 deletions src/bootlace/nav/bar.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import attrs
from dominate import tags
from dominate.dom_tag import dom_tag
from dominate.util import container

from .core import Link
Expand Down Expand Up @@ -67,7 +68,7 @@ class Brand(Link):
#: The ID of the brand
id: str = attrs.field(factory=element_id.factory("navbar-brand"))

def __tag__(self) -> tags.html_tag:
def __tag__(self) -> dom_tag:
a = as_tag(self.link)
a["class"] = "navbar-brand"
a["id"] = self.id
Expand All @@ -80,7 +81,7 @@ class NavBarCollapse(SubGroup):

id: str = attrs.field(factory=element_id.factory("navbar-collapse"))

def __tag__(self) -> tags.html_tag:
def __tag__(self) -> dom_tag:
button = tags.button(
type="button",
cls="navbar-toggler",
Expand Down Expand Up @@ -121,7 +122,7 @@ class NavBarSearch(NavElement):
method: str = "GET"
button: str | None = None

def __tag__(self) -> tags.html_tag:
def __tag__(self) -> dom_tag:
form = tags.form(id=self.id)
form.classes.add("d-flex")
form["role"] = "search"
Expand Down
17 changes: 11 additions & 6 deletions src/bootlace/nav/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import attrs
from dominate import tags
from dominate.dom_tag import dom_tag

from bootlace import links
from bootlace.image import Image
Expand Down Expand Up @@ -42,12 +43,15 @@ def enabled(self) -> bool:
"""Whether the element is enabled"""
return True

def __tag__(self) -> tags.html_tag:
def __tag__(self) -> dom_tag:
warnings.warn(BootlaceWarning(f"Unhandled element {self.__class__.__name__}"), stacklevel=2)
return tags.comment(f"unhandled element {self.__class__.__name__}")

def element_state(self, tag: tags.html_tag) -> tags.html_tag:
def element_state(self, tag: dom_tag) -> dom_tag:
"""Apply :attr:`active` and :attr:`enabled` states to the tag."""
if not isinstance(tag, tags.html_tag):
return tag

if self.active:
tag.classes.add("active")
tag.attributes["aria-current"] = "page"
Expand Down Expand Up @@ -93,10 +97,11 @@ def url(self) -> str:
"""The URL for the link."""
return self.link.url

def __tag__(self) -> tags.html_tag:
def __tag__(self) -> dom_tag:
a = as_tag(self.link)
a["id"] = self.id
a.classes.add("nav-link")
if isinstance(a, tags.html_tag):
a["id"] = self.id
a.classes.add("nav-link")

return self.element_state(a)

Expand All @@ -119,7 +124,7 @@ class Text(NavElement):
def enabled(self) -> bool:
return False

def __tag__(self) -> tags.html_tag:
def __tag__(self) -> dom_tag:
tag = tags.span(self.text, cls="nav-link")
return self.element_state(tag)

Expand Down
14 changes: 11 additions & 3 deletions src/bootlace/nav/nav.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import warnings

import attrs
from dominate import tags

from .core import NavAlignment
from .core import NavStyle
from .core import SubGroup
from bootlace.util import as_tag
from bootlace.util import BootlaceWarning
from bootlace.util import ids as element_id


Expand Down Expand Up @@ -66,9 +69,14 @@ def __tag__(self) -> tags.html_tag:
menu = tags.ul(cls="dropdown-menu", aria_labelledby=self.id)
for item in self.items:
tag = as_tag(item)
tag.classes.remove("nav-link")
if not any(cls.startswith("dropdown-") for cls in tag.classes):
tag.classes.add("dropdown-item")
if isinstance(tag, tags.html_tag):
tag.classes.remove("nav-link")
if not any(cls.startswith("dropdown-") for cls in tag.classes):
tag.classes.add("dropdown-item")
else:
warnings.warn(
BootlaceWarning(f"Item {item!r} is not an html tag, may not display properly"), stacklevel=2
)
menu.add(tags.li(tag, __pretty=False))
div.add(menu)
return div
3 changes: 2 additions & 1 deletion src/bootlace/table/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import attrs
from dominate import tags
from dominate.dom_tag import dom_tag

from bootlace.icon import Icon
from bootlace.util import as_tag
Expand Down Expand Up @@ -54,7 +55,7 @@ def attribute(self) -> str:
return self._attribute

@abstractmethod
def cell(self, value: Any) -> tags.html_tag:
def cell(self, value: Any) -> dom_tag:
"""Return the cell for the column as an HTML tag."""
raise NotImplementedError("Subclasses must implement this method")

Expand Down
7 changes: 4 additions & 3 deletions src/bootlace/table/columns.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import attrs
from dominate import tags
from dominate.dom_tag import dom_tag
from dominate.util import text
from flask import url_for

Expand All @@ -19,7 +20,7 @@ class Column(ColumnBase):
No special formatting is applied to the attribute, it is rendered as text."""

def cell(self, value: Any) -> tags.html_tag:
def cell(self, value: Any) -> dom_tag:
"""Return the cell for the column as an HTML tag."""
return text(str(getattr(value, self.attribute)))

Expand Down Expand Up @@ -49,7 +50,7 @@ class CheckColumn(ColumnBase):
#: The icon for a false value
no: Icon = attrs.field(default=Icon("x", width=16, height=16))

def cell(self, value: Any) -> tags.html_tag:
def cell(self, value: Any) -> dom_tag:
"""Return the cell for the column as an HTML tag."""
if getattr(value, self.attribute):
return as_tag(self.yes)
Expand All @@ -60,6 +61,6 @@ def cell(self, value: Any) -> tags.html_tag:
class Datetime(ColumnBase):
"""A column which shows a datetime attribute as an ISO formatted string."""

def cell(self, value: Any) -> tags.html_tag:
def cell(self, value: Any) -> dom_tag:
"""Return the cell for the column as an HTML tag."""
return text(getattr(value, self.attribute).isoformat())
11 changes: 6 additions & 5 deletions src/bootlace/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

import attrs
from dominate import tags
from dominate.dom_tag import dom_tag
from dominate.util import container
from dominate.util import text
from flask import request
Expand Down Expand Up @@ -41,13 +42,13 @@ class BootlaceWarning(UserWarning):

def _monkey_patch_dominate() -> None:
"""Monkey patch the dominate tags to support class attribute manipulation"""
tags.html_tag.classes = property(lambda self: Classes(self))
tags.html_tag.classes = property(lambda self: Classes(self)) # type: ignore


class Taggable(Protocol):
"""Protocol for objects that can be converted to a tag."""

def __tag__(self) -> tags.html_tag:
def __tag__(self) -> dom_tag:
"""Convert the object to a dominate tag.
This method gives objects control over how they are processed by :func:`as_tag`. It should return a
Expand All @@ -61,13 +62,13 @@ def __tag__(self) -> tags.html_tag:


#: A type that can be converted to a tag
IntoTag: TypeAlias = Taggable | tags.html_tag
IntoTag: TypeAlias = Taggable | dom_tag

#: A type that can be converted to a tag via :func:`as_tag`
MaybeTaggable: TypeAlias = IntoTag | str | Iterable[Taggable | tags.html_tag]
MaybeTaggable: TypeAlias = IntoTag | str | Iterable[Taggable | dom_tag]


def as_tag(item: MaybeTaggable) -> tags.html_tag:
def as_tag(item: MaybeTaggable) -> dom_tag:
"""Convert an item to a dominate tag.
:mod:`bootlace` uses :mod:`dominate` to render HTML. To do this, objects implement the :class:`Taggable` protocol,
Expand Down
Empty file.
19 changes: 19 additions & 0 deletions stubs/dominate-stubs/dom_tag.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from typing import Any
from typing import overload
from typing import Iterator
from typing import Self

class dom_tag:
attributes: dict[str, str]
def __init__(self, *args: Any, **kwargs: Any) -> None: ...
@overload
def __getitem__(self, key: str) -> str: ...
@overload
def __getitem__(self, key: int) -> "Self": ...
def __setitem__(self, key: str, value: str) -> None: ...
def add(self, *args: str | int | "dom_tag") -> "dom_tag": ...
def remove(self, *args: "dom_tag") -> "dom_tag": ...
def clear(self) -> None: ...
def __len__(self) -> int: ...
def __iter__(self) -> "Iterator[dom_tag]": ...
def render(self) -> str: ...
4 changes: 4 additions & 0 deletions stubs/dominate-stubs/svg.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .dom_tag import dom_tag

class svg(dom_tag): ...
class use(dom_tag): ...
36 changes: 36 additions & 0 deletions stubs/dominate-stubs/tags.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from typing import Iterator

from .dom_tag import dom_tag

class Classes:
def __contains__(self, cls: str) -> bool: ...
def __len__(self) -> int: ...
def __iter__(self) -> Iterator[str]: ...
def add(self, *classes: str) -> "html_tag": ...
def remove(self, *classes: str) -> "html_tag": ...
def swap(self, old: str, new: str) -> "html_tag": ...

class html_tag(dom_tag):
@property
def classes(self) -> Classes: ...

class a(html_tag): ...
class button(html_tag): ...
class div(html_tag): ...
class form(html_tag): ...
class hr(html_tag): ...
class img(html_tag): ...
class input_(html_tag): ...
class label(html_tag): ...
class li(html_tag): ...
class nav(html_tag): ...
class ol(html_tag): ...
class span(html_tag): ...
class table(html_tag): ...
class tbody(html_tag): ...
class td(html_tag): ...
class th(html_tag): ...
class thead(html_tag): ...
class tr(html_tag): ...
class ul(html_tag): ...
class comment(dom_tag): ...
4 changes: 4 additions & 0 deletions stubs/dominate-stubs/util.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .dom_tag import dom_tag

class container(dom_tag): ...
class text(dom_tag): ...

0 comments on commit e59a608

Please sign in to comment.