2626 TypeVar ,
2727 Union ,
2828 cast ,
29+ overload ,
2930)
3031
3132# Even though TypedDict is available in Python 3.8, because it's used with NotRequired,
3637else :
3738 from typing_extensions import Never , NotRequired , TypedDict
3839
40+ if sys .version_info >= (3 , 13 ):
41+ from typing import TypeIs
42+ else :
43+ from typing_extensions import TypeIs
44+
3945from typing import Literal , Protocol , SupportsIndex , runtime_checkable
4046
4147from packaging .version import Version
6369 "TagNode" ,
6470 "TagFunction" ,
6571 "Tagifiable" ,
72+ "consolidate_attrs" ,
6673 "head_content" ,
74+ "is_tag_child" ,
75+ "is_tag_node" ,
6776 "wrap_displayhook_handler" ,
6877)
6978
@@ -100,13 +109,23 @@ class MetadataNode:
100109unnamed arguments to Tag functions like `div()`.
101110"""
102111
103- TagNode = Union ["Tagifiable" , "Tag" , MetadataNode , "ReprHtml" , str ]
112+ # NOTE: If this type is updated, please update `is_tag_node()`
113+ TagNode = Union [
114+ "Tagifiable" ,
115+ # "Tag", # Tag is Tagifiable, do not include here
116+ # "TagList", # TagList is Tagifiable, do not include here
117+ MetadataNode ,
118+ "ReprHtml" ,
119+ str ,
120+ "HTML" ,
121+ ]
104122"""
105123Types of objects that can be a node in a `Tag` tree. Equivalently, these are the valid
106124elements of a `TagList`. Note that this type represents the internal structure of items
107125in a `TagList`; the user-facing type is `TagChild`.
108126"""
109127
128+ # NOTE: If this type is updated, please update `is_tag_child()`
110129TagChild = Union [
111130 TagNode ,
112131 "TagList" ,
@@ -120,13 +139,85 @@ class MetadataNode:
120139will be flattened and normalized to `TagNode` objects.
121140"""
122141
142+
123143# These two types existed in htmltools 0.14.0 and earlier. They are here so that
124144# existing versions of Shiny will be able to load, but users of those existing packages
125145# will see type errors, which should encourage them to upgrade Shiny.
126146TagChildArg = Never
127147TagAttrArg = Never
128148
129149
150+ # # No use yet, so keeping code commented for now
151+ # TagNodeT = TypeVar("TagNodeT", bound=TagNode)
152+ # """
153+ # Type variable for `TagNode`.
154+ # """
155+
156+ TagChildT = TypeVar ("TagChildT" , bound = TagChild )
157+ """
158+ Type variable for `TagChild`.
159+ """
160+
161+
162+ def is_tag_node (x : object ) -> TypeIs [TagNode ]:
163+ """
164+ Check if an object is a `TagNode`.
165+
166+ Note: The type hint is `TypeIs[TagNode]` to allow for type checking of the
167+ return value. (`TypeIs` is imported from `typing_extensions` for Python < 3.13.)
168+
169+ Parameters
170+ ----------
171+ x
172+ Object to check.
173+
174+ Returns
175+ -------
176+ :
177+ `True` if the object is a `TagNode`, `False` otherwise.
178+ """
179+ # Note: Tag and TagList are both Tagifiable
180+ return isinstance (x , (Tagifiable , MetadataNode , ReprHtml , str , HTML ))
181+
182+
183+ def is_tag_child (x : object ) -> TypeIs [TagChild ]:
184+ """
185+ Check if an object is a `TagChild`.
186+
187+ Note: The type hint is `TypeIs[TagChild]` to allow for type checking of the
188+ return value. (`TypeIs` is imported from `typing_extensions` for Python < 3.13.)
189+
190+ Parameters
191+ ----------
192+ x
193+ Object to check.
194+
195+ Returns
196+ -------
197+ :
198+ `True` if the object is a `TagChild`, `False` otherwise.
199+ """
200+
201+ if is_tag_node (x ):
202+ return True
203+ if x is None :
204+ return True
205+ if isinstance (
206+ x ,
207+ (
208+ # TagNode, # Handled above
209+ TagList ,
210+ float ,
211+ # None, # Handled above
212+ Sequence ,
213+ ),
214+ ):
215+ return True
216+
217+ # Could not determine the type
218+ return False
219+
220+
130221@runtime_checkable
131222class Tagifiable (Protocol ):
132223 """
@@ -1744,6 +1835,61 @@ def head_content(*args: TagChild) -> HTMLDependency:
17441835 return HTMLDependency (name = name , version = "0.0" , head = head )
17451836
17461837
1838+ # If no children are provided, it will not be able to infer the type of `TagChildT`.
1839+ # Using `TagChild`, even though the list will be empty.
1840+ @overload
1841+ def consolidate_attrs (
1842+ * args : TagAttrs ,
1843+ ** kwargs : TagAttrValue ,
1844+ ) -> tuple [TagAttrs , list [TagChild ]]: ...
1845+
1846+
1847+ # Same as original definition
1848+ @overload
1849+ def consolidate_attrs (
1850+ * args : TagChildT | TagAttrs ,
1851+ ** kwargs : TagAttrValue ,
1852+ ) -> tuple [TagAttrs , list [TagChildT ]]: ...
1853+
1854+
1855+ def consolidate_attrs (
1856+ * args : TagChildT | TagAttrs ,
1857+ ** kwargs : TagAttrValue ,
1858+ ) -> tuple [TagAttrs , list [TagChildT ]]:
1859+ """
1860+ Consolidate attributes and children into a single tuple.
1861+
1862+ Convenience function to consolidate attributes and children into a single tuple. All
1863+ `args` that are not dictionaries are considered children. This helps preserve the
1864+ non-attribute elements within `args`. To extract the attributes, all `args` and
1865+ `kwargs` are passed to `Tag` function and the attributes (`.attrs`) are extracted
1866+ from the resulting `Tag` object.
1867+
1868+ Parameters
1869+ ----------
1870+ *args
1871+ Child elements to this tag and attribute dictionaries.
1872+ **kwargs
1873+ Named attributes to this tag.
1874+
1875+ Returns
1876+ -------
1877+ :
1878+ A tuple of attributes and children. The attributes are a dictionary of combined
1879+ named attributes, and the children are a list of unaltered child elements.
1880+ """
1881+ tag = Tag ("consolidate_attrs" , * args , ** kwargs )
1882+
1883+ # Convert to a plain dict to avoid getting custom methods from TagAttrDict
1884+ # Cast to `TagAttrs` as that is the common type used by py-shiny
1885+ attrs = cast (TagAttrs , dict (tag .attrs ))
1886+
1887+ # Do not alter/flatten children structure (like `TagList` does)
1888+ # Instead, return all `args` who are not dictionaries
1889+ children = [child for child in args if not isinstance (child , dict )]
1890+ return (attrs , children )
1891+
1892+
17471893# =============================================================================
17481894# Utility functions
17491895# =============================================================================
@@ -1756,7 +1902,7 @@ def _tagchilds_to_tagnodes(x: Iterable[TagChild]) -> list[TagNode]:
17561902 for i , item in enumerate (result ):
17571903 if isinstance (item , (int , float )):
17581904 result [i ] = str (item )
1759- elif not isinstance (item , ( HTML , Tagifiable , Tag , MetadataNode , ReprHtml , str ) ):
1905+ elif not is_tag_node (item ):
17601906 raise TypeError (
17611907 f"Invalid tag item type: { type (item )} . "
17621908 + "Consider calling str() on this value before treating it as a tag item."
0 commit comments