Skip to content

Commit 3b6dc24

Browse files
committed
add widget cue box and styles
* add css styles and function that sets them * add tests for widget cue box * add .ipynb to .gitignore
1 parent 2cd1a62 commit 3b6dc24

File tree

7 files changed

+326
-5
lines changed

7 files changed

+326
-5
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,5 @@ coverage.xml
1717
*~
1818
.idea
1919
.vscode
20+
21+
.ipynb

pyproject.toml

+3
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,6 @@ dynamic = ["version"]
3636
[tool.setuptools.dynamic]
3737
version = {attr = "scwidgets.__version__"}
3838
readme = {file = ["README.rst"]}
39+
40+
[tool.setuptools.package-data]
41+
scwidgets = ["css/widgets.css"]

src/scwidgets/__init__.py

+28
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,30 @@
11
__version__ = "0.0.0-dev"
22
__authors__ = "the scicode-widgets developer team"
3+
4+
import os
5+
6+
from IPython.core.display import HTML
7+
8+
from ._widget_cue_box import CheckCueBox, CueBox, SaveCueBox, UpdateCueBox
9+
10+
__all__ = ["CheckCueBox", "CueBox", "SaveCueBox", "UpdateCueBox"]
11+
12+
13+
def get_css_style() -> HTML:
14+
"""
15+
When reimporting scwidgets the objects displayed by the package are destroyed,
16+
because the cell output is refreshed.
17+
Since the module is loaded from cache when reimported one cannot rexecute the
18+
code on reimport, so we rely on the user to do it on a separate scell to keep the
19+
displayed html with the css style it avice in the notebook
20+
"""
21+
with open(os.path.join(os.path.dirname(__file__), "css/widgets.css")) as file:
22+
style_txt = file.read()
23+
24+
return HTML(
25+
"HTML with scicode-widget css style sheet. \
26+
Please keep this cell output alive. \
27+
<style>"
28+
+ style_txt
29+
+ "</style>"
30+
)

src/scwidgets/_widget_cue_box.py

