Skip to content

Commit ceb6311

Browse files
authored
Make as_fillable_container() and as_fill_item() not mutate their inputs (#862)
1 parent a5de5fa commit ceb6311

File tree

9 files changed

+73
-174
lines changed

9 files changed

+73
-174
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2222

2323
* Update penguins example to credit Allison Horst and drop usage of `shiny.experimental` (#798).
2424

25+
* `as_fillable_container()` and `as_fill_item()` no longer mutate the `Tag` object that was passed in. Instead, it returns a new `Tag` object. Also closed #856: these functions now put the `html-fill-container` and `html-fill-item` CSS classes last, instead of first. (#862)
26+
2527

2628
## [0.6.0] - 2023-08-08
2729

shiny/api-examples/as_fill_item/app.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from shiny.ui import fill
77

88

9-
def outer_inner() -> tuple[htmltools.Tag, htmltools.Tag]:
9+
def outer_inner() -> htmltools.Tag:
1010
inner = ui.div(
1111
id="inner",
1212
style=htmltools.css(
@@ -22,17 +22,17 @@ def outer_inner() -> tuple[htmltools.Tag, htmltools.Tag]:
2222
border="3px red solid",
2323
),
2424
)
25-
return outer, inner
25+
return outer
2626

2727

28-
outer0, inner0 = outer_inner()
28+
outer0 = outer_inner()
2929

30-
outer1, inner1 = outer_inner()
31-
fill.as_fill_item(inner1)
30+
outer1 = outer_inner()
31+
outer1.children[0] = fill.as_fill_item(outer1.children[0])
3232

33-
outer2, inner2 = outer_inner()
34-
fill.as_fillable_container(outer2)
35-
fill.as_fill_item(inner2)
33+
outer2 = outer_inner()
34+
outer2 = fill.as_fillable_container(outer2)
35+
outer2.children[0] = fill.as_fill_item(outer2.children[0])
3636

3737

3838
app_ui = ui.page_fluid(

shiny/api-examples/as_fillable_container/app.py

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from shiny.ui import fill
77

88

9-
def outer_inner() -> tuple[htmltools.Tag, htmltools.Tag]:
9+
def outer_inner() -> htmltools.Tag:
1010
inner = ui.div(
1111
id="inner",
1212
style=htmltools.css(
@@ -22,23 +22,22 @@ def outer_inner() -> tuple[htmltools.Tag, htmltools.Tag]:
2222
border="3px red solid",
2323
),
2424
)
25-
return outer, inner
25+
return outer
2626

2727

28-
outer0, inner0 = outer_inner()
29-
outer1, inner1 = outer_inner()
30-
outer2, inner2 = outer_inner()
28+
outer0 = outer_inner()
3129

32-
fill.as_fillable_container(outer2)
33-
34-
fill.as_fillable_container(outer2)
35-
fill.as_fill_item(inner2)
30+
outer1 = outer_inner()
31+
outer1 = fill.as_fillable_container(outer1)
3632

33+
outer2 = outer_inner()
34+
outer2 = fill.as_fillable_container(outer2)
35+
outer2.children[0] = fill.as_fill_item(outer2.children[0])
3736

3837
app_ui = ui.page_fluid(
3938
ui.markdown(
4039
"""\
41-
# `as_fill_container()`
40+
# `as_fillable_container()`
4241
4342
For an item to fill its parent element,
4443
* the item must have `as_fill_item()` be called on it
@@ -49,7 +48,7 @@ def outer_inner() -> tuple[htmltools.Tag, htmltools.Tag]:
4948
),
5049
ui.row(
5150
ui.column(4, ui.h5("Default behavior")),
52-
ui.column(4, ui.h5(ui.markdown("`as_fill_container(red)`"))),
51+
ui.column(4, ui.h5(ui.markdown("`as_fillable_container(red)`"))),
5352
ui.column(
5453
4,
5554
ui.h5(ui.markdown("`as_fill_item(blue)` + `as_fillable_container(red)`")),

shiny/experimental/ui/_deprecated.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -922,7 +922,7 @@ def as_fill_carrier(
922922
warn_deprecated(
923923
"`shiny.experimental.ui.as_fill_carrier()` is deprecated. "
924924
"This method will be removed in a future version, "
925-
"please use `shiny.ui.fill.as_fill_container()` and `shiny.ui.fill.as_fillable_item()` instead."
925+
"please use `shiny.ui.fill.as_fillable_container()` and `shiny.ui.fill.as_fillable_item()` instead."
926926
)
927927

928928
if min_height is not None:

shiny/ui/_navs.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
from ._card import CardItem, WrapperCallable, card, card_body, card_footer, card_header
3535
from ._html_deps_shinyverse import components_dependency
3636
from ._sidebar import Sidebar, layout_sidebar
37-
from ._tag import tag_add_style
3837
from .css import CssUnit, as_css_padding, as_css_unit
3938
from .fill import as_fill_item, as_fillable_container
4039

@@ -1129,7 +1128,7 @@ def _make_tabs_fillable(
11291128
# must to be a fillable container.
11301129
content = as_fillable_container(as_fill_item(content))
11311130

1132-
for child in content.children:
1131+
for i, child in enumerate(content.children):
11331132
# Only work on Tags
11341133
if not isinstance(child, Tag):
11351134
continue
@@ -1146,9 +1145,11 @@ def _make_tabs_fillable(
11461145
padding=as_css_padding(padding),
11471146
__bslib_navbar_margin="0;" if navbar else None,
11481147
)
1149-
child = tag_add_style(child, styles)
1148+
child.add_style(cast(str, styles))
11501149
child = as_fillable_container(as_fill_item(child))
11511150

1151+
content.children[i] = child
1152+
11521153
return content
11531154

11541155

shiny/ui/_page.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ def page_fillable(
314314
if not isinstance(body, Tag) or body.name != "body":
315315
raise ValueError("Expected a <body> tag")
316316

317-
as_fillable_container(body)
317+
page.children[1] = as_fillable_container(body)
318318

319319
return page
320320

shiny/ui/_tag.py

Lines changed: 1 addition & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from typing import TypeVar, cast, overload
44

5-
from htmltools import Tag, TagAttrs, TagAttrValue, TagChild, div
5+
from htmltools import TagAttrs, TagAttrValue, TagChild, div
66

77
TagChildT = TypeVar("TagChildT", bound=TagChild)
88

@@ -60,104 +60,3 @@ def trinary(x: bool | str | None) -> None | str:
6060
return "true"
6161
else:
6262
return "false"
63-
64-
65-
TagT = TypeVar("TagT", bound="Tag")
66-
67-
# Tag.add_class(x: str) -> Self[Tag]:
68-
# cls = self.attrs.get("class")
69-
# if cls:
70-
# x = cls + " " + x
71-
# self.attrs["class"] = x
72-
# return self
73-
74-
75-
# Do not export
76-
# "Prepend" version of `tag.add_class(class_)`
77-
def tag_prepend_class(tag: TagT, *class_: str | None) -> TagT:
78-
classes = (
79-
*class_,
80-
tag.attrs.get("class"),
81-
)
82-
classes = [c for c in classes if c is not None]
83-
if len(classes) == 0:
84-
return tag
85-
tag.attrs["class"] = " ".join(classes)
86-
return tag
87-
88-
89-
def tag_remove_class(tag: TagT, *class_: str | None) -> TagT:
90-
"""
91-
Remove a class value from the HTML class attribute.
92-
93-
Parameters
94-
----------
95-
*class_
96-
The class name to remove.
97-
98-
Returns
99-
-------
100-
:
101-
The modified tag.
102-
"""
103-
cls = tag.attrs.get("class")
104-
# If no class values to remove from, quit
105-
if not cls:
106-
return tag
107-
108-
# Remove any `None` values
109-
set_to_remove = {c for c in class_ if c is not None}
110-
111-
# If no classes to remove, quit
112-
if len(set_to_remove) == 0:
113-
return tag
114-
115-
# Get new set of classes
116-
# Order matters, otherwise we could use `set()` subtraction: `set(cls.split(" ")) - set(class_)`
117-
new_cls: list[str] = []
118-
for cls_val in cls.split(" "):
119-
if cls_val not in set_to_remove:
120-
new_cls.append(cls_val)
121-
122-
# If no classes left, remove the attribute
123-
if len(new_cls) == 0:
124-
# If here, `attrs.class` must exist
125-
tag.attrs.pop("class")
126-
return tag
127-
128-
# Otherwise, set the new class
129-
tag.attrs["class"] = " ".join(new_cls)
130-
return tag
131-
132-
133-
def tag_add_style(
134-
tag: TagT,
135-
*style: str | None,
136-
) -> TagT:
137-
"""
138-
Add a style value(s) to the HTML style attribute.
139-
140-
Parameters
141-
----------
142-
*style
143-
CSS properties and values already properly formatted. Each should already contain trailing semicolons.
144-
145-
See Also
146-
--------
147-
~htmltools.css
148-
149-
Returns
150-
-------
151-
:
152-
The modified tag.
153-
"""
154-
styles = (
155-
tag.attrs.get("style"),
156-
*style,
157-
)
158-
non_none_style_tuple = (s for s in styles if s is not None)
159-
style_str = "".join(non_none_style_tuple)
160-
161-
if style_str:
162-
tag.attrs["style"] = style_str
163-
return tag

shiny/ui/fill/_fill.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
from __future__ import annotations
22

3+
from copy import copy
34
from typing import TypeVar
45

56
from htmltools import Tag, TagAttrs
67

78
from ..._docstring import add_example
89
from .._html_deps_shinyverse import fill_dependency
9-
from .._tag import tag_prepend_class, tag_remove_class
1010

1111
__all__ = (
1212
"as_fillable_container",
@@ -31,17 +31,16 @@
3131

3232

3333
@add_example()
34-
# TODO-future-API; These methods mutate their input values. Should they return a new object instead? Or not return at all (like `LIST.sort()`)?
3534
def as_fillable_container(
3635
tag: TagT,
3736
) -> TagT:
38-
tag_prepend_class(tag, FILL_CONTAINER_CLASS)
39-
tag.append(fill_dependency())
40-
return tag
37+
res = copy(tag)
38+
res.add_class(FILL_CONTAINER_CLASS)
39+
res.append(fill_dependency())
40+
return res
4141

4242

4343
@add_example()
44-
# TODO-future-API; These methods mutate their input values. Should they return a new object instead? Or not return at all (like `LIST.sort()`)?
4544
def as_fill_item(
4645
tag: TagT,
4746
) -> TagT:
@@ -64,17 +63,18 @@ def as_fill_item(
6463
Returns
6564
-------
6665
:
67-
The original :class:`~htmltools.Tag` object (`tag`) with additional attributes
68-
(and an :class:`~htmltools.HTMLDependency`).
66+
A copy of the original :class:`~htmltools.Tag` object (`tag`) with additional
67+
attributes (and an :class:`~htmltools.HTMLDependency`).
6968
7069
See Also
7170
--------
7271
* :func:`~shiny.ui.fill.as_fillable_container`
7372
* :func:`~shiny.ui.fill.remove_all_fill`
7473
"""
75-
tag_prepend_class(tag, FILL_ITEM_CLASS)
76-
tag.append(fill_dependency())
77-
return tag
74+
res = copy(tag)
75+
res.add_class(FILL_ITEM_CLASS)
76+
res.append(fill_dependency())
77+
return res
7878

7979

8080
def remove_all_fill(
@@ -109,8 +109,8 @@ def remove_all_fill(
109109
* :func:`~shiny.ui.fill.as_fillable_container`
110110
"""
111111

112-
tag_remove_class(tag, FILL_CONTAINER_CLASS)
113-
tag_remove_class(tag, FILL_ITEM_CLASS)
112+
tag.remove_class(FILL_CONTAINER_CLASS)
113+
tag.remove_class(FILL_ITEM_CLASS)
114114
return tag
115115

116116

0 commit comments

Comments
 (0)