Skip to content

Commit 8975bd7

Browse files
committed
Refactor SkillGUI to more completely extend GUIInterface with added unit tests
This defines support for a `gui` resource directory in skills
1 parent fc66fc4 commit 8975bd7

File tree

3 files changed

+87
-75
lines changed

3 files changed

+87
-75
lines changed

ovos_workshop/skills/base.py

Lines changed: 43 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from hashlib import md5
2222
from inspect import signature
2323
from itertools import chain
24-
from os.path import join, abspath, dirname, basename, isfile
24+
from os.path import join, abspath, dirname, basename, isfile, isdir
2525
from threading import Event
2626
from typing import List
2727

@@ -43,7 +43,7 @@
4343
from ovos_utils.intents import Intent, IntentBuilder
4444
from ovos_utils.intents.intent_service_interface import munge_regex, munge_intent_parser, IntentServiceInterface
4545
from ovos_utils.json_helper import merge_dict
46-
from ovos_utils.log import LOG
46+
from ovos_utils.log import LOG, deprecated
4747
from ovos_utils.messagebus import get_handler_name, create_wrapper, EventContainer, get_message_lang
4848
from ovos_utils.parse import match_one
4949
from ovos_utils.process_utils import RuntimeRequirements
@@ -81,76 +81,6 @@ def is_classic_core():
8181
return False # standalone
8282

8383

