Skip to content

Commit f031e5f

Browse files
committed
tests(Theme): Add tests and fix a few things
1 parent 350811e commit f031e5f

File tree

2 files changed

+140
-6
lines changed

2 files changed

+140
-6
lines changed

shiny/ui/_theme.py

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

3+
import copy
34
import os
45
import tempfile
56
from textwrap import dedent
@@ -101,11 +102,21 @@ def preset(self, value: ShinyThemePreset) -> None:
101102
check_is_valid_preset(value)
102103
self._preset = value
103104

104-
if self._css != "precompiled":
105+
has_customizations = (
106+
len(self._functions) > 0
107+
or len(self._defaults) > 0
108+
or len(self._mixins) > 0
109+
or len(self._rules) > 0
110+
)
111+
112+
if has_customizations:
105113
self._css = ""
114+
else:
115+
self._css = "precompiled" if value in ShinyThemePresetsBundled else ""
116+
106117
self._css_temp_srcdir = None
107118

108-
def add_functions(self: T, *args: list[str]) -> T:
119+
def add_functions(self: T, *args: str) -> T:
109120
"""
110121
Add custom Sass functions to the theme.
111122
@@ -119,10 +130,12 @@ def add_functions(self: T, *args: list[str]) -> T:
119130
The Sass functions to add as a single or multiple strings.
120131
"""
121132
self._css = ""
122-
self._functions.extend(dedent_array(*args))
133+
self._functions.extend(dedent_array(args))
123134
return self
124135

125-
def add_defaults(self: T, *args: str, **kwargs: dict[str, str]) -> T:
136+
def add_defaults(
137+
self: T, *args: str, **kwargs: str | float | int | bool | None
138+
) -> T:
126139
"""
127140
Add custom default values to the theme.
128141
@@ -142,7 +155,11 @@ def add_defaults(self: T, *args: str, **kwargs: dict[str, str]) -> T:
142155
`.add_defaults("$primary-color: #ff0000;")`.
143156
"""
144157
if len(args) > 0 and len(kwargs) > 0:
158+
# Python forces positional arguments to come _before_ kwargs, but default
159+
# argument order might matter. To be safe, we force users to pick one order.
145160
raise ValueError("Cannot provide both positional and keyword arguments")
161+
elif len(args) == 0 and len(kwargs) == 0:
162+
return self
146163

147164
defaults: list[str] = list(args)
148165

@@ -151,6 +168,8 @@ def add_defaults(self: T, *args: str, **kwargs: dict[str, str]) -> T:
151168
key.replace("_", "-")
152169
if isinstance(value, bool):
153170
value = "true" if value else "false"
171+
elif value is None:
172+
value = "null"
154173
defaults.append(f"${key}: {value};")
155174

156175
# Add args to the front of _defaults
@@ -232,14 +251,14 @@ def to_css(self) -> str:
232251
if self._css:
233252
if self._css == "precompiled":
234253
return self._read_precompiled_css()
235-
return self._css
254+
return copy.copy(self._css)
236255

237256
check_libsass_installed()
238257
import sass
239258

240259
self._css = sass.compile(string=self.to_sass())
241260

242-
return self._css
261+
return copy.copy(self._css)
243262

244263
def _read_precompiled_css(self) -> str:
245264
path = path_pkg_preset(self._preset, "preset.min.css")

tests/pytest/test_theme.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import pytest
2+
3+
from shiny.ui import Theme
4+
from shiny.ui._theme import (
5+
ShinyThemePreset,
6+
ShinyThemePresets,
7+
ShinyThemePresetsBundled,
8+
)
9+
10+
11+
def test_theme_stores_values_correctly():
12+
theme = (
13+
Theme("shiny")
14+
.add_defaults(
15+
headings_color="red",
16+
bar_color="purple",
17+
select_color_text="green",
18+
bslib_dashboard_design=True,
19+
)
20+
.add_functions("@function get-color($color) { @return $color; }")
21+
.add_rules(
22+
"""
23+
strong { color: $primary; }
24+
.sidebar-title { color: $danger; }
25+
""",
26+
".special { color: $warning; }",
27+
)
28+
.add_mixins("@mixin alert { color: $alert; }")
29+
)
30+
31+
check_vars = [
32+
"_preset",
33+
"name",
34+
"_functions",
35+
"_defaults",
36+
"_mixins",
37+
"_rules",
38+
"_css",
39+
]
40+
41+
theme_dict = {k: v for k, v in vars(theme).items() if k in check_vars}
42+
43+
assert theme_dict == {
44+
"_preset": "shiny",
45+
"name": None,
46+
"_functions": ["@function get-color($color) { @return $color; }"],
47+
"_defaults": [
48+
"$headings_color: red;",
49+
"$bar_color: purple;",
50+
"$select_color_text: green;",
51+
"$bslib_dashboard_design: true;",
52+
],
53+
"_mixins": ["@mixin alert { color: $alert; }"],
54+
"_rules": [
55+
"\nstrong { color: $primary; }\n.sidebar-title { color: $danger; }\n",
56+
".special { color: $warning; }",
57+
],
58+
"_css": "",
59+
}
60+
61+
62+
def test_theme_preset_must_be_valid():
63+
with pytest.raises(ValueError, match="Invalid preset"):
64+
Theme("not_a_valid_preset") # type: ignore
65+
66+
67+
@pytest.mark.parametrize("preset", ShinyThemePresets)
68+
def test_theme_css_compiles_and_is_cached(preset: ShinyThemePreset):
69+
theme = Theme(preset)
70+
if preset in ShinyThemePresetsBundled:
71+
assert theme._css == "precompiled"
72+
else:
73+
assert theme._css == ""
74+
75+
# Adding rules resets the theme's cached CSS
76+
theme.add_rules(".MY_RULE { color: red; }")
77+
assert theme._css == ""
78+
79+
first_css = theme.to_css()
80+
assert first_css.find("Bootstrap") != -1
81+
assert first_css.find(".MY_RULE") != -1
82+
assert theme.to_css() == first_css # Cached value is returned
83+
84+
# Adding another customization resets the theme's cached CSS
85+
theme.add_mixins(".MY_MIXIN { color: blue; }")
86+
second_css = theme.to_css()
87+
assert second_css != first_css, "First and second compiled CSS are the same"
88+
assert second_css.find("Bootstrap") != -1
89+
assert second_css.find(".MY_MIXIN") != -1
90+
91+
92+
def test_theme_update_preset():
93+
theme = Theme("shiny")
94+
assert theme._preset == "shiny"
95+
assert theme._css == "precompiled" if "shiny" in ShinyThemePresetsBundled else ""
96+
97+
theme.preset = "bootstrap"
98+
assert theme._preset == "bootstrap"
99+
assert theme._css == (
100+
"precompiled" if "bootstrap" in ShinyThemePresetsBundled else ""
101+
)
102+
103+
theme.preset = "sketchy"
104+
assert theme._preset == "sketchy"
105+
assert theme._css == (
106+
"precompiled" if "sketchy" in ShinyThemePresetsBundled else ""
107+
)
108+
109+
with pytest.raises(ValueError, match="Invalid preset"):
110+
theme.preset = "not_a_valid_preset" # type: ignore
111+
112+
113+
def test_theme_defaults_positional_or_keyword():
114+
with pytest.raises(ValueError, match="Cannot provide both"):
115+
Theme("shiny").add_defaults("$color: red;", other_color="green")

0 commit comments

Comments
 (0)