-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Test and update the
wizard
module (#466)
Issue #343 is also fixed in this commit. The issue caused from when the accordion widget has all steps folder, the selected_index can be `None`, when the status of a step widget is reset, it triggers a widget update and hits at the exception.
- Loading branch information
1 parent
3b7aff7
commit f3fdff2
Showing
2 changed files
with
107 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,12 +4,12 @@ | |
* Carl Simon Adorf <[email protected]> | ||
""" | ||
from enum import Enum | ||
from threading import Thread | ||
from time import sleep, time | ||
import enum | ||
import threading | ||
import time | ||
|
||
import ipywidgets as ipw | ||
import traitlets | ||
import traitlets as tl | ||
|
||
|
||
class AtLeastTwoStepsWizardError(ValueError): | ||
|
@@ -21,10 +21,10 @@ def __init__(self, steps): | |
) | ||
|
||
|
||
class WizardAppWidgetStep(traitlets.HasTraits): | ||
class WizardAppWidgetStep(tl.HasTraits): | ||
"One step of a WizardAppWidget." | ||
|
||
class State(Enum): | ||
class State(enum.Enum): | ||
"""Each step is always in one specific state. | ||
The state is used to determine: | ||
|
@@ -68,8 +68,8 @@ class State(Enum): | |
# All error states have negative codes | ||
FAIL = -1 # the step has unrecoverably failed | ||
|
||
state = traitlets.UseEnum(State) | ||
auto_advance = traitlets.Bool() | ||
state = tl.UseEnum(State) | ||
auto_advance = tl.Bool() | ||
|
||
def can_reset(self): | ||
return hasattr(self, "reset") | ||
|
@@ -90,13 +90,13 @@ class WizardAppWidget(ipw.VBox): | |
@classmethod | ||
def icons(cls): | ||
"""Return the icon set and return animated icons based on the current time stamp.""" | ||
t = time() | ||
t = time.time() | ||
return { | ||
key: item if isinstance(item, str) else item[int(t * len(item) % len(item))] | ||
for key, item in cls.ICONS.items() | ||
} | ||
|
||
selected_index = traitlets.Int(allow_none=True) | ||
selected_index = tl.Int(allow_none=True) | ||
|
||
def __init__(self, steps, **kwargs): | ||
# The number of steps must be greater than one | ||
|
@@ -116,12 +116,14 @@ def __init__(self, steps, **kwargs): | |
|
||
# Automatically update titles to implement the "spinner" | ||
|
||
self._run_update_thread = True | ||
|
||
def spinner_thread(): | ||
while True: | ||
sleep(0.1) | ||
while self._run_update_thread: | ||
time.sleep(0.1) | ||
self._update_titles() | ||
|
||
Thread(target=spinner_thread).start() | ||
threading.Thread(target=spinner_thread).start() | ||
|
||
# Watch for changes to each step's state | ||
for widget in widgets: | ||
|
@@ -178,6 +180,8 @@ def _consider_auto_advance(self, _=None): | |
This is performed whenever the current step is within the SUCCESS state and has | ||
the auto_advance attribute set to True. | ||
""" | ||
if self.accordion.selected_index is None: # All children are hidden | ||
return | ||
|
||
with self.hold_trait_notifications(): | ||
index = self.accordion.selected_index | ||
|
@@ -196,7 +200,7 @@ def _update_step_state(self, _): | |
self._update_buttons() | ||
self._consider_auto_advance() | ||
|
||
@traitlets.observe("selected_index") | ||
@tl.observe("selected_index") | ||
def _observe_selected_index(self, change): | ||
"Activate/deactivate the next-button based on which step is selected." | ||
self._update_buttons() | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import ipywidgets as ipw | ||
import traitlets as tl | ||
|
||
|
||
def test_wizard_app_widget(): | ||
from aiidalab_widgets_base import WizardAppWidget, WizardAppWidgetStep | ||
|
||
class Step1(ipw.HBox, WizardAppWidgetStep): | ||
config = tl.Bool() | ||
|
||
def __init__(self, **kwargs): | ||
self.order_button = ipw.Button(description="Submit order", disabled=False) | ||
self.order_button.on_click(self.submit_order) | ||
super().__init__(children=[self.order_button], **kwargs) | ||
|
||
def submit_order(self, _=None): | ||
self.config = True | ||
|
||
@tl.default("config") | ||
def _default_config(self): | ||
return False | ||
|
||
def reset(self): | ||
self.config = False | ||
|
||
@tl.observe("config") | ||
def _observe_config(self, _=None): | ||
self.state = self.State.SUCCESS if self.config else self.State.INIT | ||
|
||
class Step2(ipw.HBox, WizardAppWidgetStep): | ||
config = tl.Bool() | ||
|
||
def __init__(self, **kwargs): | ||
self.results = ipw.HTML("Results") | ||
super().__init__(children=[self.results], **kwargs) | ||
|
||
def submit_order(self, _=None): | ||
pass | ||
|
||
@tl.default("config") | ||
def _default_config(self): | ||
return False | ||
|
||
@tl.observe("config") | ||
def _observe_config(self, change): | ||
if self.config: | ||
self.state = self.State.READY | ||
else: | ||
self.state = self.State.INIT | ||
|
||
s1 = Step1(auto_advance=True) | ||
s2 = Step2(auto_advance=True) | ||
tl.dlink((s1, "config"), (s2, "config")) | ||
|
||
widget = WizardAppWidget( | ||
steps=[ | ||
("Setup", s1), | ||
("View results", s2), | ||
], | ||
testing=True, | ||
) | ||
|
||
# Check initial state. | ||
assert s1.state == s1.State.INIT | ||
assert s2.state == s2.State.INIT | ||
assert widget.accordion.selected_index == 0 | ||
|
||
s1.order_button.click() | ||
|
||
# Check state after finishing the first step. | ||
assert s1.state == s1.State.SUCCESS | ||
assert s2.state == s2.State.READY | ||
assert widget.accordion.selected_index == 1 | ||
assert widget.next_button.disabled is True | ||
|
||
# Check state after resetting the app. | ||
widget.reset() | ||
assert s1.state == s1.State.INIT | ||
assert s2.state == s2.State.INIT | ||
assert widget.back_button.disabled is True | ||
|
||
# Check state after finishing the first step again. | ||
s1.order_button.click() | ||
assert s1.state == s1.State.SUCCESS | ||
widget.accordion.selected_index = None # All steps are closed. | ||
s1.config = False # This should trigger an attempt to advance to the next step. | ||
|
||
# Stop the thread that is running the header update to let pytest finish. | ||
widget._run_update_thread = False |