84-
class SkillGUI(GUIInterface):
85-
"""SkillGUI - Interface to the Graphical User Interface
86-
87-
Values set in this class are synced to the GUI, accessible within QML
88-
via the built-in sessionData mechanism. For example, in Python you can
89-
write in a skill:
90-
self.gui['temp'] = 33
91-
self.gui.show_page('Weather.qml')
92-
Then in the Weather.qml you'd access the temp via code such as:
93-
text: sessionData.time
94-
"""
95-
96-
def __init__(self, skill):
97-
self.skill = skill
98-
super().__init__(skill.skill_id, config=Configuration())
99-
100-
@property
101-
def bus(self):
102-
if self.skill:
103-
return self.skill.bus
104-
105-
@property
106-
def skill_id(self):
107-
return self.skill.skill_id
108-
109-
def setup_default_handlers(self):
110-
"""Sets the handlers for the default messages."""
111-
msg_type = self.build_message_type('set')
112-
self.skill.add_event(msg_type, self.gui_set)
113-
114-
def register_handler(self, event, handler):
115-
"""Register a handler for GUI events.
116-
117-
When using the triggerEvent method from Qt
118-
triggerEvent("event", {"data": "cool"})
119-
120-
Args:
121-
event (str): event to catch
122-
handler: function to handle the event
123-
"""
124-
msg_type = self.build_message_type(event)
125-
self.skill.add_event(msg_type, handler)
126-
127-
def _pages2uri(self, page_names):
128-
# Convert pages to full reference
129-
page_urls = []
130-
for name in page_names:
131-
page = self.skill._resources.locate_qml_file(name)
132-
if page:
133-
if self.remote_url:
134-
page_urls.append(self.remote_url + "/" + page)
135-
elif page.startswith("file://"):
136-
page_urls.append(page)
137-
else:
138-
page_urls.append("file://" + page)
139-
else:
140-
raise FileNotFoundError(f"Unable to find page: {name}")
141-
142-
return page_urls
143-
144-
def shutdown(self):
145-
"""Shutdown gui interface.
146-
147-
Clear pages loaded through this interface and remove the skill
148-
reference to make ref counting warning more precise.
149-
"""
150-
self.release()
151-
self.skill = None
152-
153-
15484
def simple_trace(stack_trace):
15585
"""Generate a simplified traceback.
15686
@@ -1954,3 +1884,44 @@ def get_scheduled_event_status(self, name):
19541884
def cancel_all_repeating_events(self):
19551885
"""Cancel any repeating events started by the skill."""
19561886
return self.event_scheduler.cancel_all_repeating_events()
1887+
1888+
1889+
class SkillGUI(GUIInterface):
1890+
def __init__(self, skill: BaseSkill):
1891+
"""
1892+
Wraps `GUIInterface` for use with a skill.
1893+
"""
1894+
self._skill = skill
1895+
skill_id = skill.skill_id
1896+
bus = skill.bus
1897+
config = skill.config_core.get('gui')
1898+
ui_directories = self._get_ui_directories()
1899+
GUIInterface.__init__(self, skill_id=skill_id, bus=bus, config=config,
1900+
ui_directories=ui_directories)
1901+
1902+
@property
1903+
@deprecated("`skill` should not be referenced directly", "0.1.0")
1904+
def skill(self):
1905+
return self._skill
1906+
1907+
def _get_ui_directories(self) -> dict:
1908+
"""
1909+
Get a dict of UI directories by GUI framework.
1910+
@return: Dict of framework name to UI resource directory
1911+
"""
1912+
ui_directories = dict()
1913+
base_directory = self._skill.root_dir
1914+
if isdir(join(base_directory, "gui")):
1915+
LOG.debug("Skill implements resources in `gui` directory")
1916+
ui_directories["all"] = join(base_directory, "gui")
1917+
return ui_directories
1918+
LOG.info("Checking for legacy UI directories")
1919+
# TODO: Add deprecation log after ovos-gui is implemented
1920+
if isdir(join(base_directory, "ui5")):
1921+
ui_directories["qt5"] = join(base_directory, "ui5")
1922+
if isdir(join(base_directory, "ui6")):
1923+
ui_directories["qt6"] = join(base_directory, "ui6")
1924+
if isdir(join(base_directory, "ui")) and "qt5" not in ui_directories:
1925+
LOG.debug("Handling `ui` directory as `qt5`")
1926+
ui_directories["qt5"] = join(base_directory, "ui")
1927+
return ui_directories

test/unittests/skills/gui/ui/test.qml

Whitespace-only changes.

test/unittests/test_skill.py

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import json
22
import unittest
3-
from unittest.mock import Mock
3+
from unittest.mock import Mock, patch
44

55
from ovos_bus_client import Message
66

77
from ovos_workshop.skills.ovos import OVOSSkill
88
from ovos_workshop.skills.mycroft_skill import MycroftSkill, is_classic_core
99
from mycroft.skills import MycroftSkill as CoreSkill
1010
from ovos_utils.messagebus import FakeBus
11-
from os.path import dirname
11+
from os.path import dirname, join
1212
from ovos_workshop.skill_launcher import SkillLoader
1313

1414

@@ -206,4 +206,45 @@ def test_load(self):
206206
self.assertTrue(args.startup_called)
207207
self.assertEqual(args.skill_id, "args")
208208
self.assertEqual(args.bus, bus)
209-
self.assertEqual(args.gui, gui)
209+
self.assertEqual(args.gui, gui)
210+
211+
212+
class TestSkillGui(unittest.TestCase):
213+
class LegacySkill(Mock):
214+
skill_id = "old_skill"
215+
bus = FakeBus()
216+
config_core = {"gui": {"test": True,
217+
"legacy": True}}
218+
root_dir = join(dirname(__file__), "skills", "gui")
219+
220+
class GuiSkill(Mock):
221+
skill_id = "new_skill"
222+
bus = FakeBus()
223+
config_core = {"gui": {"test": True,
224+
"legacy": False}}
225+
root_dir = join(dirname(__file__), "skills")
226+
227+
@patch("ovos_workshop.skills.base.GUIInterface.__init__")
228+
def test_skill_gui(self, interface_init):
229+
from ovos_utils.gui import GUIInterface
230+
from ovos_workshop.skills.base import SkillGUI
231+
232+
# Old skill with `ui` directory in root
233+
old_skill = self.LegacySkill()
234+
old_gui = SkillGUI(old_skill)
235+
self.assertEqual(old_gui.skill, old_skill)
236+
self.assertIsInstance(old_gui, GUIInterface)
237+
interface_init.assert_called_once_with(
238+
old_gui, skill_id=old_skill.skill_id, bus=old_skill.bus,
239+
config=old_skill.config_core['gui'],
240+
ui_directories={"qt5": join(old_skill.root_dir, "ui")})
241+
242+
# New skill with `gui` directory in root
243+
new_skill = self.GuiSkill()
244+
new_gui = SkillGUI(new_skill)
245+
self.assertEqual(new_gui.skill, new_skill)
246+
self.assertIsInstance(new_gui, GUIInterface)
247+
interface_init.assert_called_with(
248+
new_gui, skill_id=new_skill.skill_id, bus=new_skill.bus,
249+
config=new_skill.config_core['gui'],
250+
ui_directories={"all": join(new_skill.root_dir, "gui")})

0 commit comments

Comments
 (0)