+184
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
from typing import List, Optional, Union
2+
3+
from ipywidgets import Box, Widget
4+
from traitlets.utils.sentinel import Sentinel
5+
6+
7+
class CueBox(Box):
8+
"""
9+
A box around the widget :param widget_to_cue: that adds a visual cue defined in the
10+
:param css_style: when the trait :param traits_to_observe: in the widget :param
11+
widget_to_observe: changes.
12+
13+
:param widget_to_observe:
14+
The widget to observa if the :param traits_to_observe: has changed.
15+
:param traits_to_observe:
16+
The trait from the :param widget_to_observe: to observe if changed.
17+
Specify `traitlets.All` to observe all traits.
18+
:param widget_to_cue:
19+
The widget to wrap the box around to give a visual cue, once :param
20+
traits_to_observe: has changed
21+
If None, then the :param widget_to_cue: is set to :param widget_to_observe:.
22+
:param css_syle:
23+
- **initialization**: the css style of the box during initialization
24+
- **on_trait_changed**: the css style that is added when :param
25+
traits_to_observe: in widget :param widget_to_observe: changes.
26+
It is supposed to change the style of the box such that the user has a visual
27+
cue that :param widget_to_cue: has changed.
28+
29+
Further accepts the same (keyword) arguments as :py:class:`ipywidgets.Box`.
30+
"""
31+
32+
def __init__(
33+
self,
34+
widget_to_observe: Widget,
35+
traits_to_observe: Union[str, List[str], Sentinel, None] = "value",
36+
widget_to_cue: Optional[Widget] = None,
37+
css_style: Optional[dict] = None,
38+
*args,
39+
**kwargs,
40+
):
41+
self._widget_to_observe = widget_to_observe
42+
self._traits_to_observe = traits_to_observe
43+
44+
if widget_to_cue is None:
45+
self._widget_to_cue = widget_to_observe
46+
else:
47+
self._widget_to_cue = widget_to_cue
48+
49+
self._widget_to_observe = widget_to_observe
50+
51+
if css_style is None:
52+
self._css_style = {
53+
"base": "scwidget-cue-box",
54+
"cue": "scwidget-cue-box--cue",
55+
}
56+
else:
57+
self._css_style = css_style
58+
59+
super().__init__([self._widget_to_cue], *args, **kwargs)
60+
61+
self._widget_to_observe.observe(
62+
self._on_traits_to_observe_changed, traits_to_observe
63+
)
64+
self.add_class(self._css_style["base"])
65+
66+
@property
67+
def widget_to_observe(self):
68+
return self._widget_to_observe
69+
70+
@property
71+
def traits_to_observe(self):
72+
return self._traits_to_observe
73+
74+
@property
75+
def widget_to_cue(self):
76+
return self._widget_to_cue
77+
78+
def _on_traits_to_observe_changed(self, change: dict):
79+
self.add_class(self._css_style["cue"])
80+
81+
def remove_cue(self):
82+
self.remove_class(self._css_style["cue"])
83+
84+
85+
class SaveCueBox(CueBox):
86+
"""
87+
A box around the widget :param widget_to_cue: that adds a visual cue defined in the
88+
:param css_style: when the trait :param traits_to_observe: in the widget :param
89+
widget_to_observe: changes.
90+
91+
:param widget_to_observe:
92+
The widget to observa if the :param traits_to_observe: has changed.
93+
:param traits_to_observe:
94+
The trait from the :param widget_to_observe: to observe if changed.
95+
Specify `traitlets.All` to observe all traits.
96+
:param widget_to_cue:
97+
The widget to wrap the box around to give a visual cue, once :param
98+
traits_to_observe: has changed
99+
If None, then the :param widget_to_cue: is set to :param widget_to_observe:.
100+
101+
Further accepts the same (keyword) arguments as :py:class:`ipywidgets.Box`.
102+
"""
103+
104+
def __init__(
105+
self,
106+
widget_to_observe: Widget,
107+
traits_to_observe: Union[str, List[str], Sentinel] = "value",
108+
widget_to_cue: Optional[Widget] = None,
109+
*args,
110+
**kwargs,
111+
):
112+
css_style = {
113+
"base": "scwidget-save-cue-box",
114+
"cue": "scwidget-save-cue-box--cue",
115+
}
116+
super().__init__(widget_to_observe, traits_to_observe, widget_to_cue, css_style)
117+
118+
119+
class CheckCueBox(CueBox):
120+
"""
121+
A box around the widget :param widget_to_cue: that adds a visual cue defined in the
122+
:param css_style: when the trait :param traits_to_observe: in the widget :param
123+
widget_to_observe: changes.
124+
125+
:param widget_to_observe:
126+
The widget to observa if the :param traits_to_observe: has changed.
127+
:param traits_to_observe:
128+
The trait from the :param widget_to_observe: to observe if changed.
129+
Specify `traitlets.All` to observe all traits.
130+
:param widget_to_cue:
131+
The widget to wrap the box around to give a visual cue, once :param
132+
traits_to_observe: has changed
133+
If None, then the :param widget_to_cue: is set to :param widget_to_observe:.
134+
135+
Further accepts the same (keyword) arguments as :py:class:`ipywidgets.Box`.
136+
"""
137+
138+
def __init__(
139+
self,
140+
widget_to_observe: Widget,
141+
traits_to_observe: Union[str, List[str], Sentinel] = "value",
142+
widget_to_cue: Optional[Widget] = None,
143+
*args,
144+
**kwargs,
145+
):
146+
css_style = {
147+
"base": "scwidget-check-cue-box",
148+
"cue": "scwidget-check-cue-box--cue",
149+
}
150+
super().__init__(widget_to_observe, traits_to_observe, widget_to_cue, css_style)
151+
152+
153+
class UpdateCueBox(CueBox):
154+
"""
155+
A box around the widget :param widget_to_cue: that adds a visual cue defined in the
156+
:param css_style: when the trait :param traits_to_observe: in the widget :param
157+
widget_to_observe: changes.
158+
159+
:param widget_to_observe:
160+
The widget to observa if the :param traits_to_observe: has changed.
161+
:param traits_to_observe:
162+
The trait from the :param widget_to_observe: to observe if changed.
163+
Specify `traitlets.All` to observe all traits.
164+
:param widget_to_cue:
165+
The widget to wrap the box around to give a visual cue, once :param
166+
traits_to_observe: has changed
167+
If None, then the :param widget_to_cue: is set to :param widget_to_observe:.
168+
169+
Further accepts the same (keyword) arguments as :py:class:`ipywidgets.Box`.
170+
"""
171+
172+
def __init__(
173+
self,
174+
widget_to_observe: Widget,
175+
traits_to_observe: Union[str, List[str], Sentinel] = "value",
176+
widget_to_cue: Optional[Widget] = None,
177+
*args,
178+
**kwargs,
179+
):
180+
css_style = {
181+
"base": "scwidget-update-cue-box",
182+
"cue": "scwidget-update-cue-box--cue",
183+
}
184+
super().__init__(widget_to_observe, traits_to_observe, widget_to_cue, css_style)

src/scwidgets/css/widgets.css

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/* scwidgets default cue box */
2+
3+
.scwidget-cue-box {
4+
border-left : 0px solid;
5+
}
6+
7+
.scwidget-cue-box--cue {
8+
border-left : 5px solid;
9+
border-left-color: #000000;
10+
}
11+
12+
/* scwidgets save cue box */
13+
14+
.scwidget-save-cue-box {
15+
border-left : 0px solid;
16+
}
17+
18+
.scwidget-save-cue-box--cue {
19+
border-left : 5px solid;
20+
border-left-color: #77216F;
21+
}
22+
23+
/* scwidgets check cue box */
24+
25+
.scwidget-check-cue-box {
26+
border-left : 0px solid;
27+
}
28+
29+
.scwidget-check-cue-box--cue {
30+
border-left : 5px solid;
31+
border-left-color: #20b5e9;
32+
}
33+
34+
/* scwidgets update cue box */
35+
36+
.scwidget-update-cue-box {
37+
border-left : 0px solid;
38+
}
39+
40+
.scwidget-update-cue-box--cue {
41+
border-left : 5px solid;
42+
border-left-color: #e95420;
43+
}
44+

tests/notebooks/widgets.py tests/notebooks/widget_cue_box.py

+18-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,22 @@
1313
# name: python3
1414
# ---
1515

16-
from ipywidgets import Button
16+
# +
17+
from ipywidgets import Label
1718

18-
Button(description="Text")
19+
import scwidgets
20+
from scwidgets import CueBox
21+
22+
# -
23+
24+
scwidgets.get_css_style()
25+
26+
widget1 = Label("Text")
27+
cued_widget1 = CueBox(widget1)
28+
cued_widget1
29+
30+
widget2 = Label("Text")
31+
cued_widget2 = CueBox(widget2)
32+
cued_widget2
33+
34+
widget2.value = "Changed Text"

tests/test_widgets.py

+47-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import pytest
12
import requests
3+
from selenium.common.exceptions import NoSuchElementException
24
from selenium.webdriver.common.by import By
35

46

@@ -14,12 +16,54 @@ def test_notebook_running(notebook_service):
1416
assert response.status_code == 200
1517

1618

17-
def test_widgets(selenium_driver):
19+
def test_widget_cue_box(selenium_driver):
1820
"""
1921
Basic test checks if button with description "Text" exists
2022
2123
:param selenium_driver: see conftest.py
2224
"""
23-
driver = selenium_driver("tests/notebooks/widgets.ipynb")
25+
driver = selenium_driver("tests/notebooks/widget_cue_box.ipynb")
2426

25-
driver.find_element(By.XPATH, '//Button[text()="Text"]')
27+
# Each cell of the notebook, the cell number can be retrieved from the
28+
# attribute "data-windowed-list-index"
29+
nb_cells = driver.find_elements(
30+
By.CLASS_NAME, "lm-Widget.jp-Cell.jp-CodeCell.jp-Notebook-cell"
31+
)
32+
33+
# Checks if the labels widget with value "Text" exists in cell 2
34+
widget1 = nb_cells[2].find_element(
35+
By.CLASS_NAME, "lm-Widget.jupyter-widgets.widget-inline-hbox.widget-label"
36+
)
37+
assert widget1.text == "Text"
38+
39+
# Checks if the labels widget with unchanged value has the correct css style
40+
nb_cells[2].find_element(
41+
By.CLASS_NAME,
42+
"lm-Widget.lm-Panel.jupyter-widgets.widget-container"
43+
".widget-box.scwidget-cue-box.scwidget-cue-box",
44+
)
45+
# we assume error is raised because the widget should not be cued
46+
with pytest.raises(NoSuchElementException, match=r".*Unable to locate element.*"):
47+
nb_cells[2].find_element(
48+
By.CLASS_NAME,
49+
"lm-Widget.lm-Panel.jupyter-widgets.widget-container"
50+
".widget-box.scwidget-cue-box.scwidget-cue-box--cue",
51+
)
52+
53+
# Checks if the labels widget with value "Cahnged Text" exists in cell 3
54+
widget2 = nb_cells[3].find_element(
55+
By.CLASS_NAME, "lm-Widget.jupyter-widgets.widget-inline-hbox.widget-label"
56+
)
57+
assert widget2.text == "Changed Text"
58+
59+
# Checks if the labels widget with changed value has the correct css style
60+
nb_cells[3].find_element(
61+
By.CLASS_NAME,
62+
"lm-Widget.lm-Panel.jupyter-widgets.widget-container.widget-box"
63+
".scwidget-cue-box.scwidget-cue-box",
64+
)
65+
nb_cells[3].find_element(
66+
By.CLASS_NAME,
67+
"lm-Widget.lm-Panel.jupyter-widgets.widget-container.widget-box"
68+
".scwidget-cue-box.scwidget-cue-box--cue",
69+
)

0 commit comments

Comments
 (0)